How to find out when a list (asynchronous) has been rendered to the DOM

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) way

  • we 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 Signal

  • we used the Angular effect function to listening to the queried view-children

  • we are not depending anymore to the ngAfterViewInit lifecycle hook