6. Lifecycle Hooks
LoggerService:
import { Injectable } from '@angular/core';
@Injectable()
export class LoggerService {
logs: string[] = [];
previousMessage = '';
previousMessageCount = 1;
log(message: string) {
if (message === this.previousMessage) {
// repeat message; update last log entry with count
this.logs[this.logs.length - 1] = message + `${this.previousMessage += 1}x`;
} else {
// new message log it
this.previousMessage = message;
this.previousMessageCount = 1;
this.logs.push(message);
}
}
clear() {
this.logs.length = 0;
}
// schedules a view refresh to ensure display catches up
tick() { this.tick_then(() => { }); }
tick_then(fn: () => any) { setTimeout(fn, 0); }
}
TechnologyLifeCycleHookComponent:
import { OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy, SimpleChanges } from '@angular/core';
import { Component, Input } from '@angular/core';
import { LoggerService } from './logger.service';
let nextId = 1;
export class TechnologyLifeCycleHook implements OnInit {
constructor(private loggerService: LoggerService) { }
ngOnInit() {
this.logIt(`OnInit`);
}
logIt(message: string) {
this.loggerService.log(`#${nextId++} ${message}`);
}
}
@Component({
selector: 'app-technology-life-cycle-hook',
template: '<p>Now you see my technology: {{ name }}</p>',
styles: ['p { background: #ff9999; padding: 8px;}']
})
export class TechnologyLifeCycleHookComponent extends TechnologyLifeCycleHook
implements OnInit, OnChanges, DoCheck, AfterContentInit,
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
@Input() name: string;
private verb = 'initialized';
constructor(loggerService: LoggerService) {
super(loggerService);
const is = this.name ? 'is' : 'is not';
this.logIt(`name ${is} known at construction`);
}
// only called for/if there is an @input variable set by parent
ngOnChanges(changes: SimpleChanges) {
const changesMsgs: string[] = [];
for (const propName in changes) {
if (propName === 'name') {
const name = changes['name'].currentValue;
changesMsgs.push(`name ${this.verb} to ${name}`);
} else {
changesMsgs.push(propName + ' ' + this.verb);
}
}
this.logIt(`OnChanges: ${changesMsgs.join('; ')}`);
this.verb = 'changed'; // next time it will be a change
}
// beware called frequently, called in every change detection cycle anywhere on the page
ngDoCheck() { this.logIt(`DoCheck`); }
ngAfterContentInit() { this.logIt('AfterContentInit'); }
// beware called frequently, called in every change detection cycle anywhere on the page
ngAfterContentChecked() { this.logIt('AfterContentChecked'); }
ngAfterViewInit() { this.logIt('AfterViewInit'); }
// beware called frequently, called in every change detection cycle anywhere on the page
ngAfterViewChecked() { this.logIt('AfterViewChecked'); }
ngOnDestroy() { this.logIt('OnDestroy'); }
}
TechnologyLifeCycleHookHostComponent:
import { Component } from '@angular/core';
import { LoggerService } from './logger.service';
@Component({
selector: 'app-technology-life-cycle-hook-host',
template: `
<div class="host">
<h2>Technology-LifeCycle-Hook</h2>
<button (click)="toggleChild()">
{{ hasChild ? 'Destroy' : 'Create' }} TechnologyLifeCycleHookComponent
</button>
<button (click)="updateTechnology()" [hidden]="!hasChild">Update Technology</button>
<app-technology-life-cycle-hook *ngIf="hasChild" [name]="technologyName">
</app-technology-life-cycle-hook>
<h4>-- Lifecycle Hook Log --</h4>
<div *ngFor="let message of hookLog">{{ message }}</div>
</div>
`,
styles: [`.host { background: #ff3333; color: white; } h2 { color: white; }`]
})
export class TechnologyLifeCycleHookHostComponent {
hasChild = false;
hookLog: string[];
technologyName = 'Angular 4';
private loggerService: LoggerService;
constructor(loggerService: LoggerService) {
this.loggerService = loggerService;
this.hookLog = loggerService.logs;
}
toggleChild() {
this.hasChild = !this.hasChild;
if (this.hasChild) {
this.technologyName = 'Angular 4';
this.loggerService.clear(); // clear log on create
}
this.loggerService.tick();
}
updateTechnology() {
this.technologyName += '*';
this.loggerService.tick();
}
}
Let's view the results in our Chromium Web Browser:

Spy Directive:
import { Directive, OnInit, OnDestroy } from '@angular/core';
import { LoggerService } from './logger.service';
let nextId = 1;
@Directive({ selector: '[mySpy]'})
export class SpyDirective implements OnInit, OnDestroy {
constructor(private loggerService: LoggerService) { }
ngOnInit() { this.logIt(`onInit`); }
ngOnDestroy() { this.logIt(`onDestroy`); }
private logIt(message: string) {
this.loggerService.log(`Spy #${nextId++} ${message}`);
}
}
Spy Component:
import { Component } from '@angular/core';
import { LoggerService } from './logger.service';
@Component({
selector: 'app-spy-host',
template: `
<div class="host">
<h2>Spy Directive</h2>
<input [(ngModel)]="newName" (keyup.enter)="addTechnology()">
<button (click)="addTechnology()">Add Technology</button>
<button (click)="reset()">Reset Technologies</button>
<p></p>
<div *ngFor="let technology of technologies" mySpy class="technologies">
{{ technology }}
</div>
<h4>-- Spy Lifecycle Hook Log --</h4>
<div *ngFor="let message of spyLog">{{ message }}</div>
</div>
`,
styles: [`.host { background: #ffcccc; } .technologies { background: #ff6666; }`]
})
export class SpyHostComponent {
newName = 'Angular 5';
technologies: string[] = ['Angular 4', 'Angular CLI', 'Angular Material'];
spyLog: string[];
constructor(private loggerService: LoggerService) {
this.spyLog = loggerService.logs;
}
addTechnology() {
if (this.newName.trim()) {
this.technologies.push(this.newName.trim());
this.newName = '';
this.loggerService.tick();
}
}
reset() {
this.loggerService.log('-- reset --');
this.technologies.length = 0;
this.loggerService.tick();
}
}
App Component:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h1>{{ title }}</h1>
<div class="left">
<app-spy-host></app-spy-host>
</div>
<div class="right">
<img [src]="imageUrl" class="img-round" alt="princess image">
</div>
`
})
export class AppComponent {
title = 'Tour of Technologies';
imageUrl = '../assets/polymer6.jpg';
}
Let's view the results in Chromium:

OnChanges()
OnChangesComponent:
import { Component, Input, OnChanges, SimpleChanges, ViewChild} from '@angular/core';
class Technology {
constructor(public name: string) { }
}
@Component({
selector: 'app-on-changes',
template: `
<div class="technology">
<p>{{ technology.name }} can {{ power }}</p>
<h4>-- Change Log --</h4>
<div *ngFor="let change of changeLog"> {{ change }}</div>
</div>
`,
styles: [`
.technology { background: LightYellow; padding: 8px; margin-top: 8px; }
p { background: Yellow; padding: 8px; margin-top: 8px; }
`]
})
export class OnChangesComponent implements OnChanges {
@Input() technology: Technology;
@Input() power: string;
changeLog: string[] = [];
ngOnChanges(changes: SimpleChanges) {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
const current = JSON.stringify(change.currentValue);
const previous = JSON.stringify(change.previousValue);
this.changeLog.push(`${propName}: currentValue = ${current}, previousValue = ${previous}`);
}
}
reset() { this.changeLog.length = 0; }
}
/********************************************************/
@Component({
selector: 'app-on-changes-host',
template: `
<div class="host">
<h2>{{ title }}</h2>
<table>
<tr><td>Power: </td><td><input [(ngModel)]="power"></td></tr>
<tr><td>Technology.name: </td><td><input [(ngModel)]="technology.name"></td></tr>
</table>
<p><button (click)="reset()">Reset Log</button></p>
<app-on-changes [technology]="technology" [power]="power"></app-on-changes>
</div>
`,
styles: [`.host { background: #ffcccc; }`]
})
export class OnChangesHostComponent {
technology: Technology;
power: string;
title = 'OnChanges';
@ViewChild(OnChangesComponent) childView: OnChangesComponent;
constructor() {
this.reset();
}
reset() {
// new technology object every time; triggers onChanges
this.technology = new Technology('Angular 5');
// setting power only triggers onChanges if this value is different
this.power = 'data-bind';
if (this.childView) { this.childView.reset(); }
}
}
Let's have a look at this beauty in our Chromium Browser:

DoCheck()
DoCheckComponent:
import { Component, DoCheck, Input, ViewChild } from '@angular/core';
class Technology {
constructor(public name: string) { }
}
@Component({
selector: 'app-do-check',
template: `
<div class="technology">
<p>{{ technology.name }} can {{ power }}</p>
<h4>-- Change Log --</h4>
<div *ngFor="let change of changeLog">{{ change }}</div>
</div>
`,
styles: [`
.technology { background: LightYellow; padding: 8px; margin-top: 8px }
p { background: Yellow; padding: 8px; margin-top: 8px }
`]
})
export class DoCheckComponent implements DoCheck {
@Input() technology: Technology;
@Input() power: string;
changeLog: string[] = [];
private changeDetected = false;
private oldTechnologyName = '';
private oldPower = '';
private oldLogLength = 0;
private noChangeCount = 0;
ngDoCheck() {
if (this.technology.name !== this.oldTechnologyName) {
this.changeDetected = true;
this.changeLog.push(`DoCheck: Technology name changed to "${this.technology.name}"
from "${this.oldTechnologyName}"`);
this.oldTechnologyName = this.technology.name;
}
if (this.power !== this.oldPower) {
this.changeDetected = true;
this.changeLog.push(`DoCheck: Power changed to "${this.power}" from "${this.oldPower}"`);
this.oldPower = this.power;
}
if (this.changeDetected) {
this.noChangeCount = 0;
} else {
// log hook was called when no relevant change
const count = this.noChangeCount += 1;
const noChangeMessage = `DoCheck called ${count}x when no change to technology or power`;
if (count === 1) {
// add new no change method
this.changeLog.push(noChangeMessage);
} else {
// update last no change message
this.changeLog[this.changeLog.length - 1] = noChangeMessage;
}
}
this.changeDetected = false;
}
reset(): void {
this.changeDetected = true;
this.changeLog.length = 0;
}
}
/*************************************************************/
@Component({
selector: 'app-do-check-host',
template: `
<div class="host">
<h2>{{ title }}</h2>
<table>
<tr><td>Power</td><td><input [(ngModel)]="power"></td></tr>
<tr><td>Technology.name</td><td><input [(ngModel)]="technology.name"></td></tr>
</table>
<p><button (click)="reset()">Reset Log</button></p>
<app-do-check [technology]="technology" [power]="power"></app-do-check>
</div>
`,
styles: [`.host { background: #ff9999; }`]
})
export class DoCheckHostComponent {
technology: Technology;
power: string;
title = 'DoCheck';
@ViewChild(DoCheckComponent) childView: DoCheckComponent;
constructor() { this.reset(); }
reset(): void {
this.technology = new Technology('Angular 5');
this.power = 'service worker';
if (this.childView) { this.childView.reset(); }
}
}
Let's view the running application in Chromium:

AfterViewComponent:
import { AfterViewChecked, AfterViewInit, Component, ViewChild } from '@angular/core';
import { LoggerService } from './logger.service';
@Component({
selector: 'app-my-child-view',
template: `<input [(ngModel)]="technology">`
})
export class ChildViewComponent {
technology = 'Angular 5';
}
/**********************************************/
@Component({
selector: 'app-after-view',
template: `
<div>-- child view begins --</div>
<app-my-child-view></app-my-child-view>
<div>-- child view ends --</div>
` + `
<p *ngIf="comment" class="comment">
{{ comment }}
</p>
`
})
export class AfterViewComponent implements AfterViewChecked, AfterViewInit {
comment = '';
private previousTechnology = '';
// query for viewchild of type childviewcomponent
@ViewChild(ChildViewComponent) viewChild: ChildViewComponent;
constructor(private loggerService: LoggerService) { this.logIt('AfterView constructor'); }
ngAfterViewInit() {
// viewchild is set after view is initialized
this.logIt('AfterViewInit');
this.doSomething();
}
ngAfterViewChecked() {
// viewchild is updated after view has been checked
if (this.previousTechnology === this.viewChild.technology) {
this.logIt('AfterViewChecked (no change)');
} else {
this.previousTechnology = this.viewChild.technology;
this.logIt('AfterView checked');
this.doSomething();
}
}
// this surrogate for real business logic sets the comment
private doSomething(): void {
const cmt = this.viewChild.technology.length > 10 ? `That's a long name` : '';
if (cmt !== this.comment) {
// wait a tick because component's view has already been checked
this.loggerService.tick_then(() => this.comment = cmt);
}
}
private logIt(method: string): void {
const child = this.viewChild;
const message = `${method}: ${child ? child.technology : 'no' } child view`;
this.loggerService.log(message);
}
}
/**********************************************/
@Component({
selector: 'app-after-view-host',
template: `
<div class="host">
<h2>AfterView</h2>
<app-after-view *ngIf="show"></app-after-view>
<h4>-- AfterView Logs --</h4>
<p><button (click)="reset()">Reset</button></p>
<div *ngFor="let message of logs">{{ message }}</div>
</div>
`,
styles: ['.host { background: #ffe6e6; }']
})
export class AfterViewHostComponent {
logs: string[];
show = true;
constructor(private loggerService: LoggerService) { this.logs = loggerService.logs; }
reset(): void {
this.logs.length = 0;
// quickly remove and reload afterviewcomponentwhich recreates it
this.show = false;
this.loggerService.tick_then(() => this.show = true);
}
}
AfterContentComponent:
import { AfterContentChecked, AfterContentInit, Component, ContentChild } from '@angular/core';
import { LoggerService } from './logger.service';
@Component({
selector: 'app-my-child',
template: `<input [(ngModel)]="technology">`
})
export class ChildContentComponent {
technology = 'Angular 5';
}
/**********************************************/
@Component({
selector: 'app-after-content',
template: `
<div>-- projected content begins --</div>
<ng-content></ng-content>
<div>-- projected content ends --</div>
` +
`
<p *ngIf="comment" class="comment">
{{ comment }}
</p>
`
})
export class AfterContentComponent implements AfterContentChecked, AfterContentInit {
comment = '';
private previousTechnology = '';
// query for content child of type childcomponent
@ContentChild(ChildContentComponent) contentChild: ChildContentComponent;
constructor(private loggerService: LoggerService) { this.logIt('AfterContent constructor'); }
ngAfterContentInit() {
// contentchild is set after content has been initialized
this.logIt('AfterContentInit');
this.doSomething();
}
ngAfterContentChecked() {
// contentchild is updated after content has been checked
if (this.previousTechnology === this.contentChild.technology) {
this.logIt('AfterContentChecked (no change)');
} else {
this.previousTechnology = this.contentChild.technology;
this.logIt('AfterContentChecked');
this.doSomething();
}
}
// this surrogate for real business logic sets the comment
private doSomething(): void {
this.comment = this.contentChild.technology.length > 10 ? `That's a long name` : '';
}
private logIt(method: string): void {
const child = this.contentChild;
const message = `${method}: ${child ? child.technology : 'no'} child content`;
this.loggerService.log(message);
}
}
/**********************************************/
@Component({
selector: 'app-after-content-host',
template: `
<div class="host">
<h2>AfterContent</h2>
<div *ngIf="show">` +
`
<app-after-content>
<app-my-child>
</app-my-child>
</app-after-content>
` +
`</div>
<h4>-- AfterContentLogs --</h4>
<p><button (click)="reset()">Reset</button></p>
<div *ngFor="let message of logs">{{ message }}</div>
</div>
`,
styles: [`.host { background: #ffb3b3; }`]
})
export class AfterContentHostComponent {
logs: string[];
show = true;
constructor(private loggerService: LoggerService) {
this.logs = loggerService.logs;
}
reset(): void {
this.logs.length = 0;
// quickly remove and reload aftercontentcomponent which recreates it
this.show = false;
this.loggerService.tick_then(() => this.show = true);
}
}
CounterComponent:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { LoggerService } from './logger.service';
@Component({
selector: 'app-my-counter',
template: `
<div class="counter">
Counter = {{ counter }}
<h5>-- Counter Change Log --</h5>
<div *ngFor="let change of changeLog" mySpy>{{ change }}</div>
</div>
`,
styles: [`.counter { background: #ffe6e6; padding: 8px; margin-top: 8px; }`]
})
export class MyCounterComponent implements OnChanges {
@Input() counter: number;
changeLog: string[] = [];
ngOnChanges(changes: SimpleChanges) {
// empty changelog whenever counter goes to zero
// hint: this is a way to respond programmatically to external value changes
if (this.counter === 0) {
this.changeLog.length = 0;
}
// a change to counter is the only change we care about
const change = changes['counter'];
const current = change.currentValue;
const previous = JSON.stringify(change.previousValue); // first time is {}; after is integer
this.changeLog.push(`counter: currentValue = ${current}, previousValue = ${previous}`);
}
}
/**********************************************/
@Component({
selector: 'app-counter-host',
template: `
<div class="host">
<h2>Counter Spy</h2>
<button (click)="updateCounter()">Update Counter</button>
<button (click)="reset()">Reset Counter</button>
<app-my-counter [counter]="value"></app-my-counter>
<h4>-- Spy Lifecycle Hook Log --</h4>
<div *ngFor="let message of spyLog">{{ message }}</div>
</div>
`,
styles: [`.host { background: #ff6666; }`]
})
export class CounterHostComponent {
value: number;
spyLog: string[] = [];
private loggerService: LoggerService;
constructor(loggerService: LoggerService) {
this.loggerService = loggerService;
this.spyLog = loggerService.logs;
this.reset();
}
updateCounter() {
this.value += 1;
this.loggerService.tick();
}
reset() {
this.loggerService.log('-- reset --');
this.value = 0;
this.loggerService.tick();
}
}
Let's view the results in our Chromium Browser:
