docs(Lifecycle Hooks): new chapter

closes #574
This commit is contained in:
Ward Bell 2015-11-21 11:23:40 -08:00
parent 82383bc673
commit 1a98ef5844
18 changed files with 810 additions and 0 deletions

View File

@ -0,0 +1 @@
**/*.js

View File

@ -0,0 +1,102 @@
// #docregion
import {
Component, Input, Output,
AfterContentChecked, AfterContentInit, ContentChild,
AfterViewInit, ViewChild
} from 'angular2/core';
import {ChildComponent} from './child.component';
import {LoggerService} from './logger.service';
@Component({
selector: 'after-content',
template: `
<div class="after-content">
<div>-- child content begins --</div>
<ng-content></ng-content>
<div>-- child content ends --</div>
</div>
`,
styles: ['.after-content {background: LightCyan; padding: 8px;}'],
})
export class AfterContentComponent
implements AfterContentChecked, AfterContentInit, AfterViewInit {
private _logger:LoggerService;
constructor(logger:LoggerService){
this._logger = logger;
logger.log('AfterContent ctor: ' + this._getMessage());
}
// Query for a CONTENT child of type `ChildComponent`
@ContentChild(ChildComponent) contentChild: ChildComponent;
// Query for a VIEW child of type`ChildComponent`
// No such VIEW child exists!
// This component holds content but no view of that type.
@ViewChild(ChildComponent) viewChild: ChildComponent;
///// Hooks
ngAfterContentInit() {
// contentChild is set after the content has been initialized
this._logger.log('AfterContentInit: ' + this._getMessage());
}
ngAfterViewInit() {
this._logger.log(`AfterViewInit: There is ${this.viewChild ? 'a' : 'no'} view child`);
}
private _prevHero:string;
ngAfterContentChecked() {
// contentChild is updated after the content has been checked
// Called frequently; only report when the hero changes
if (!this.contentChild || this._prevHero === this.contentChild.hero) {return;}
this._prevHero = this.contentChild.hero;
this._logger.log('AfterContentChecked: ' + this._getMessage());
}
private _getMessage(): string {
let cmp = this.contentChild;
return cmp ? `"${cmp.hero}" child content` : 'no child content';
}
}
/***************************************/
@Component({
selector: 'after-content-parent',
template: `
<div class="parent">
<h2>AfterContent</h2>
<after-content>
<input [(ngModel)]="hero">
<button (click)="showChild = !showChild">Toggle child view</button>
<my-child *ngIf="showChild" [hero]="hero"></my-child>
</after-content>
<h4>-- Lifecycle Hook Log --</h4>
<div *ngFor="#msg of hookLog">{{msg}}</div>
</div>
`,
styles: ['.parent {background: powderblue; padding: 8px; margin:100px 8px;}'],
directives: [AfterContentComponent, ChildComponent],
providers:[LoggerService]
})
export class AfterContentParentComponent {
hookLog:string[];
hero = 'Magneta';
showChild = true;
constructor(logger:LoggerService){
this.hookLog = logger.logs;
}
}

View File

@ -0,0 +1,80 @@
// #docregion
import {
Component, Input, Output,
AfterContentInit, ContentChild,
AfterViewChecked, AfterViewInit, ViewChild
} from 'angular2/core';
import {ChildComponent} from './child.component';
import {LoggerService} from './logger.service';
@Component({
selector: 'after-view-parent',
template: `
<div class="parent">
<h2>AfterView</h2>
<div>
<input [(ngModel)]="hero">
<button (click)="showChild = !showChild">Toggle child view</button>
<my-child *ngIf="showChild" [hero]="hero"></my-child>
</div>
<h4>-- Lifecycle Hook Log --</h4>
<div *ngFor="#msg of hookLog">{{msg}}</div>
</div>
`,
styles: ['.parent {background: burlywood; padding: 8px; margin:100px 8px;}'],
directives: [ChildComponent],
providers:[LoggerService]
})
export class AfterViewParentComponent
implements AfterContentInit, AfterViewChecked, AfterViewInit {
private _logger:LoggerService;
constructor(logger:LoggerService){
this._logger = logger;
this.hookLog = logger.logs;
logger.log('AfterView ctor: ' + this._getMessage());
}
hookLog:string[];
hero = 'Magneta';
showChild = true;
// Query for a CONTENT child of type `ChildComponent`
// No such CONTENT child exists!
// This component holds a view but no content of that type.
@ContentChild(ChildComponent) contentChild: ChildComponent;
// Query for a VIEW child of type `ChildComponent`
@ViewChild(ChildComponent) viewChild: ChildComponent;
///// Hooks
ngAfterContentInit() {
this._logger.log(`AfterContentInit: There is ${this.contentChild ? 'a' : 'no'} content child`);
}
ngAfterViewInit() {
// viewChild is set after the view has been initialized
this._logger.log('AfterViewInit: ' + this._getMessage());
}
private _prevHero:string;
ngAfterViewChecked() {
// viewChild is updated after the view has been checked
// Called frequently; only report when the hero changes
if (!this.viewChild || this._prevHero === this.viewChild.hero) {return;}
this._prevHero = this.viewChild.hero;
this._logger.log('AfterViewChecked: ' + this._getMessage());
}
private _getMessage(): string {
let cmp = this.viewChild;
return cmp ? `"${cmp.hero}" child view` : 'no child view';
}
}

View File

@ -0,0 +1,43 @@
// #docregion
import {Component} from 'angular2/core';
import {AfterContentParentComponent} from './after-content.component';
import {AfterViewParentComponent} from './after-view.component';
import {CounterParentComponent} from './counter.component';
import {OnChangesParentComponent} from './on-changes.component';
import {PeekABooParentComponent} from './peek-a-boo-parent.component';
import {SpyParentComponent} from './spy.component';
/***************************************/
/*
template: `
<peek-a-boo-parent></peek-a-boo-parent>
<on-changes-parent></on-changes-parent>
<after-view-parent></after-view-parent>
<after-content-parent></after-content-parent>
<spy-parent></spy-parent>
<counter-parent></counter-parent>
`,
*/
@Component({
selector: 'my-app',
template: `
<peek-a-boo-parent></peek-a-boo-parent>
<on-changes-parent></on-changes-parent>
<after-view-parent></after-view-parent>
<after-content-parent></after-content-parent>
<spy-parent></spy-parent>
<counter-parent></counter-parent>
`,
directives: [
AfterContentParentComponent,
AfterViewParentComponent,
OnChangesParentComponent,
PeekABooParentComponent,
SpyParentComponent,
CounterParentComponent
]
})
export class AppComponent {
}

View File

@ -0,0 +1,4 @@
import {bootstrap} from 'angular2/platform/browser';
import {AppComponent} from './app.component';
bootstrap(AppComponent).catch(err => console.error(err));

View File

@ -0,0 +1,20 @@
// #docregion
import {Component, Input} from 'angular2/core';
@Component({
selector: 'my-child',
template: `
<div class="my-child">
<div>-- child view begins --</div>
<div class="child">{{hero}} is my hero.</div>
<div>-- child view ends --</div>
</div>
`,
styles: [
'.child {background: Yellow; padding: 8px; }',
'.my-child {background: LightYellow; padding: 8px; margin-top: 8px}'
]
})
export class ChildComponent {
@Input() hero: string;
}

View File

@ -0,0 +1,86 @@
// #docregion
import {
Component, Input, Output,
OnChanges, SimpleChange,
} from 'angular2/core';
import {Spy} from './spy.directive';
import {LoggerService} from './logger.service';
@Component({
selector: 'my-counter',
template: `
<div class="counter">
Counter = {{counter}}
<h5>-- Counter Change Log --</h5>
<div *ngFor="#chg of changeLog" my-spy>{{chg}}</div>
</div>
`,
styles: ['.counter {background: LightYellow; padding: 8px; margin-top: 8px}'],
directives:[Spy]
})
export class MyCounter implements OnChanges {
@Input() counter: number;
changeLog:string[] = [];
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
// Empty the 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
let prop = changes['counter'];
let cur = prop.currentValue;
let prev = JSON.stringify(prop.previousValue); // first time is {}; after is integer
this.changeLog.push(`counter: currentValue = ${cur}, previousValue = ${prev}`);
}
}
/***************************************/
@Component({
selector: 'counter-parent',
template: `
<div class="parent">
<h2>Counter Spy</h2>
<button (click)="updateCounter()">Update counter</button>
<button (click)="reset()">Reset Counter</button>
<my-counter [counter]="value"></my-counter>
<h4>-- Spy Lifecycle Hook Log --</h4>
<div *ngFor="#msg of spyLog">{{msg}}</div>
</div>
`,
styles: ['.parent {background: gold; padding: 10px; margin:100px 8px;}'],
directives: [MyCounter],
providers: [LoggerService]
})
export class CounterParentComponent {
value: number;
spyLog:string[] = [];
private _logger:LoggerService;
constructor(logger:LoggerService){
this._logger = logger;
this.spyLog = logger.logs;
this.reset();
}
updateCounter() {
this.value += 1;
}
reset(){
this._logger.log('-- reset --');
this.value=0;
}
}

View File

@ -0,0 +1,19 @@
import {Injectable} from 'angular2/core';
@Injectable()
export class LoggerService {
logs:string[] = [];
log(msg:string, noTick:boolean = false) {
if (!noTick) { this.tick(); }
this.logs.push(msg);
}
clear() {this.logs.length = 0;}
tick() {
setTimeout(() => {
// console.log('tick')
}, 0);
}
}

View File

@ -0,0 +1,84 @@
// #docregion
import {
Component, Input, Output,
OnChanges, SimpleChange,
} from 'angular2/core';
export class Hero {
constructor(public name:string){}
}
@Component({
selector: 'my-hero',
template: `
<div class="hero">
<p>{{hero.name}} can {{power}}</p>
<h4>-- Change Log --</h4>
<div *ngFor="#chg of changeLog">{{chg}}</div>
</div>
`,
styles: [
'.hero {background: LightYellow; padding: 8px; margin-top: 8px}',
'p {background: Yellow; padding: 8px; margin-top: 8px}'
]
})
export class MyHeroComponent implements OnChanges {
@Input() hero: Hero;
@Input() power: string;
@Input() reset: {};
changeLog:string[] = [];
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
// Empty the changeLog whenever 'reset' property changes
// hint: this is a way to respond programmatically to external value changes.
if (changes['reset']) { this.changeLog.length = 0; }
for (let propName in changes) {
let prop = changes[propName];
let cur = JSON.stringify(prop.currentValue)
let prev = JSON.stringify(prop.previousValue); // first time is {}; after is integer
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
}
}
}
/***************************************/
@Component({
selector: 'on-changes-parent',
template: `
<div class="parent">
<h2>OnChanges</h2>
<div>Hero.name: <input [(ngModel)]="hero.name"> <i>does NOT trigger onChanges</i></div>
<div>Power: <input [(ngModel)]="power"> <i>DOES trigger onChanges</i></div>
<div><button (click)="reset()">Reset Log</button> <i>triggers onChanges and clears the change log</i></div>
<my-hero [hero]="hero" [power]="power" [reset]="resetTrigger"></my-hero>
</div>
`,
styles: ['.parent {background: Lavender; padding: 10px; margin:100px 8px;}'],
directives: [MyHeroComponent]
})
export class OnChangesParentComponent {
hero:Hero;
power:string;
resetTrigger = false;
constructor() {
this.reset();
}
reset(){
// new Hero object every time; triggers onChange
this.hero = new Hero('Windstorm');
// setting power only triggers onChange if this value is different
this.power = 'sing';
// always triggers onChange ... which is interpreted as a reset
this.resetTrigger = !this.resetTrigger;
}
}

View File

@ -0,0 +1,54 @@
// #docregion
import {Component} from 'angular2/core';
import {PeekABooComponent} from './peek-a-boo.component'
import {LoggerService} from './logger.service';
@Component({
selector: 'peek-a-boo-parent',
template: `
<div class="parent">
<h2>Peek-A-Boo</h2>
<button (click)="toggleChild()">
{{hasChild ? 'Destroy' : 'Create'}} PeekABooComponent
</button>
<button (click)="updateHero()" [hidden]="!hasChild">Update Hero</button>
<peek-a-boo *ngIf="hasChild" [name]="heroName">
</peek-a-boo>
<h4>-- Lifecycle Hook Log --</h4>
<div *ngFor="#msg of hookLog">{{msg}}</div>
</div>
`,
styles: ['.parent {background: moccasin; padding: 10px; margin:100px 8px}'],
directives: [PeekABooComponent],
providers: [LoggerService]
})
export class PeekABooParentComponent {
hasChild = false;
hookLog:string[];
heroName = 'Windstorm';
private _logger:LoggerService;
constructor(logger:LoggerService){
this._logger = logger;
this.hookLog = logger.logs;
}
toggleChild() {
this.hasChild = !this.hasChild;
if (this.hasChild) {
this.heroName = 'Windstorm';
this._logger.clear(); // clear log on create
}
this._logger.tick();
}
updateHero() {
this.heroName += '!';
this._logger.tick();
}
}

View File

@ -0,0 +1,98 @@
// #docregion
// #docregion lc-imports
import {
OnChanges, SimpleChange,
OnInit,
// DoCheck, // not demonstrated
AfterContentInit,
AfterContentChecked,
AfterViewInit,
AfterViewChecked,
OnDestroy
} from 'angular2/core';
// #docregion lc-imports
import {Component, Input, Output} from 'angular2/core';
import {LoggerService} from './logger.service';
let nextId = 1;
@Component({
selector: 'peek-a-boo',
template: '<p>Now you see my hero, {{name}}</p>',
styles: ['p {background: LightYellow; padding: 8px}']
})
// Don't HAVE to mention the Lifecycle Hook interfaces
// unless we want typing and tool support.
export class PeekABooComponent
implements OnChanges, OnInit,AfterContentInit,AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
@Input() name:string;
private _afterContentCheckedCounter = 1;
private _afterViewCheckedCounter = 1;
private _id = nextId++;
private _logger:LoggerService;
private _onChangesCounter = 1;
private _verb = 'initialized';
constructor(logger:LoggerService){
this._logger = logger;
}
// only called if there is an @input variable set by parent.
ngOnChanges(changes: {[propertyName: string]: SimpleChange}){
let changesMsgs:string[] = []
for (let propName in changes) {
if (propName === 'name') {
let name = changes['name'].currentValue;
changesMsgs.push(`name ${this._verb} to "${name}"`);
} else {
changesMsgs.push(propName + ' ' + this._verb);
}
}
this._logIt(`onChanges (${this._onChangesCounter++}): ${changesMsgs.join('; ')}`);
this._verb = 'changed'; // next time it will be a change
}
ngOnInit() {
this._logIt(`onInit`);
}
ngAfterContentInit(){
this._logIt(`afterContentInit`);
}
// Called after every change detection check
// of the component (directive) CONTENT
// Beware! Called frequently!
ngAfterContentChecked(){
let counter = this._afterContentCheckedCounter++;
let msg = `afterContentChecked (${counter})`;
this._logIt(msg);
}
ngAfterViewInit(){
this._logIt(`afterViewInit`);
}
// Called after every change detection check
// of the component (directive) VIEW
// Beware! Called frequently!
ngAfterViewChecked(){
let counter = this._afterViewCheckedCounter++;
let msg = `afterViewChecked (${counter})`;
this._logIt(msg);
}
ngOnDestroy() {
this._logIt(`onDestroy`);
}
private _logIt(msg:string){
// Don't tick or else
// the AfterContentChecked and AfterViewChecked recurse.
// Let parent call tick()
this._logger.log(`#${this._id } ${msg}`, true);
}
}

View File

@ -0,0 +1,55 @@
// #docregion
import {Component} from 'angular2/core';
import {LoggerService} from './logger.service';
import {Spy} from './spy.directive';
@Component({
selector: 'spy-parent',
template: `
<div class="parent">
<h2>Spy Directive</h2>
<input [(ngModel)]="newName" (keyup.enter)="addHero()">
<button (click)="addHero()">Add Hero</button>
<button (click)="reset()">Reset Heroes</button>
<p></p>
<div *ngFor="#hero of heroes" my-spy class="heroes">
{{hero}}
</div>
<h4>-- Spy Lifecycle Hook Log --</h4>
<div *ngFor="#msg of spyLog">{{msg}}</div>
</div>
`,
styles: [
'.parent {background: khaki; padding: 10px; margin:100px 8px}',
'.heroes {background: LightYellow; padding: 0 8px}'
],
directives: [Spy],
providers: [LoggerService]
})
export class SpyParentComponent {
newName = 'Herbie';
heroes:string[] = ['Windstorm', 'Magneta'];
spyLog:string[];
private _logger:LoggerService;
constructor(logger:LoggerService){
this._logger = logger;
this.spyLog = logger.logs;
}
addHero() {
if (this.newName.trim()) {
this.heroes.push(this.newName.trim());
this.newName = '';
}
}
reset(){
this._logger.log('-- reset --');
this.heroes.length = 0;
}
}

View File

@ -0,0 +1,33 @@
// #docregion
import {Directive, Input,
OnInit, OnDestroy} from 'angular2/core';
import {LoggerService} from './logger.service';
/***************************************/
let nextId = 1;
// Spy on any element to which it is applied.
// Usage: <div my-spy>...</div>
@Directive({selector: '[my-spy]'})
export class Spy implements OnInit, OnDestroy {
private _id = nextId++;
private _logger:LoggerService;
constructor(logger:LoggerService){
this._logger = logger;
}
ngOnInit() {
this._logIt(`onInit`);
}
ngOnDestroy() {
this._logIt(`onDestroy`);
}
private _logIt(msg:string){
this._logger.log(`Spy #${this._id } ${msg}`);
}
}

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<!-- #docregion -->
<html>
<head>
<title>Angular 2 Lifecycle Hooks</title>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<script>
System.config({
packages: {
app: {
format: 'register',
defaultExtension: 'js'
}
}
});
System.import('app/boot')
.then(null, console.error.bind(console));
</script>
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>

View File

@ -0,0 +1,7 @@
{
"description": "Lifecycle Hooks",
"files":["!**/*.d.ts", "!**/*.js"],
"tags": ["lifecycle", "hooks",
"onInit", "onDestroy", "onChange",
"ngOnInit", "ngOnDestroy", "ngOnChange"]
}

View File

@ -48,6 +48,11 @@
"intro": "Discover the basics of screen navigation with the Angular 2 router"
},
"lifecycle-hooks": {
"title": "Lifecycle Hooks",
"intro": "Angular calls lifecycle hook methods on our directives and components as it creates, changes, and destroys them."
},
"attribute-directives": {
"title": "Attribute Directives",
"intro": "Attribute directives attach behavior to elements."

View File

@ -0,0 +1,90 @@
include ../../../../_includes/_util-fns
:marked
# Component Lifecycle
A Component has a lifecycle managed by Angular itself. Angular creates it, renders it, creates and renders its children,
checks it when its data-bound properties change, and destroys it before removing it from the DOM.
Angular offers **Lifecycle hooks**
that give us visibility into these key moments and the ability to act when they occur.
We cover these hooks in this chapter and demonstrate how they work in code.
[Live Example](/resources/live-examples/lifecycle-hooks/ts/plnkr.html)
<!--
https://github.com/angular/angular/blob/master/modules/angular2/src/core/linker/interfaces.ts
-->
.l-main-section
:marked
## The Lifecycle Hooks
Directive and component instances have a lifecycle
as Angular creates, updates, and destroys them.
Developers can tap into key moments in that lifecycle by implementing
one or more of the "Lifecycle Hook" interfaces, all of them available
in the `angular2/core` library.
Here is the complete lifecycle hook interface inventory:
* `OnInit`
* `OnDestroy`
* `DoCheck`
* `OnChanges`
* `AfterContentInit`
* `AfterContentChecked`
* `AfterViewInit`
* `AfterViewChecked`
No directive or component will implement all of them and some of them only make
sense for components.
Each interface has a single hook method whose name is the interface name prefixed with `ng`.
For example, the `OnInit` interface has a hook method names `ngOnInit`.
Angular calls these hook methods in the following order:
* `ngOnChanges` - called when an input or output binding value changes
* `ngOnInit` - after the first `ngOnChanges`
* `ngDoCheck` - developer's custom change detection
* `ngAfterContentInit` - after component content initialized
* `ngAfterContentChecked` - after every check of component content
* `ngAfterViewInit` - after component's view(s) are initialized
* `ngAfterViewChecked` - after every check of a component's view(s)
* `ngOnDestroy` - just before the directive is destroyed.
The [live example](/resources/live-examples/lifecycle-hooks/ts/plnkr.html) demonstrates
these hooks.
:marked
## Peek-a-boo
The `PeekABooComponent` demonstrates all of the hooks in the same component.
.l-sub-section
:marked
Except for `DoCheck`. If our component superseded regular Angular change detection
with its own change detection processing
we would also add a `ngDoCheck` method. We would **not** implement `ngOnChanges`.
We write either `ngOnChanges` or `ngDoCheck`, not both.
Custom change detection and `ngDoCheck` are on our documentation backlog.
:marked
Peek-a-boo is a demo. We'd rarely if ever implement all interfaces like this in real life.
We look forward to explaining the Peek-a-boo example and the other lifecycle hook examples in
an update to this chapter. Meanwhile, please enjoy poking around in the
[code](/resources/live-examples/lifecycle-hooks/ts/plnkr.html).
## Interface optional?
The lifecycle interfaces are optional.
We recommend adding them to benefit from TypeScript's strong typing and editor tooling.
But they disappear from the transpiled JavaScript.
Angular can't see them at runtime. And they are useless to someone developing in
a language without interfaces (such as pure JavaScript).
Fortunately, they aren't necessary.
We don't have to add the lifecycle hook interfaces to our directives and components to benefit from the hooks themselves.
Angular instead inspects our directive and component classes
and calls the hook methods *if they are defined*.
Angular will find and call methods like `ngOnInit()`, with or without the interfaces.