DOM reading and writing with new lifecycle hooks in Angular
Connie Leung
Posted on September 10, 2023
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);
Install dependency
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.
@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.
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
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.
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.
<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.
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:
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/
Posted on September 10, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.