DOM reading and writing with new lifecycle hooks in Angular
Tags: angular, tutorial, typescript, webdev
Published: 2023-09-10 17:51:49

##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.

```typescript
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);
```

###Install dependency

```bash
npm i --save-exact chart.js
```

###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.

```typescript
@ViewChild('canvas', { static: true, read: ElementRef<HTMLCanvasElement> })
canvas!: ElementRef<HTMLCanvasElement>;
```

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.

```typescript
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,
              }
            ]
          }
        }
      );
    });
}
```

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

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

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.

```typescript
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();
      }
    });
}
```

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.

```html
<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';
```

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.

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

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

      this.chart.update();
   }
});
```

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:
{%embed https://stackblitz.com/edit/stackblitz-starters-rey9qy?file=src%2Fmain.ts %}

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:

- Github Repo: https://github.com/railsstudent/ng-after-render-demo
- Stackblitz: https://stackblitz.com/edit/stackblitz-starters-rey9qy?file=src%2Fmain.ts
- AfterNextRender and AfterRender: https://angular.io/guide/lifecycle-hooks#reading-and-writing-the-dom
- Chart.js: https://www.chartjs.org/docs/latest/