DOM reading and writing with new lifecycle hooks in Angular

Connie Leung - Sep 10 '23 - - Dev Community

Introduction

In Angular 16, Angular has added two new lifecycle hooks, afterNextRender and afterRender, for DOM reading and writing.

  • afterNextRender – executes once and is similar to AfterViewInit but it does not execute in server-side rendering (SSR)
  • afterRender – executes after every change detection

According to the Angular documentation, afterNextRender is similar to AfterViewInit but it does not cause issues that AfterViewInit has in SSR. On the other hand, afterRender executes after every change detection to synchronize state with DOM.

In this blog post, I describe how to use afterNextRender to add new chart on canvas and afterRender to redraw chart to synchronize chart options.

Scenario of using afterNextRender and afterRender

In the example, I want to use a thirty-party chart library, Chart.js, to render a bar chart on a canvas element. Therefore, I implement afterNextRender hook to insert the chart to the canvas. Then, I use RxJS timer operator to append data points to the underlying chart array every one second.

To make the example interactive, there is a color dropdown that changes the color of the bars. I am going to implement afterRender hook that examines the inputs and update the chart after every change detection.

import 'zone.js/dist/zone';
import { afterNextRender, afterRender, ChangeDetectionStrategy, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import Chart from 'chart.js/auto';
import { take, timer } from 'rxjs';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [FormsModule],
  template: `
    <h1>Hello from Lifecycle Hooks!</h1>
    <div>
      <div>
        <label>
          Bar Color:
          <select [(ngModel)]="barColor">
              <option value="red">Red</option>
              <option value="pink">Pink</option>
              <option value="magenta">Magenta</option>
              <option value="rebeccapurple">Rebecca Purple</option>
              <option value="cyan">Cyan</option>
              <option value="blue">Blue</option>
              <option value="green">Green</option>
              <option value="yellow">Yellow</option>
          </select>
        </label>
      </div>
      <canvas #canvas></canvas>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App implements OnDestroy {
  data = [
    { year: 2017, count: 10 },
    { year: 2018, count: 20 },
    { year: 2019, count: 15 },
    { year: 2020, count: 25 },
    { year: 2021, count: 22 },
    { year: 2022, count: 30 },
    { year: 2023, count: 4 },
  ];
  chart: Chart | null = null;
  chartData: { year: number; count: number} | null = null;
  barColor = 'red';

  constructor() {
    timer(100, 1000)
      .pipe(
        take(5),
      ).subscribe(
        (value) => { 
          this.chartData = { year: 2024 + value, count: Math.floor(Math.random() * 20) };
        }
      );

      // afterNextRender and afterRender are implemented in the constructor
  }

  ngOnDestroy(): void {
    this.chart?.destroy();
  }
}

bootstrapApplication(App);
Enter fullscreen mode Exit fullscreen mode

Install dependency

npm i --save-exact chart.js
Enter fullscreen mode Exit fullscreen mode

Implement afterNextRender lifecycle hook to attach chart to canvas

The afterNextRender lifecycle hook executes once after the next change detection. Therefore, it is the ideal entry point to insert a new chart into DOM.

@ViewChild('canvas', { static: true, read: ElementRef<HTMLCanvasElement> })
canvas!: ElementRef<HTMLCanvasElement>;
Enter fullscreen mode Exit fullscreen mode

First, I use @ViewChild decorator to obtain the ElementRef of the canvas. this.canvas.nativeElement returns an instance of HTMLCanvasElement that passes to the constructor of Chart.js in afterNextRender hook.

constructor() {
   ... omitted other codes

  afterNextRender(() => {
      console.log('afterNextRender called');
      this.chart = new Chart(this.canvas.nativeElement, 
        {
          type: 'bar',
          data: {
            labels: this.data.map(row => row.year),
            datasets: [
              {
                label: 'Acquisitions by year',
                data: this.data.map(row => row.count),
                backgroundColor: this.barColor,
              }
            ]
          }
        }
      );
    });
}
Enter fullscreen mode Exit fullscreen mode

In the constructor, I implement afterNextRender and pass a callback to instantiate the chart and render it on the canvas.

I have successfully inserted a JavaScript Chart to DOM. DOM reading and writing is performed exactly once such that canvas does not display multiple charts erroneously.

Implement afterRender lifecycle hook to perform repeated DOM reading and writing

timer(100, 1000)
      .pipe(
        take(5),
      ).subscribe(
        (value) => { 
          this.chartData = { year: 2024 + value, count: Math.floor(Math.random() * 20) };
        }
 );
Enter fullscreen mode Exit fullscreen mode

This timer operator randomizes 5 data points and assigns to this.chartData every one second. After this.chartData is set, the chart displays the new bar in afterRender hook.

constructor() {
   ... omitted other codes

  afterNextRender(() => {
       ... instantiate chart ....
  });

  afterRender (() => {
      if (this.chart) {
        const datasets = this.chart.data.datasets;
        if (this.chartData) {
          const { year, count } = this.chartData;
          this.chart.data.labels?.push(year);
          datasets.forEach((dataset) => {
            dataset.data.push(count);
          });
          this.chartData = null;
        }

        this.chart.update();
      }
    });
}
Enter fullscreen mode Exit fullscreen mode

In afterRender hook, chart label and chart data are pushed to the data set when both this.chart and this.chartData are defined. Then, this.chart.update() is invoked to redraw the chart on canvas.

DOM reading and writing in afterRender hook based on user inputs

Another user case is to update bar color when user selects color from a dropdown. This simple dropdown applies 2-way data binding to bind ngModel to this.barColor.

<div>
     <label>
          Bar Color:
          <select [(ngModel)]="barColor">
              <option value="red">Red</option>
              <option value="pink">Pink</option>
              <option value="magenta">Magenta</option>
              <option value="rebeccapurple">Rebecca Purple</option>
              <option value="cyan">Cyan</option>
              <option value="blue">Blue</option>
              <option value="green">Green</option>
              <option value="yellow">Yellow</option>
          </select>
    </label>
</div>

barColor = 'red';
Enter fullscreen mode Exit fullscreen mode

When user makes selection, the application updates this.barColor and triggers change detection. afterRender is then fired after every change detection. Therefore, I put logic in afterRender hook to update bar color and redraw the graph on canvas.

afterRender (() => {
   if (this.chart) {
      const datasets = this.chart.data.datasets;
      ... append data to dataset ...

      datasets.forEach((dataset) => {
        dataset.backgroundColor = this.barColor;
      });

      this.chart.update();
   }
});
Enter fullscreen mode Exit fullscreen mode

In afterRender hook, dataset.backgroundColor = this.barColor; updates the color and this.chart.update(); updates the chart again.

The following Stackblitz repo shows the final results:

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .