DEV Community

gaotter
gaotter

Posted on

Send initial data to a component in Angular Universal

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');
Enter fullscreen mode Exit fullscreen mode

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}] });
  });

Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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');
}

Enter fullscreen mode Exit fullscreen mode

test2.component.html

<p>{{message.message}}</p>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

Trying to provide the injection token in the app.module.ts using

  providers: [{provide: MESSAGE, useValue: null }],
Enter fullscreen mode Exit fullscreen mode

and then

 @Inject(MESSAGE) private messageModel:MessageModel
Enter fullscreen mode Exit fullscreen mode

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)