First time of using ngOnChanges in Angular 2

When i first learn Angular 2, i think i will never need ngOnChanges, because it seems to run change detection so many times, even when using OnPush change detection strategy. To me, ngOnInit and ngAfterViewInit are the most important hooks, sometimes i will need ngOnDestroy, i.e. to unsubscribe Observables.

But recently i create a free style mind map using Angular 2, ngrx, localStorage and Bootstrap, and i came across a scenario that i have to use ngOnChanges. Before i explain why, you can run the app:

website: https://shoot-your-mind.firebaseapp.com/

github: https://github.com/penghuili/shoot-your-mind/


Why i need ngOnChanges?

Well, the idea of my app is very simple, you can add ideas, and connect them with lines. Like this:

And the architecture of the app is like this:

I use <canvas> to draw the lines in the CanvasComponent. The template is like this:

<canvas #canvas 
[attr.width.px]=”canvasWidth”
[attr.height.px]=”canvasHeight”>
</canvas>

And the component:

export class CanvasComponent implements AfterViewInit {
@Input() canvasWidth: number;
@Input() canvasHeight: number;
@Input() lines: Line[];
@ViewChild(“canvas”) canvas: ElementRef;
ctx: CanvasRenderingContext2D;
    ngAfterViewInit() {
this.drawLines();
}
    private drawLines() {
this.ctx = this.canvas.nativeElement.getContext("2d");
this.ctx.clearRect(0, 0,
this.canvasWidth, this.canvasHeight);
this.lines.forEach(line => {
this.drawLine(
line.ideaA.centerX,
line.ideaA.centerY,
line.ideaB.centerX,
line.ideaB.centerY);
});
}
private drawLine(x1: number, y1: number,
x2: number, y2:number) {
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.stroke();
}
}

Firstly, Iwant to call this.drawLines() in ngAfterViewInit (ngOnInit is not possible, because i am using ViewChild), when I navigate to CanvasComponent, ideas are positioned right, all lines are drawn correctly, everything works fine. Then when i drag an idea, the position of the idea will change via ngrx, and the lines connected to it will also change, this means the #Input() Lines will change, and all lines will be drawn again, the effect is lines moves with idea. But the lines stand where they are, not moving!

Finally i found out why. When the #Input() Lines change, CanvasComponent will not be created again, angular uses the same instance, so the ngAfterViewInit will not be called.

The solution is call this.drawLines() in ngOnChanges, and set changeDetection to OnPush.


Why did i make this mistake?

Because when i draw lines, *ngFor is in my head. Both are doing loop.

If now i am not drawing line, but only display a list of names, like:

<ul>
<li *ngFor="let name of names">{{name}}</li>
</ul>

I don’t need ngAfterViewInit or even ngOnInit, let alone ngOnChanges. Because *ngFor is a structural directive, if #Input() names change, Angular will destroy all the <li> elements, and re-render them with new data.

But if i draw lines with <canvas>, i am not using *ngFor, most importantly, there are no <line> elements in the template, the template only has a poor <canvas> element, and lines are created with pure js code. So after #Input() lines change, Angular can do nothing to the view.

So i have to use ngOnChanges.

By the way, i can’t just throw ngAfterViewInit away. Without ngAfterViewInit when i navigate to a CanvasComponent, no lines will be drawn. The reason is, when Angular creates the CanvasComponent for the first time, this.lines hasn’t changed, so ngOnChanges is not called.


Conclusion

Most of the time we don’t need ngOnChanges, but some time we may come across a special scenario, and at that time, ngOnChanges will say:

“Challenge accepted!”