Avius
Avius

Reputation: 6264

How to detect when Angular Universal switches from SSR content to client content?

When an Angular Universal application bootstraps in the browser, it replaces the whole server-side-rendered page with content generated in the client. Although in many cases it is possible to make this transition seem seamless by employing certain techniques, it is still not hydration.

Here's an example issue that arises from this. Say there's a server-side-rendered input field and I manage to type something into this input before the client app bootstraps. If that happens, I will lose whatever I entered as soon as the client application bootstraps.

While I am not too concerned with this happening to my users, it does cause issues with E2E tests. The driver starts typing as soon as there are input nodes available and then the data entered is lost once the client app is bootstrapped. I would like to force the test driver to wait until the client app is bootstrapped.

My question: how can I detect the moment when Angular has switched it's server-side-rendered content for the client-generated application?

It seems that whenever bootstrapModule() resolves, this has already happened. Am I correct and is this reliable?

Upvotes: 1

Views: 1874

Answers (1)

bwegs
bwegs

Reputation: 3767

It sounds like you are suffering from the unintended side effects of rendering all your app content twice, first on the server and then once again on the client.

One step to optimize this and prevent the issue you're encountering is by utilizing custom directives, one that you can apply to content that you only want to be rendered on the server and another for content you only want rendered by the client (browser). For example, the following directive can be used to mark content that you only want to be rendered on the server:

@Directive({
    selector: '[appShellRender]'
})
export class AppShellRenderDirective implements OnInit {

constructor(
    private viewContainer: ViewContainerRef,
    private templateRef: TemplateRef<any>,
    @Inject(PLATFORM_ID) private platformId) {}
      
ngOnInit() {
    // if we're on the server then render this content 
    if (isPlatformServer(this.platformId)) {
        this.viewContainer.createEmbeddedView(this.templateRef);
    }
    else {
        this.viewContainer.clear();
    }
  }
}

And here's what the directive might look like in action:

<mat-sidenav-container fullscreen>
    ...
    <router-outlet></router-outlet>
  
    <!-- only render spinner on the server! -->
    <div class="spinner-container" *appShellRender>
        <mat-spinner></mat-spinner>
    </div>
</mat-sidenav-container>

Once you've implemented a more finely-grained rendering solution you should only be rendering your inputs once and your test scripts will thank you.

For a more thorough explanation and additional examples I highly recommend checking out this in-depth Angular Universal guide provided by Angular University that served as the primary source for this answer.

Upvotes: 1

Related Questions