How to find out when a list (asynchronous) has been rendered to the DOM
In this article I will show you an 'old' way by using the @ViewChildren Angular decoraters and how you can do it easier by using the new Signal way of Angular.
Old way example:
import {
Component,
ElementRef,
QueryList,
ViewChildren,
AfterViewInit,
} from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';
import { of, delay } from 'rxjs';
interface Todo {
name: string;
completed: boolean;
id: number;
}
@Component({
selector: 'app-root',
standalone: true,
imports: [AsyncPipe],
template: `
<ul>
@for(todo of todos$ | async; track todo.id) {
<li #todoItem>{{ todo.name }}</li>
}
</ul>
`,
})
export class App implements AfterViewInit {
@ViewChildren('todoItem') todoElements!: QueryList<ElementRef>;
todos$ = of([
{ name: 'todo 1', completed: false, id: 1 },
{ name: 'todo 2', completed: false, id: 2 },
{ name: 'todo 3', completed: false, id: 3 },
] as Todo[]).pipe(delay(5000));
ngAfterViewInit() {
this.todoElements.changes.subscribe(() => {
console.log('All todos are rendered');
console.log(this.todoElements.toArray());
});
}
}
bootstrapApplication(App);
The main problem above in my opinion is the dependency to the component lifecycle-hook ngAfterViewInit
. The developer has to know that the todoElements
only at this point available in the DOM.
Also it is not for every beginner Angular developer visible that a changes
observable exists on the todoElements
QueryList instance.
In the second example I will show you how Angular Signals can make the life of the developers much easier than before:
Angular Signals example:
import {
Component,
ElementRef,
viewChildren,
effect
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';
import { of, delay } from 'rxjs';
interface Todo {
name: string;
completed: boolean;
id: number;
}
@Component({
selector: 'app-root',
standalone: true,
template: `
<ul>
@for(todo of todos(); track todo.id) {
<li #todoItem>{{ todo.name }}</li>
}
</ul>
`,
})
export class App {
todoElements = viewChildren<ElementRef>('todoItem');
todos = toSignal(of([
{ name: 'todo 1', completed: false, id: 1 },
{ name: 'todo 2', completed: false, id: 2 },
{ name: 'todo 3', completed: false, id: 3 },
] as Todo[]).pipe(delay(5000)));
constructor() {
effect(() => {
console.log('All todos are rendered', this.todoElements().length);
})
}
}
bootstrapApplication(App);
What are the code benefits of the example above to the previous old fashion way?
we could get rid of the AsyncPipe (template and import)
we used to the new reactive
viewChildren
function to get the viewChildren elements. It's a much cleaner (no decorater needed anymore) waywe could remove the exclamation mark to suppresses errors that a property is not initialized
we used the new Angular
toSignal
function to turn the todos-observable into a Signalwe used the Angular
effect
function to listening to the queried view-childrenwe are not depending anymore to the
ngAfterViewInit
lifecycle hook
Subscribe to my newsletter
Read articles from Oliver Waterkamp directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by