Steps
Setting up a simple test angular universal, I wanted to try to send some initial data to the components, and have it server render all the data. To test it I used postman as it does not run any JavaScript
To get up and running call ng new univesaltest1
and to enable universal called ng add @nguniversal/express-engine
To have some components to test with run ng generate component test --module app.module.ts
and ng generate component test2 --module app.module.ts
Then making a simple message model and an injection token
import { InjectionToken } from "@angular/core";
export class MessageModel {
constructor(
public message: string
) { }
}
// injecton token
export const MESSAGE = new InjectionToken<MessageModel>('message');
In the server.ts file set up the initial model and some query string code. Then provide the model using the MESSAGE injection token.
// All regular routes use the Universal engine
server.get('*', (req, res) => {
const message:string = req.query['message']?.toString() ?? 'hello from server.ts';
const messageModel = new MessageModel(message);
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }, {provide: MESSAGE, useValue: messageModel}] });
});
In the test.component.ts inject platform id and on the server run code inject the message model using the MESSAGE injection token. I need the transfer state as the rendering on the client will set the message, overriding the server rendered message. If I was to disable JavaScript in the browser, I do not need the transfer state.
import { isPlatformServer } from '@angular/common';
import { Component, inject, Inject, PLATFORM_ID } from '@angular/core';
import { makeStateKey, StateKey, TransferState } from '@angular/platform-browser';
import { MESSAGE, MessageModel } from 'src/models/message.model';
@Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.css']
})
export class TestComponent {
// transfare state key
private messageStateKey:StateKey<MessageModel> = makeStateKey<MessageModel>('message');
public message: MessageModel = this.state.get(this.messageStateKey, new MessageModel('hello from test.component.ts'));
constructor(@Inject(PLATFORM_ID) private platformId:object, private state:TransferState)
{
// check if we are on the server
if(isPlatformServer(this.platformId)) {
const serverMessage = inject(MESSAGE);
this.message = serverMessage;
// set the message in the state
this.state.set(this.messageStateKey, serverMessage);
}
}
}
To make it more interesting input the message model into test2 component using this code
test.component.html
<div >
<app-test2 [message]="message"></app-test2>
</div>
test2.component.ts
import { Component, Input } from '@angular/core';
import { MessageModel } from 'src/models/message.model';
@Component({
selector: 'app-test2',
templateUrl: './test2.component.html',
styleUrls: ['./test2.component.css']
})
export class Test2Component {
@Input() message: MessageModel = new MessageModel('hello from test2.component.ts');
}
test2.component.html
<p>{{message.message}}</p>
Now we are all set. To test the code start the universal version using npm run dev:ssr
Calling it postman and all the html with the model data is rendered on the server
Request: localhost:4200?message="Hello from query string"
<body>
<app-root _nghost-sc3="" ng-version="15.2.4" ng-server-context="ssr">
<app-test _ngcontent-sc3="" _nghost-sc2="">
<div _ngcontent-sc2="">
<app-test2 _ngcontent-sc2="" _nghost-sc1="" ng-reflect-message="[object Object]">
<p _ngcontent-sc1="">"Hello from query string"</p>
</app-test2>
</div>
</app-test>
</app-root>
<script src="runtime.js" type="module"></script>
<script src="polyfills.js" type="module"></script>
<script src="vendor.js" type="module"></script>
<script src="main.js" type="module"></script>
<script id="serverApp-state" type="application/json">
{&q;message&q;:{&q;message&q;:&q;\&q;Hello from query string\&q;&q;}}
</script>
</body>
The Road
I did some mistakes when trying to get this to work. In the first try I tried to inject the Message model using the @Inject in the constructor. The problem then is that the client part of the application is not able to make sense of the provided message model.
I go the error
R3InjectorError(AppModule)[InjectionToken message -> InjectionToken message -> InjectionToken message]:
NullInjectorError: No provider for InjectionToken message!
Trying to provide the injection token in the app.module.ts using
providers: [{provide: MESSAGE, useValue: null }],
and then
@Inject(MESSAGE) private messageModel:MessageModel
The client app model overrides the provided message with null. So you could se the message blink before the client set it to null.
The solution was to not try to mix the server injected code with the clint part of the code.
Top comments (0)