angularangular-templateangular-lifecycle-hooksngtemplateoutlet

ngRouterOutlet and component lifecycle - problem with rendering custom template


Introduction

I have created a shared table component that uses Angular Material to render a table based on data and column configurations. The component has default templates for string and number data types, but users should be able to provide custom templates for any column.

I have an example app with three routes: Home, Page1 (with a table), and Page2 (also with a table). However, when the user navigates to the table pages, the table is not fully rendered on the first load. It only renders fully after the user navigates to the same page again. I suspect this is due to the lifecycle and lack of templates on the first render, which throws the Error: ExpressionChangedAfterItHasBeenCheckedError.

What I expect

I expect the table to render fully on the first load without any errors.

Also, I have a side question: since the ExpressionChangedAfterItHasBeenCheckedError is thrown, is it possible that the issue will resolve itself in Prod?

Example

I have replicated this issue on Stackblitz, but to fully experience the issue, please visit this full page: https://stackblitz-starters-5gez6t.stackblitz.io/

Here is the Stackblitz editor link: https://stackblitz.com/edit/stackblitz-starters-5gez6t


Solution

  • Short Answer

    I suspect this is due to the lifecycle and lack of templates on the first render,

    You are exactly right. You can simply update your @ViewChild decorators for your default ng-template references in table.component.ts, to use {static: true} instead of {static: false}

    Updated stackblitz editor view: https://stackblitz.com/edit/stackblitz-starters-co2me9?file=src%2Ftable%2Ftable.component.ts

    Updated stackblitz app share link: https://stackblitz-starters-co2me9.stackblitz.io

    Long answer: Lifecycle hooks and @ViewChild {static: true/false}

    Per the docs, the static: boolean property of the ViewChild decorator metadata dictates when the view query will be resolved, before (in ngOnInit) or after change detection (in ngAfterViewInit), and it defaults to false.

    I would recommend that you always try to use {static: true}, unless you absolutely can't. The main benefit of this approach is not requiring the use of additional lifecycle hooks (like ngAfterViewInit). If your element is dynamically rendered (ex: the element in your component template is inside of an *ngIf or *ngFor), you can't use static:true. In other words, if the element isn't dynamic, it's static.

    In this particular case, all of your default column templates are static (not dynamic) ng-templates which live at the bottom of your table.component.html, so you should reference them with static:true

    You can also see in your existing logs, where you have added most of the component lifecycle hooks (as well as a log in your getTemplate component function) exactly what's happening. The getTemplate function is being used in the component template, which will run the function before the ngAfterViewInit hook, and because you're currently using static:false the ViewChild references being logged are undefined until the log statement inside ngAfterViewInit.

    Side Answer RE: ExpressionChangedAfterItHasBeenChecked

    Per the docs, this is an error that is only thrown in development mode, specifically caused by the extra change detection cycle that runs in development mode. So as long as you build and deploy the app with a production configuration (ex: ng build --prod) you will not see the error.

    However, just because you won't see those error logs in the console in the deployed version of the app doesn't mean you can simply ignore them during development. This particular bug you're seeing would still occur, you just would not see the ExpressionChangedAfterItHasBeenChecked error.