ngDoCheck In Angular
In this article, we will learn how to use the ngDoCheck lifecycle hook. Using the DoCheck hook, we will learn how to keep track of changes to @Input properties. We also examine how key-value and inerrable differ in how they work.
What is ngDoCheck lifecycle hook
In the previous chapter, we looked at how the OnChanges hook works. When Angular detects a change to the data-bound input property, it fires this event.
We also looked at how OnChanges isn't fired when the input property is an array/object since Angular compares the properties using dirty checking.
When Angular fails to detect changes to the input property, we can use the DoCheck to construct our own custom change detection. After each change detection, the Angular fires the DoCheck hook.
ngDoCheck example
Let's build on the code we created in the previous OnChanges article.
add the following code under customer.ts file.
export class Customer { code: number; name: string; }
Now, The app.component.ts file has not changed.
import { Component} from '@angular/core'; import { Customer } from './customer'; @Component({ selector: 'app-root', template: ` <h1>{{title}}!</h1> <p> Message : <input type='text' [(ngModel)]='message'> </p> <p> Code : <input type='text' [(ngModel)]='code'></p> <p> Name : <input type='text' [(ngModel)]='name'></p> <p><button (click)="updateCustomer()">Update </button> <child-component [message]=message [customer]=customer></child-component> ` , styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'ngOnChanges'; message = ''; customer: Customer = new Customer(); name= ''; code= 0; updateCustomer() { this.customer.name = this.name; this.customer.code = this.code; } }
Add the following code under the Child Component
import { Component, Input, OnChanges, OnInit, SimpleChanges, SimpleChange, DoCheck } from '@angular/core'; import { Customer } from './customer'; @Component({ selector: 'child-component', template: `<h2>Child Component</h2> <p>Message {{ message }} </p> <p>Customer Name {{ customer.name }} </p> <p>Customer Code {{ customer.code }} </p> <p>Do Check count {{ DocheckCount }} </p> <ul><li *ngFor="let log of changelog;"> {{ log }}</li></ul> ` }) export class ChildComponent implements OnChanges, DoCheck, OnInit { @Input() message: string; @Input() customer: Customer; changelog: string[] = []; oldCustomer: Customer= new Customer(); DocheckCount = 0; ngOnInit() { console.log('OnInit'); this.oldCustomer = Object.assign({}, this.customer); } ngDoCheck() { console.log('Docheck'); this.DocheckCount++; if (this.oldCustomer.name !== this.customer.name || this.oldCustomer.code !== this.customer.code ) { const to = JSON.stringify(this.customer); const from = JSON.stringify(this.oldCustomer); const changeLog = `DoCheck customer: changed from ${from} to ${to} `; this.changelog.push(changeLog); this.oldCustomer = Object.assign({}, this.customer); } } ngOnChanges(changes: SimpleChanges) { console.log('OnChanges'); console.log(JSON.stringify(changes)); // tslint:disable-next-line:forin for (const propName in changes) { const change = changes[propName]; const to = JSON.stringify(change.currentValue); const from = JSON.stringify(change.previousValue); const changeLog = `${propName}: changed from ${from} to ${to} `; this.changelog.push(changeLog); } } }
We started by importing the DoCheck function from the @angular/core library.
import { Component, Input, OnChanges, OnInit, SimpleChanges, SimpleChange, DoCheck } from '@angular/core'; import { Customer } from './customer';
The DoCheck Interface should be implemented.
export class ChildComponent implements OnChanges, DoCheck, OnInit {
We've added a new property called oldCustomer to store the customer's previous value. We also have the DoCheckcount property, which maintains track of the number of times this hook has been called.
oldCustomer: Customer= new Customer(); DocheckCount = 0;
In the OnInit hook, we're cloning the customer object into oldCustomer. To see if the customer object has changed, the old customer values are compared to the new customer values.
ngOnInit() { console.log('OnInit'); this.oldCustomer = Object.assign({}, this.customer); }
Finally, in the ngDoChek hook, we compare new customer values to oldCustomer values to see if there are any differences.
ngDoCheck() { console.log('Docheck'); this.DocheckCount++; if (this.oldCustomer.name !== this.customer.name || this.oldCustomer.code !== this.customer.code ) { const to = JSON.stringify(this.customer); const from = JSON.stringify(this.oldCustomer); const changeLog = `DoCheck customer: changed from ${from} to ${to} `; this.changelog.push(changeLog); this.oldCustomer = Object.assign({}, this.customer); } }
When ngDoCheck is called
Checking for changes
- key-value differs
- iterable differs
key-value differs
Iterable differs
Example of key-value differs
Import KeyValueDiffers from @angular/core
Now, Inject it into the constructor
constructor(private differs: KeyValueDiffers) { }
Make a distinct property for the customer object.
differ: any;
ngOnInit() { console.log('OnInit'); this.differ = this.differs.find(this.customer).create(null); }
We then check if our object has changed using the diff method of the differ. If there is no change, the object returns null. It returns an object containing the object's modifications.
const customerChanges = this.differ.diff(this.customer);
Using the forEachChangedItem, forEachAddedItem, and forEachRemovedItem methods, we can determine what properties were added, changed, or removed, as shown below.
if (customerChanges) { console.log(customerChanges); customerChanges.forEachChangedItem(r => this.changelog.push('changed ' + r.key + ' ' + JSON.stringify( r.currentValue))); customerChanges.forEachAddedItem(r => this.changelog.push('added ' + r.key + ' ' + JSON.stringify( r.currentValue))); customerChanges.forEachRemovedItem(r => this.changelog.push('removed ' + r.key + ' ' + JSON.stringify( r.currentValue))); }
The following is the complete child component code:
import { Component, Input, OnChanges, OnInit, SimpleChanges, SimpleChange, DoCheck, KeyValueDiffers } from '@angular/core'; import { Customer } from './customer'; @Component({ selector: 'child-component', template: `<h2>Child Component</h2> <p>Message {{ message }} </p> <p>Customer Name {{ customer.name }} </p> <p>Customer Code {{ customer.code }} </p> <p>Do Check count {{ DocheckCount }} </p> <ul><li *ngFor="let log of changelog;"> {{ log }}</li></ul> ` }) export class ChildComponent implements OnChanges, DoCheck, OnInit { @Input() message: string; @Input() customer: Customer; changelog: string[] = []; oldCustomer: Customer= new Customer(); DocheckCount = 0; differ: any; constructor(private differs: KeyValueDiffers) { } ngOnInit() { console.log('OnInit'); this.differ = this.differs.find(this.customer).create(null); } ngDoCheck() { console.log('Docheck'); this.DocheckCount++; const customerChanges = this.differ.diff(this.customer); if (customerChanges) { console.log(customerChanges); customerChanges.forEachChangedItem(r => this.changelog.push('changed ' + r.key + ' ' + JSON.stringify( r.currentValue))); customerChanges.forEachAddedItem(r => this.changelog.push('added ' + r.key + ' ' + JSON.stringify( r.currentValue))); customerChanges.forEachRemovedItem(r => this.changelog.push('removed ' + r.key + ' ' + JSON.stringify( r.currentValue))); } } ngOnChanges(changes: SimpleChanges) { console.log('OnChanges'); console.log(JSON.stringify(changes)); // tslint:disable-next-line:forin for (const propName in changes) { const change = changes[propName]; const to = JSON.stringify(change.currentValue); const from = JSON.stringify(change.previousValue); const changeLog = `${propName}: changed from ${from} to ${to} `; this.changelog.push(changeLog); } } }