docs(lifecycle-hooks): add hook sections, revise sample, add DoCheck & tests
|
@ -8,7 +8,7 @@ describe('Lifecycle hooks', function () {
|
|||
expect(element.all(by.css('h2')).get(0).getText()).toEqual('Peek-A-Boo');
|
||||
});
|
||||
|
||||
it('should be able to drive peek-a-boo button', function () {
|
||||
it('should support peek-a-boo', function () {
|
||||
var pabComp = element(by.css('peek-a-boo-parent peek-a-boo'));
|
||||
expect(pabComp.isPresent()).toBe(false, "should not be able to find the 'peek-a-boo' component");
|
||||
var pabButton = element.all(by.css('peek-a-boo-parent button')).get(0);
|
||||
|
@ -29,80 +29,118 @@ describe('Lifecycle hooks', function () {
|
|||
});
|
||||
});
|
||||
|
||||
it('should be able to trigger onChanges', function () {
|
||||
var onChangesViewEle = element.all(by.css('on-changes-parent my-hero div')).get(0);
|
||||
it('should support OnChanges hook', function () {
|
||||
var onChangesViewEle = element.all(by.css('on-changes div')).get(0);
|
||||
var inputEles = element.all(by.css('on-changes-parent input'));
|
||||
var heroNameInputEle = inputEles.get(0);
|
||||
var powerInputEle = inputEles.get(1);
|
||||
var heroNameInputEle = inputEles.get(1);
|
||||
var powerInputEle = inputEles.get(0);
|
||||
var titleEle = onChangesViewEle.element(by.css('p'));
|
||||
expect(titleEle.getText()).toContain('Windstorm can sing');
|
||||
var changeLogEles = onChangesViewEle.all(by.css('div'));
|
||||
expect(changeLogEles.count()).toEqual(3, "should start with 3 messages");
|
||||
|
||||
expect(titleEle.getText()).toContain('Windstorm can sing');
|
||||
expect(changeLogEles.count()).toEqual(2, "should start with 2 messages");
|
||||
// heroNameInputEle.sendKeys('-foo-').then(function () {
|
||||
sendKeys(heroNameInputEle, '-foo-').then(function () {
|
||||
expect(titleEle.getText()).toContain('Windstorm-foo- can sing');
|
||||
expect(changeLogEles.count()).toEqual(3, "should still have 3 messages");
|
||||
expect(changeLogEles.count()).toEqual(2, "should still have 2 messages");
|
||||
// protractor bug with sendKeys means that line below does not work.
|
||||
// return powerInputEle.sendKeys('-bar-');
|
||||
return sendKeys(powerInputEle, '-bar-');
|
||||
}).then(function () {
|
||||
expect(titleEle.getText()).toContain('Windstorm-foo- can sing-bar-');
|
||||
// 8 == 3 previously + length of '-bar-'
|
||||
expect(changeLogEles.count()).toEqual(8, "should have 8 messages now");
|
||||
// 7 == 2 previously + length of '-bar-'
|
||||
expect(changeLogEles.count()).toEqual(7, "should have 7 messages now");
|
||||
});
|
||||
});
|
||||
|
||||
it('should support after-view hooks', function () {
|
||||
var inputEle = element(by.css('after-view-parent input'));
|
||||
var buttonEle = element(by.css('after-view-parent button'));
|
||||
var logEles = element.all(by.css('after-view-parent h4 ~ div'));
|
||||
var childViewTextEle = element(by.css('after-view-parent my-child .child'));
|
||||
expect(childViewTextEle.getText()).toContain('Magneta is my hero');
|
||||
expect(logEles.count()).toBeGreaterThan(2);
|
||||
it('should support DoCheck hook', function () {
|
||||
var doCheckViewEle = element.all(by.css('do-check div')).get(0);
|
||||
var inputEles = element.all(by.css('do-check-parent input'));
|
||||
var heroNameInputEle = inputEles.get(1);
|
||||
var powerInputEle = inputEles.get(0);
|
||||
var titleEle = doCheckViewEle.element(by.css('p'));
|
||||
var changeLogEles = doCheckViewEle.all(by.css('div'));
|
||||
var logCount;
|
||||
|
||||
expect(titleEle.getText()).toContain('Windstorm can sing');
|
||||
changeLogEles.count().then(function(count) {
|
||||
expect(count).toBeGreaterThan(3, "should start with at least 4 messages");
|
||||
logCount = count;
|
||||
// heroNameInputEle.sendKeys('-foo-').then(function () {
|
||||
return sendKeys(heroNameInputEle, '-foo-')
|
||||
}).then(function () {
|
||||
expect(titleEle.getText()).toContain('Windstorm-foo- can sing');
|
||||
return changeLogEles.count()
|
||||
}).then(function (count) {
|
||||
expect(count).toEqual(logCount + 10, 'should add 10 more messages')
|
||||
logCount = count;
|
||||
// return powerInputEle.sendKeys('-bar-');
|
||||
return sendKeys(powerInputEle, '-bar-');
|
||||
}).then(function () {
|
||||
expect(titleEle.getText()).toContain('Windstorm-foo- can sing-bar-');
|
||||
// 7 == 2 previously + length of '-bar-'
|
||||
expect(changeLogEles.count()).toEqual(logCount + 15, 'should add 15 more messages');
|
||||
});
|
||||
});
|
||||
|
||||
it('should support AfterView hooks', function () {
|
||||
var parentEle = element(by.tagName('after-view-parent'));
|
||||
var buttonEle = parentEle.element(by.tagName('button')); // Reset
|
||||
var commentEle = parentEle.element(by.className('comment'));
|
||||
var logEles = parentEle.all(by.css('h4 ~ div'));
|
||||
var childViewInputEle = parentEle.element(by.css('my-child input'));
|
||||
var logCount;
|
||||
|
||||
expect(childViewInputEle.getAttribute('value')).toContain('Magneta');
|
||||
expect(commentEle.isPresent()).toBe(false, 'comment should not be in DOM');
|
||||
|
||||
logEles.count().then(function(count) {
|
||||
logCount = count;
|
||||
return sendKeys(inputEle, "-test-");
|
||||
return sendKeys(childViewInputEle, "-test-");
|
||||
}).then(function() {
|
||||
expect(childViewTextEle.getText()).toContain('-test-');
|
||||
expect(childViewInputEle.getAttribute('value')).toContain('-test-');
|
||||
expect(commentEle.isPresent()).toBe(true,'should have comment because >10 chars');
|
||||
expect(commentEle.getText()).toContain('long name');
|
||||
return logEles.count();
|
||||
}).then(function(count) {
|
||||
expect(logCount + 6).toEqual(count, "6 additional log messages should have been added");
|
||||
expect(logCount + 11).toEqual(count, "11 additional log messages should have been added");
|
||||
logCount = count;
|
||||
return buttonEle.click();
|
||||
}).then(function() {
|
||||
expect(childViewTextEle.isPresent()).toBe(false,"child view should no longer be part of the DOM");
|
||||
sendKeys(inputEle, "-foo-");
|
||||
expect(logEles.count()).toEqual(logCount, "no additional log messages should have been added");
|
||||
expect(logEles.count()).toBeLessThan(logCount, "log should shrink after reset");
|
||||
});
|
||||
});
|
||||
|
||||
it('should support after-content hooks', function () {
|
||||
var inputEle = element(by.css('after-content-parent input'));
|
||||
var buttonEle = element(by.css('after-content-parent button'));
|
||||
var logEles = element.all(by.css('after-content-parent h4 ~ div'));
|
||||
var childViewTextEle = element(by.css('after-content-parent my-child .child'));
|
||||
expect(childViewTextEle.getText()).toContain('Magneta is my hero');
|
||||
expect(logEles.count()).toBeGreaterThan(2);
|
||||
|
||||
it('should support AfterContent hooks', function () {
|
||||
var parentEle = element(by.tagName('after-content-parent'));
|
||||
var buttonEle = parentEle.element(by.tagName('button')); // Reset
|
||||
var commentEle = parentEle.element(by.className('comment'));
|
||||
var logEles = parentEle.all(by.css('h4 ~ div'));
|
||||
var childViewInputEle = parentEle.element(by.css('my-child input'));
|
||||
var logCount;
|
||||
|
||||
expect(childViewInputEle.getAttribute('value')).toContain('Magneta');
|
||||
expect(commentEle.isPresent()).toBe(false, 'comment should not be in DOM');
|
||||
|
||||
logEles.count().then(function(count) {
|
||||
logCount = count;
|
||||
return sendKeys(inputEle, "-test-");
|
||||
return sendKeys(childViewInputEle, "-test-");
|
||||
}).then(function() {
|
||||
expect(childViewTextEle.getText()).toContain('-test-');
|
||||
expect(childViewInputEle.getAttribute('value')).toContain('-test-');
|
||||
expect(commentEle.isPresent()).toBe(true,'should have comment because >10 chars');
|
||||
expect(commentEle.getText()).toContain('long name');
|
||||
return logEles.count();
|
||||
}).then(function(count) {
|
||||
expect(logCount + 6).toEqual(count, "6 additional log messages should have been added");
|
||||
expect(logCount + 11).toEqual(count, "11 additional log messages should have been added");
|
||||
logCount = count;
|
||||
return buttonEle.click();
|
||||
}).then(function() {
|
||||
expect(childViewTextEle.isPresent()).toBe(false,"child view should no longer be part of the DOM");
|
||||
sendKeys(inputEle, "-foo-");
|
||||
expect(logEles.count()).toEqual(logCount, "no additional log messages should have been added");
|
||||
expect(logEles.count()).toBeLessThan(logCount, "log should shrink after reset");
|
||||
});
|
||||
});
|
||||
|
||||
it('should support "spy" hooks', function () {
|
||||
it('should support spy\'s OnInit & OnDestroy hooks', function () {
|
||||
var inputEle = element(by.css('spy-parent input'));
|
||||
var addHeroButtonEle = element(by.cssContainingText('spy-parent button','Add Hero'));
|
||||
var resetHeroesButtonEle = element(by.cssContainingText('spy-parent button','Reset Heroes'));
|
||||
|
@ -123,7 +161,7 @@ describe('Lifecycle hooks', function () {
|
|||
})
|
||||
});
|
||||
|
||||
it('should support "spy counter" hooks', function () {
|
||||
it('should support "spy counter"', function () {
|
||||
var updateCounterButtonEle = element(by.cssContainingText('counter-parent button','Update'));
|
||||
var resetCounterButtonEle = element(by.cssContainingText('counter-parent button','Reset'));
|
||||
var textEle = element(by.css('counter-parent my-counter > div'));
|
||||
|
|
|
@ -1,102 +1,119 @@
|
|||
// #docplaster
|
||||
// #docregion
|
||||
import {
|
||||
Component, Input, Output,
|
||||
AfterContentChecked, AfterContentInit, ContentChild,
|
||||
AfterViewInit, ViewChild
|
||||
} from 'angular2/core';
|
||||
import {Component, AfterContentChecked, AfterContentInit, ContentChild} from 'angular2/core';
|
||||
|
||||
import {ChildComponent} from './child.component';
|
||||
import {LoggerService} from './logger.service';
|
||||
|
||||
//////////////////
|
||||
@Component({
|
||||
selector: 'my-child',
|
||||
template: '<input [(ngModel)]="hero">'
|
||||
})
|
||||
export class ChildComponent {
|
||||
hero = 'Magneta';
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
@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;}'],
|
||||
|
||||
// #docregion template
|
||||
template:`
|
||||
<div>-- projected content begins --</div>
|
||||
<ng-content></ng-content>
|
||||
<div>-- projected content ends --</div>`
|
||||
// #enddocregion template
|
||||
+ `
|
||||
<p *ngIf="comment" class="comment">
|
||||
{{comment}}
|
||||
</p>
|
||||
`
|
||||
})
|
||||
export class AfterContentComponent
|
||||
implements AfterContentChecked, AfterContentInit, AfterViewInit {
|
||||
|
||||
private _logger:LoggerService;
|
||||
|
||||
constructor(logger:LoggerService){
|
||||
this._logger = logger;
|
||||
logger.log('AfterContent ctor: ' + this._getMessage());
|
||||
}
|
||||
// #docregion hooks
|
||||
export class AfterContentComponent implements AfterContentChecked, AfterContentInit {
|
||||
private _prevHero = '';
|
||||
|
||||
// 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;
|
||||
// #enddocregion hooks
|
||||
constructor(private _logger:LoggerService){
|
||||
this._logIt('AfterContent constructor');
|
||||
}
|
||||
|
||||
|
||||
///// Hooks
|
||||
// #docregion hooks
|
||||
ngAfterContentInit() {
|
||||
// contentChild is set after the content has been initialized
|
||||
this._logger.log('AfterContentInit: ' + this._getMessage());
|
||||
// viewChild is set after the view has been initialized
|
||||
this._logIt('AfterContentInit');
|
||||
this._doSomething();
|
||||
}
|
||||
|
||||
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());
|
||||
// viewChild is updated after the view has been checked
|
||||
if (this._prevHero === this.contentChild.hero) {
|
||||
this._logIt('AfterContentChecked (no change)');
|
||||
} else {
|
||||
this._prevHero = this.contentChild.hero;
|
||||
this._logIt('AfterContentChecked');
|
||||
this._doSomething();
|
||||
}
|
||||
}
|
||||
// #enddocregion hooks
|
||||
|
||||
comment = '';
|
||||
|
||||
// #docregion do-something
|
||||
|
||||
// This surrogate for real business logic sets the `comment`
|
||||
private _doSomething() {
|
||||
this.comment = this.contentChild.hero.length > 10 ? "That's a long name" : '';
|
||||
}
|
||||
|
||||
private _getMessage(): string {
|
||||
let cmp = this.contentChild;
|
||||
return cmp ? `"${cmp.hero}" child content` : 'no child content';
|
||||
private _logIt(method:string){
|
||||
let vc = this.contentChild;
|
||||
let message = `${method}: ${vc ? vc.hero:'no'} child view`
|
||||
this._logger.log(message);
|
||||
}
|
||||
|
||||
// #docregion hooks
|
||||
// ...
|
||||
}
|
||||
// #enddocregion hooks
|
||||
|
||||
/***************************************/
|
||||
|
||||
//////////////
|
||||
@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>
|
||||
<div *ngIf="show">` +
|
||||
// #docregion parent-template
|
||||
`<after-content>
|
||||
<my-child></my-child>
|
||||
</after-content>`
|
||||
// #enddocregion parent-template
|
||||
+ `</div>
|
||||
|
||||
<my-child *ngIf="showChild" [hero]="hero"></my-child>
|
||||
</after-content>
|
||||
|
||||
<h4>-- Lifecycle Hook Log --</h4>
|
||||
<div *ngFor="#msg of hookLog">{{msg}}</div>
|
||||
<h4>-- AfterContent Logs --</h4>
|
||||
<p><button (click)="reset()">Reset</button></p>
|
||||
<div *ngFor="#msg of logs">{{msg}}</div>
|
||||
</div>
|
||||
`,
|
||||
styles: ['.parent {background: powderblue; padding: 8px; margin:100px 8px;}'],
|
||||
directives: [AfterContentComponent, ChildComponent],
|
||||
providers:[LoggerService]
|
||||
styles: ['.parent {background: burlywood}'],
|
||||
providers:[LoggerService],
|
||||
directives: [AfterContentComponent, ChildComponent]
|
||||
})
|
||||
export class AfterContentParentComponent {
|
||||
|
||||
hookLog:string[];
|
||||
hero = 'Magneta';
|
||||
showChild = true;
|
||||
logs:string[];
|
||||
show = true;
|
||||
|
||||
constructor(logger:LoggerService){
|
||||
this.hookLog = logger.logs;
|
||||
this.logs = logger.logs;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.logs.length=0;
|
||||
// quickly remove and reload AfterContentComponent which recreates it
|
||||
this.show = false;
|
||||
setTimeout(() => this.show = true, 0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,80 +1,121 @@
|
|||
// #docplaster
|
||||
// #docregion
|
||||
import {
|
||||
Component, Input, Output,
|
||||
AfterContentInit, ContentChild,
|
||||
AfterViewChecked, AfterViewInit, ViewChild
|
||||
} from 'angular2/core';
|
||||
import {Component, AfterViewChecked, AfterViewInit, ViewChild} from 'angular2/core';
|
||||
|
||||
import {ChildComponent} from './child.component';
|
||||
import {LoggerService} from './logger.service';
|
||||
|
||||
//////////////////
|
||||
// #docregion child-view
|
||||
@Component({
|
||||
selector: 'my-child',
|
||||
template: '<input [(ngModel)]="hero">'
|
||||
})
|
||||
export class ChildViewComponent {
|
||||
hero = 'Magneta';
|
||||
}
|
||||
// #enddocregion child-view
|
||||
|
||||
//////////////////////
|
||||
@Component({
|
||||
selector: 'after-view',
|
||||
// #docregion template
|
||||
template: `
|
||||
<div>-- child view begins --</div>
|
||||
<my-child></my-child>
|
||||
<div>-- child view ends --</div>`
|
||||
// #enddocregion template
|
||||
+ `
|
||||
<p *ngIf="comment" class="comment">
|
||||
{{comment}}
|
||||
</p>
|
||||
`,
|
||||
|
||||
directives: [ChildViewComponent]
|
||||
})
|
||||
// #docregion hooks
|
||||
export class AfterViewComponent implements AfterViewChecked, AfterViewInit {
|
||||
private _prevHero = '';
|
||||
|
||||
// Query for a VIEW child of type `ChildViewComponent`
|
||||
@ViewChild(ChildViewComponent) viewChild: ChildViewComponent;
|
||||
|
||||
// #enddocregion hooks
|
||||
constructor(private _logger:LoggerService){
|
||||
this._logIt('AfterView constructor');
|
||||
}
|
||||
|
||||
// #docregion hooks
|
||||
ngAfterViewInit() {
|
||||
// viewChild is set after the view has been initialized
|
||||
this._logIt('AfterViewInit');
|
||||
this._doSomething();
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
// viewChild is updated after the view has been checked
|
||||
if (this._prevHero === this.viewChild.hero) {
|
||||
this._logIt('AfterViewChecked (no change)');
|
||||
} else {
|
||||
this._prevHero = this.viewChild.hero;
|
||||
this._logIt('AfterViewChecked');
|
||||
this._doSomething();
|
||||
}
|
||||
}
|
||||
// #enddocregion hooks
|
||||
|
||||
comment = '';
|
||||
|
||||
// #docregion do-something
|
||||
// This surrogate for real business logic sets the `comment`
|
||||
private _doSomething() {
|
||||
let c = this.viewChild.hero.length > 10 ? "That's a long name" : '';
|
||||
if (c !== this.comment) {
|
||||
// Wait a tick because the component's view has already been checked
|
||||
setTimeout(() => this.comment = c, 0);
|
||||
}
|
||||
}
|
||||
// #enddocregion do-something
|
||||
|
||||
private _logIt(method:string){
|
||||
let vc = this.viewChild;
|
||||
let message = `${method}: ${vc ? vc.hero:'no'} child view`
|
||||
this._logger.log(message);
|
||||
}
|
||||
// #docregion hooks
|
||||
// ...
|
||||
}
|
||||
// #enddocregion hooks
|
||||
|
||||
//////////////
|
||||
@Component({
|
||||
selector: 'after-view-parent',
|
||||
template: `
|
||||
<div class="parent">
|
||||
<h2>AfterView</h2>
|
||||
|
||||
<div>
|
||||
<input [(ngModel)]="hero">
|
||||
<button (click)="showChild = !showChild">Toggle child view</button>
|
||||
<after-view *ngIf="show"></after-view>
|
||||
|
||||
<my-child *ngIf="showChild" [hero]="hero"></my-child>
|
||||
</div>
|
||||
|
||||
<h4>-- Lifecycle Hook Log --</h4>
|
||||
<div *ngFor="#msg of hookLog">{{msg}}</div>
|
||||
<h4>-- AfterView Logs --</h4>
|
||||
<p><button (click)="reset()">Reset</button></p>
|
||||
<div *ngFor="#msg of logs">{{msg}}</div>
|
||||
</div>
|
||||
`,
|
||||
styles: ['.parent {background: burlywood; padding: 8px; margin:100px 8px;}'],
|
||||
directives: [ChildComponent],
|
||||
providers:[LoggerService]
|
||||
styles: ['.parent {background: burlywood}'],
|
||||
providers:[LoggerService],
|
||||
directives: [AfterViewComponent]
|
||||
})
|
||||
export class AfterViewParentComponent
|
||||
implements AfterContentInit, AfterViewChecked, AfterViewInit {
|
||||
|
||||
private _logger:LoggerService;
|
||||
export class AfterViewParentComponent {
|
||||
logs:string[];
|
||||
show = true;
|
||||
|
||||
constructor(logger:LoggerService){
|
||||
this._logger = logger;
|
||||
this.hookLog = logger.logs;
|
||||
logger.log('AfterView ctor: ' + this._getMessage());
|
||||
this.logs = logger.logs;
|
||||
}
|
||||
|
||||
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`);
|
||||
reset() {
|
||||
this.logs.length=0;
|
||||
// quickly remove and reload AfterViewComponent which recreates it
|
||||
this.show = false;
|
||||
setTimeout(() => this.show = true, 0)
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<a id="top"></a>
|
||||
<h1>Component Lifecycle Hooks</h1>
|
||||
<a href="#hooks">Peek-a-boo: (most) lifecycle hooks</a><br>
|
||||
<a href="#onchanges">OnChanges</a><br>
|
||||
<a href="#docheck">DoCheck</a><br>
|
||||
<a href="#after-view">AfterViewInit & AfterViewChecked</a><br>
|
||||
<a href="#after-content">AfterContentInit & AfterContentChecked</a><br>
|
||||
<a href="#spy">Spy: directive with OnInit & OnDestroy</a><br>
|
||||
<a href="#counter">Counter: OnChanges + Spy directive</a><br>
|
||||
|
||||
<a id="hooks"></a>
|
||||
<peek-a-boo-parent></peek-a-boo-parent>
|
||||
<a href="#top">back to top</a>
|
||||
|
||||
<a id="spy"></a>
|
||||
<spy-parent></spy-parent>
|
||||
<a href="#top">back to top</a>
|
||||
|
||||
<a id="onchanges"></a>
|
||||
<on-changes-parent></on-changes-parent>
|
||||
<a href="#top">back to top</a>
|
||||
|
||||
<a id="docheck"></a>
|
||||
<do-check-parent></do-check-parent>
|
||||
<a href="#top">back to top</a>
|
||||
|
||||
<a id="after-view"></a>
|
||||
<after-view-parent></after-view-parent>
|
||||
<a href="#top">back to top</a>
|
||||
|
||||
<a id="after-content"></a>
|
||||
<after-content-parent></after-content-parent>
|
||||
<a href="#top">back to top</a>
|
||||
|
||||
<a id="counter"></a>
|
||||
<counter-parent></counter-parent>
|
||||
<a href="#top">back to top</a>
|
|
@ -4,39 +4,22 @@ import {Component} from 'angular2/core';
|
|||
import {AfterContentParentComponent} from './after-content.component';
|
||||
import {AfterViewParentComponent} from './after-view.component';
|
||||
import {CounterParentComponent} from './counter.component';
|
||||
import {DoCheckParentComponent} from './do-check.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>
|
||||
`,
|
||||
templateUrl: 'app/app.component.html',
|
||||
directives: [
|
||||
AfterContentParentComponent,
|
||||
AfterViewParentComponent,
|
||||
CounterParentComponent,
|
||||
DoCheckParentComponent,
|
||||
OnChangesParentComponent,
|
||||
PeekABooParentComponent,
|
||||
SpyParentComponent,
|
||||
CounterParentComponent
|
||||
]
|
||||
})
|
||||
export class AppComponent {
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
// #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;
|
||||
}
|
|
@ -58,7 +58,7 @@ export class MyCounter implements OnChanges {
|
|||
<div *ngFor="#msg of spyLog">{{msg}}</div>
|
||||
</div>
|
||||
`,
|
||||
styles: ['.parent {background: gold; padding: 10px; margin:100px 8px;}'],
|
||||
styles: ['.parent {background: gold;}'],
|
||||
directives: [MyCounter],
|
||||
providers: [LoggerService]
|
||||
})
|
||||
|
@ -76,11 +76,13 @@ export class CounterParentComponent {
|
|||
|
||||
updateCounter() {
|
||||
this.value += 1;
|
||||
this._logger.tick();
|
||||
}
|
||||
|
||||
reset(){
|
||||
this._logger.log('-- reset --');
|
||||
this.value=0;
|
||||
this._logger.tick();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
// #docregion
|
||||
import {Component, DoCheck, OnChanges, Input, SimpleChange, ViewChild} from 'angular2/core';
|
||||
|
||||
class Hero {
|
||||
constructor(public name:string){}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'do-check',
|
||||
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 DoCheckComponent implements DoCheck, OnChanges {
|
||||
@Input() hero: Hero;
|
||||
@Input() power: string;
|
||||
|
||||
changeDetected = false;
|
||||
changeLog:string[] = [];
|
||||
oldHeroName = '';
|
||||
oldPower = '';
|
||||
oldLogLength = 0;
|
||||
noChangeCount = 0;
|
||||
|
||||
// #docregion ng-do-check
|
||||
ngDoCheck() {
|
||||
|
||||
if (this.hero.name !== this.oldHeroName) {
|
||||
this.changeDetected = true;
|
||||
this.changeLog.push(`DoCheck: Hero name changed to "${this.hero.name}" from "${this.oldHeroName}"`)
|
||||
this.oldHeroName = this.hero.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 that hook was called when there was no relevant change.
|
||||
let count = this.noChangeCount += 1
|
||||
let noChangeMsg = `DoCheck called ${count}x when no change to hero or power`;
|
||||
if (count === 1) {
|
||||
// add new "no change" message
|
||||
this.changeLog.push(noChangeMsg);
|
||||
} else {
|
||||
// update last "no change" message
|
||||
this.changeLog[this.changeLog.length-1] = noChangeMsg;
|
||||
}
|
||||
}
|
||||
|
||||
this.changeDetected = false;
|
||||
}
|
||||
// #enddocregion ng-do-check
|
||||
|
||||
// Copied from OnChangesComponent
|
||||
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
|
||||
for (let propName in changes) {
|
||||
let prop = changes[propName];
|
||||
let cur = JSON.stringify(prop.currentValue)
|
||||
let prev = JSON.stringify(prop.previousValue);
|
||||
this.changeLog.push(`OnChanges: ${propName}: currentValue = ${cur}, previousValue = ${prev}`);
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.changeDetected = true;
|
||||
this.changeLog.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************/
|
||||
|
||||
@Component({
|
||||
selector: 'do-check-parent',
|
||||
templateUrl:'app/on-changes-parent.component.html',
|
||||
styles: ['.parent {background: Lavender}'],
|
||||
directives: [DoCheckComponent]
|
||||
})
|
||||
export class DoCheckParentComponent {
|
||||
hero:Hero;
|
||||
power:string;
|
||||
title = 'DoCheck';
|
||||
@ViewChild(DoCheckComponent) childView:DoCheckComponent;
|
||||
|
||||
constructor() { this.reset(); }
|
||||
|
||||
reset(){
|
||||
this.hero = new Hero('Windstorm');
|
||||
this.power = 'sing';
|
||||
this.childView && this.childView.reset();
|
||||
}
|
||||
}
|
|
@ -3,14 +3,24 @@ import {Injectable} from 'angular2/core';
|
|||
@Injectable()
|
||||
export class LoggerService {
|
||||
logs:string[] = [];
|
||||
prevMsg = '';
|
||||
prevMsgCount = 1;
|
||||
|
||||
log(msg:string, noTick:boolean = false) {
|
||||
if (!noTick) { this.tick(); }
|
||||
this.logs.push(msg);
|
||||
log(msg:string) {
|
||||
if (msg === this.prevMsg) {
|
||||
// Repeat message; update last log entry with count.
|
||||
this.logs[this.logs.length-1] = msg + ` (${this.prevMsgCount+=1}x)`;
|
||||
} else {
|
||||
// New message; log it.
|
||||
this.prevMsg = msg;
|
||||
this.prevMsgCount = 1;
|
||||
this.logs.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {this.logs.length = 0;}
|
||||
|
||||
// schedules a view refresh to ensure display catches up
|
||||
tick() {
|
||||
setTimeout(() => {
|
||||
// console.log('tick')
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<div class="parent">
|
||||
<h2>{{title}}</h2>
|
||||
|
||||
<table>
|
||||
<tr><td>Power: </td><td><input [(ngModel)]="power"></td></tr>
|
||||
<tr><td>Hero.name: </td><td><input [(ngModel)]="hero.name"></td></tr>
|
||||
</table>
|
||||
<p><button (click)="reset()">Reset Log</button></p>
|
||||
|
||||
<!-- #docregion on-changes -->
|
||||
<on-changes [hero]="hero" [power]="power"></on-changes>
|
||||
<!-- #enddocregion on-changes -->
|
||||
<do-check [hero]="hero" [power]="power"></do-check>
|
||||
</div>
|
|
@ -1,16 +1,16 @@
|
|||
// #docregion
|
||||
import {
|
||||
Component, Input, Output,
|
||||
OnChanges, SimpleChange,
|
||||
Component, Input, ViewChild,
|
||||
OnChanges, SimpleChange
|
||||
} from 'angular2/core';
|
||||
|
||||
|
||||
export class Hero {
|
||||
class Hero {
|
||||
constructor(public name:string){}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-hero',
|
||||
selector: 'on-changes',
|
||||
template: `
|
||||
<div class="hero">
|
||||
<p>{{hero.name}} can {{power}}</p>
|
||||
|
@ -24,61 +24,51 @@ export class Hero {
|
|||
'p {background: Yellow; padding: 8px; margin-top: 8px}'
|
||||
]
|
||||
})
|
||||
export class MyHeroComponent implements OnChanges {
|
||||
export class OnChangesComponent implements OnChanges {
|
||||
// #docregion inputs
|
||||
@Input() hero: Hero;
|
||||
@Input() power: string;
|
||||
@Input() reset: {};
|
||||
// #enddocregion inputs
|
||||
|
||||
changeLog:string[] = [];
|
||||
|
||||
// #docregion ng-on-changes
|
||||
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
|
||||
let cur = JSON.stringify(prop.currentValue)
|
||||
let prev = JSON.stringify(prop.previousValue);
|
||||
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
|
||||
}
|
||||
}
|
||||
// #enddocregion ng-on-changes
|
||||
|
||||
reset() { this.changeLog.length = 0; }
|
||||
}
|
||||
|
||||
/***************************************/
|
||||
|
||||
@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]
|
||||
templateUrl:'app/on-changes-parent.component.html',
|
||||
styles: ['.parent {background: Lavender;}'],
|
||||
directives: [OnChangesComponent]
|
||||
})
|
||||
export class OnChangesParentComponent {
|
||||
hero:Hero;
|
||||
power:string;
|
||||
resetTrigger = false;
|
||||
title = 'OnChanges';
|
||||
@ViewChild(OnChangesComponent) childView:OnChangesComponent;
|
||||
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset(){
|
||||
// new Hero object every time; triggers onChange
|
||||
// new Hero object every time; triggers onChanges
|
||||
this.hero = new Hero('Windstorm');
|
||||
// setting power only triggers onChange if this value is different
|
||||
// setting power only triggers onChanges if this value is different
|
||||
this.power = 'sing';
|
||||
// always triggers onChange ... which is interpreted as a reset
|
||||
this.resetTrigger = !this.resetTrigger;
|
||||
this.childView && this.childView.reset();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import {LoggerService} from './logger.service';
|
|||
<div *ngFor="#msg of hookLog">{{msg}}</div>
|
||||
</div>
|
||||
`,
|
||||
styles: ['.parent {background: moccasin; padding: 10px; margin:100px 8px}'],
|
||||
styles: ['.parent {background: moccasin}'],
|
||||
directives: [PeekABooComponent],
|
||||
providers: [LoggerService]
|
||||
})
|
||||
|
|
|
@ -1,21 +1,31 @@
|
|||
// #docregion
|
||||
// #docregion lc-imports
|
||||
import {
|
||||
OnChanges, SimpleChange,
|
||||
OnInit,
|
||||
// DoCheck, // not demonstrated
|
||||
DoCheck,
|
||||
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;
|
||||
|
||||
// #docregion ngOnInit
|
||||
export class PeekABoo implements OnInit {
|
||||
constructor(private _logger:LoggerService) { }
|
||||
|
||||
// implement OnInit's `ngOnInit` method
|
||||
ngOnInit() { this._logIt(`OnInit`); }
|
||||
|
||||
protected _logIt(msg:string){
|
||||
this._logger.log(`#${nextId++} ${msg}`);
|
||||
}
|
||||
}
|
||||
// #enddocregion ngOnInit
|
||||
|
||||
@Component({
|
||||
selector: 'peek-a-boo',
|
||||
template: '<p>Now you see my hero, {{name}}</p>',
|
||||
|
@ -23,23 +33,23 @@ let nextId = 1;
|
|||
})
|
||||
// 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 {
|
||||
export class PeekABooComponent extends PeekABoo implements
|
||||
OnChanges, OnInit, DoCheck,
|
||||
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;
|
||||
constructor(logger:LoggerService) {
|
||||
super(logger);
|
||||
|
||||
let is = this.name ? 'is' : 'is not';
|
||||
this._logIt(`name ${is} known at construction`);
|
||||
}
|
||||
|
||||
// only called if there is an @input variable set by parent.
|
||||
// only called for/if there is an @input variable set by parent.
|
||||
ngOnChanges(changes: {[propertyName: string]: SimpleChange}){
|
||||
let changesMsgs:string[] = []
|
||||
for (let propName in changes) {
|
||||
|
@ -50,49 +60,25 @@ export class PeekABooComponent
|
|||
changesMsgs.push(propName + ' ' + this._verb);
|
||||
}
|
||||
}
|
||||
this._logIt(`onChanges (${this._onChangesCounter++}): ${changesMsgs.join('; ')}`);
|
||||
this._logIt(`OnChanges: ${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);
|
||||
}
|
||||
// Called in every change detection cycle anywhere on the page
|
||||
ngDoCheck(){ this._logIt(`DoCheck`); }
|
||||
|
||||
ngAfterViewInit(){
|
||||
this._logIt(`afterViewInit`);
|
||||
}
|
||||
ngAfterContentInit(){ this._logIt(`AfterContentInit`); }
|
||||
|
||||
// Called after every change detection check
|
||||
// of the component (directive) VIEW
|
||||
// Beware! Called frequently!
|
||||
// Called in every change detection cycle anywhere on the page
|
||||
ngAfterContentChecked(){ this._logIt(`AfterContentChecked`); }
|
||||
|
||||
ngAfterViewChecked(){
|
||||
let counter = this._afterViewCheckedCounter++;
|
||||
let msg = `afterViewChecked (${counter})`;
|
||||
this._logIt(msg);
|
||||
}
|
||||
ngAfterViewInit(){ this._logIt(`AfterViewInit`); }
|
||||
|
||||
ngOnDestroy() {
|
||||
this._logIt(`onDestroy`);
|
||||
}
|
||||
// Beware! Called frequently!
|
||||
// Called in every change detection cycle anywhere on the page
|
||||
ngAfterViewChecked(){ this._logIt(`AfterViewChecked`); }
|
||||
|
||||
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);
|
||||
}
|
||||
ngOnDestroy() { this._logIt(`OnDestroy`); }
|
||||
}
|
|
@ -8,48 +8,52 @@ import {Spy} from './spy.directive';
|
|||
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>
|
||||
<p>
|
||||
<input [(ngModel)]="newName"
|
||||
(keyup.enter)="addHero()"
|
||||
placeholder="Hero name">
|
||||
<button (click)="addHero()">Add Hero</button>
|
||||
<button (click)="reset()">Reset Heroes</button>
|
||||
</p>` +
|
||||
// #docregion template
|
||||
`<div *ngFor="#hero of heroes" my-spy class="heroes">
|
||||
{{hero}}
|
||||
</div>`
|
||||
// #enddocregion template
|
||||
+ `<h4>-- Spy Lifecycle Hook Log --</h4>
|
||||
<div *ngFor="#msg of spyLog">{{msg}}</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
'.parent {background: khaki; padding: 10px; margin:100px 8px}',
|
||||
'.parent {background: khaki;}',
|
||||
'.heroes {background: LightYellow; padding: 0 8px}'
|
||||
],
|
||||
directives: [Spy],
|
||||
providers: [LoggerService]
|
||||
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;
|
||||
constructor(private _logger:LoggerService){
|
||||
this.spyLog = _logger.logs;
|
||||
}
|
||||
|
||||
addHero() {
|
||||
if (this.newName.trim()) {
|
||||
this.heroes.push(this.newName.trim());
|
||||
this.newName = '';
|
||||
this._logger.tick();
|
||||
}
|
||||
}
|
||||
|
||||
removeHero(hero:string) {
|
||||
this.heroes.splice(this.heroes.indexOf(hero), 1);
|
||||
this._logger.tick();
|
||||
}
|
||||
reset(){
|
||||
this._logger.log('-- reset --');
|
||||
this.heroes.length = 0;
|
||||
this._logger.tick();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +1,23 @@
|
|||
// #docregion
|
||||
import {Directive, Input,
|
||||
OnInit, OnDestroy} from 'angular2/core';
|
||||
|
||||
import {Directive, OnInit, OnDestroy} from 'angular2/core';
|
||||
import {LoggerService} from './logger.service';
|
||||
|
||||
/***************************************/
|
||||
let nextId = 1;
|
||||
|
||||
// #docregion spy-directive
|
||||
// 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(private _logger:LoggerService) { }
|
||||
|
||||
constructor(logger:LoggerService){
|
||||
this._logger = logger;
|
||||
}
|
||||
ngOnInit() { this._logIt(`onInit`); }
|
||||
|
||||
ngOnInit() {
|
||||
this._logIt(`onInit`);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._logIt(`onDestroy`);
|
||||
}
|
||||
ngOnDestroy() { this._logIt(`onDestroy`); }
|
||||
|
||||
private _logIt(msg:string){
|
||||
this._logger.log(`Spy #${this._id } ${msg}`);
|
||||
this._logger.log(`Spy #${nextId++} ${msg}`);
|
||||
}
|
||||
}
|
||||
// #enddocregion spy-directive
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<title>Angular 2 Lifecycle Hooks</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="sample.css">
|
||||
|
||||
<!-- IE required polyfills, in this exact order -->
|
||||
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"description": "Lifecycle Hooks",
|
||||
"files":["!**/*.d.ts", "!**/*.js"],
|
||||
"tags": ["lifecycle", "hooks",
|
||||
"onInit", "onDestroy", "onChange",
|
||||
"ngOnInit", "ngOnDestroy", "ngOnChange"]
|
||||
"OnInit", "OnDestroy", "OnChange", "DoCheck",
|
||||
"AfterContentInit", "AfterContentChecked",
|
||||
"AfterViewInit", "AfterViewChecked"]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
.parent {
|
||||
color: #666;
|
||||
margin: 14px 0;
|
||||
padding: 8px;
|
||||
}
|
||||
input {
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
.comment {
|
||||
color: red;
|
||||
font-style: italic;
|
||||
}
|
|
@ -1,90 +1,575 @@
|
|||
include ../_util-fns
|
||||
|
||||
- var top="vertical-align:top"
|
||||
|
||||
: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**
|
||||
Angular offers **component 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)
|
||||
* [The lifecycle hooks](#hooks-overview)
|
||||
* [The hook-call sequence](#hook-sequence)
|
||||
* [Other Angular lifecycle hooks](#other-lifecycles)
|
||||
* [The lifecycle sample](#the-sample)
|
||||
* [All](#peek-a-boo)
|
||||
* [Spying OnInit and OnDestroy](#spy)
|
||||
* [OnChanges](#onchanges)
|
||||
* [DoCheck](#docheck)
|
||||
* [AfterViewInit and AfterViewChecked](#afterview)
|
||||
* [AfterContentInit and AfterContentChecked](#aftercontent)
|
||||
|
||||
Try the [Live Example](/resources/live-examples/lifecycle-hooks/ts/plnkr.html)
|
||||
|
||||
<!--
|
||||
https://github.com/angular/angular/blob/master/modules/angular2/src/core/linker/interfaces.ts
|
||||
-->
|
||||
|
||||
a(id="hooks-overview")
|
||||
.l-main-section
|
||||
:marked
|
||||
## The Lifecycle Hooks
|
||||
## Component 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.
|
||||
one or more of the *Lifecycle Hook* interfaces in the `angular2/core` library.
|
||||
|
||||
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 named `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.
|
||||
|
||||
We might implement it in a component class like this:
|
||||
+makeExample('lifecycle-hooks/ts/app/peek-a-boo.component.ts', 'ngOnInit', 'peek-a-boo.component.ts (excerpt)')(format='.')
|
||||
:marked
|
||||
## Peek-a-boo
|
||||
The `PeekABooComponent` demonstrates all of the hooks in the same component.
|
||||
No directive or component will implement all of them and some of the hooks only make sense for components.
|
||||
|
||||
Angular only calls a directive/component hook method *if it is defined*.
|
||||
|
||||
.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.
|
||||
### Interface optional?
|
||||
The interfaces are optional for JavaScript and Typescript developers from a purely technical perspective.
|
||||
The JavaScript language doesn't have interfaces.
|
||||
Angular can't see TypeScript interfaces at runtime because they disappear from the transpiled 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.
|
||||
|
||||
Nonetheless, we strongly recommend adding interfaces to TypeScript directive classes
|
||||
in order to benefit from strong typing and editor tooling.
|
||||
|
||||
Here are the component lifecycle hook methods:
|
||||
|
||||
### Directives and Components
|
||||
|
||||
table(width="100%")
|
||||
col(width="20%")
|
||||
col(width="80%")
|
||||
tr
|
||||
th Hook
|
||||
th Purpose
|
||||
tr(style=top)
|
||||
td ngOnInit
|
||||
td
|
||||
:marked
|
||||
Initialize the directive/component after Angular initializes the data-bound input properties.
|
||||
tr(style=top)
|
||||
td ngOnChanges
|
||||
td
|
||||
:marked
|
||||
Respond after Angular sets a data-bound input property.
|
||||
The method receives a `changes` object of current and previous values.
|
||||
tr(style=top)
|
||||
td ngDoCheck
|
||||
td
|
||||
:marked
|
||||
Detect and act upon changes that Angular can or won't
|
||||
detect on its own. Called every change detection run.
|
||||
tr(style=top)
|
||||
td ngOnDestroy
|
||||
td
|
||||
:marked
|
||||
Cleanup just before Angular destroys the directive/component.
|
||||
Unsubscribe observables and detach event handlers to avoid memory leaks.
|
||||
|
||||
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.
|
||||
### Components only
|
||||
|
||||
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).
|
||||
table(width="100%")
|
||||
col(width="20%")
|
||||
col(width="80%")
|
||||
tr
|
||||
th Hook
|
||||
th Purpose
|
||||
tr(style=top)
|
||||
td ngAfterContentInit
|
||||
td
|
||||
:marked
|
||||
After Angular projects external content into its view.
|
||||
tr(style=top)
|
||||
td ngAfterContentChecked
|
||||
td
|
||||
:marked
|
||||
After Angular checks the bindings of the external content that it projected into its view.
|
||||
tr(style=top)
|
||||
td ngAfterViewInit
|
||||
td
|
||||
:marked
|
||||
After Angular creates the component's view(s).
|
||||
tr(style=top)
|
||||
td ngAfterViewChecked
|
||||
td
|
||||
:marked
|
||||
After Angular checks the bindings of the component's view(s).
|
||||
:marked
|
||||
Angular does not call the hook methods in this order.
|
||||
|
||||
## Interface optional?
|
||||
The lifecycle interfaces are optional.
|
||||
We recommend adding them to benefit from TypeScript's strong typing and editor tooling.
|
||||
a(id="hook-sequence")
|
||||
.l-main-section
|
||||
:marked
|
||||
## Lifecycle sequence
|
||||
*After* Angular creates a component/directive by `new`-ing its constructor,
|
||||
it calls the lifecycle hook methods in the following sequence at specific moments:
|
||||
table(width="100%")
|
||||
col(width="20%")
|
||||
col(width="80%")
|
||||
tr
|
||||
th Hook
|
||||
th Timing
|
||||
tr(style=top)
|
||||
td ngOnChanges
|
||||
td
|
||||
:marked
|
||||
before `ngOnInit` and when a data-bound input property value changes.
|
||||
tr(style=top)
|
||||
td ngOnInit
|
||||
td
|
||||
:marked
|
||||
after the first `ngOnChanges`.
|
||||
tr(style=top)
|
||||
td ngDoCheck
|
||||
td
|
||||
:marked
|
||||
during every Angular change detection cycle.
|
||||
tr(style=top)
|
||||
td ngAfterContentInit
|
||||
td
|
||||
:marked
|
||||
after projecting content into the component.
|
||||
tr(style=top)
|
||||
td ngAfterContentChecked
|
||||
td
|
||||
:marked
|
||||
after every check of projected component content.
|
||||
tr(style=top)
|
||||
td ngAfterViewInit
|
||||
td
|
||||
:marked
|
||||
after initializing the component's views and child views.
|
||||
tr(style=top)
|
||||
td ngAfterViewChecked
|
||||
td
|
||||
:marked
|
||||
after every check of the component's views and child views.
|
||||
tr(style=top)
|
||||
td ngOnDestroy
|
||||
td
|
||||
:marked
|
||||
just before Angular destroys the directive/component.
|
||||
|
||||
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).
|
||||
a(id="other-lifecycles")
|
||||
.l-main-section
|
||||
:marked
|
||||
## Other lifecycle hooks
|
||||
|
||||
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.
|
||||
Other Angular sub-system may have their own lifecycle hooks apart from the component hooks we've listed.
|
||||
The router, for instance, also has it's own [router lifecycle hooks](router.html#router-lifecycle-hooks)
|
||||
that allow us to tap into specific moments in route navigation.
|
||||
|
||||
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.
|
||||
A parallel can be drawn between `ngOnInit` and `routerOnActivate`.
|
||||
Both are prefixed so as to avoid collision, and both run right when a component is 'booting' up.
|
||||
|
||||
3rd party libraries might implement their hooks as well in order to give us, the developers, more
|
||||
control over how these libraries are used.
|
||||
|
||||
a(id="the-sample")
|
||||
.l-main-section
|
||||
:marked
|
||||
## Lifecycle exercises
|
||||
|
||||
The [live example](/resources/live-examples/lifecycle-hooks/ts/plnkr.html)
|
||||
demonstrates the lifecycle hooks in action through a series of exercises
|
||||
presented as components under the control of the root `AppComponent`.
|
||||
|
||||
They follow a common pattern: a *parent* component serves as a test rig for
|
||||
a *child* component that illustrates one or more of the lifecycle hook methods.
|
||||
|
||||
Here's a brief description of each exercise:
|
||||
|
||||
table(width="100%")
|
||||
col(width="20%")
|
||||
col(width="80%")
|
||||
tr
|
||||
th Component
|
||||
th Description
|
||||
tr(style=top)
|
||||
td <a href="#peek-a-boo">Peek-a-boo</a>
|
||||
td
|
||||
:marked
|
||||
Demonstrates every lifecycle hook.
|
||||
Each hook method writes to the on-screen log.
|
||||
tr(style=top)
|
||||
td <a href="#spy">Spy</a>
|
||||
td
|
||||
:marked
|
||||
Directives have lifecycle hooks too.
|
||||
We create a `SpyDirective` that logs when the element it spies upon is
|
||||
created or destroyed using the `ngOnInit` and `ngOnDestroy` hooks.
|
||||
|
||||
We apply the `SpyDirective` to a `<div>` in an `ngFor` *hero* repeater
|
||||
managed by the parent `SpyComponent`.
|
||||
tr(style=top)
|
||||
td <a href="#onchanges">OnChanges</a>
|
||||
td
|
||||
:marked
|
||||
See how Angular calls the `ngOnChanges` hook with a `changes` object
|
||||
every time one of the component input properties changes.
|
||||
Shows how to interpret the `changes` object.
|
||||
tr(style=top)
|
||||
td <a href="#docheck">DoCheck</a>
|
||||
td
|
||||
:marked
|
||||
Implements a `ngDoCheck` method with custom change detection
|
||||
that works because the data behave in a particularly simple way.
|
||||
See how often Angular calls this hook and
|
||||
watch it post changes to a log.
|
||||
tr(style=top)
|
||||
td <a href="#afterview">AfterView</a>
|
||||
td
|
||||
:marked
|
||||
Shows what Angular means by a *view*.
|
||||
Demonstrates the `ngAfterViewInit` and `ngAfterViewChecked` hooks.
|
||||
tr(style=top)
|
||||
td <a href="#aftercontent">AfterContent</a>
|
||||
td
|
||||
:marked
|
||||
Shows how to project external content into a component and
|
||||
how to distinguish projected content from a component's view children.
|
||||
Demonstrates the `ngAfterContentInit` and `ngAfterContentChecked` hooks.
|
||||
tr(style=top)
|
||||
td Counter
|
||||
td
|
||||
:marked
|
||||
Demonstrates a combination of a component and a directive
|
||||
each with its own hooks.
|
||||
|
||||
In this example, a `CounterComponent` logs a change (via `ngOnChanges`)
|
||||
every time the parent component increments its input counter property.
|
||||
Meanwhile, we apply the `SpyDirective` from the previous example
|
||||
to the `CounterComponent` log and watch log entries be created and destroyed.
|
||||
|
||||
:marked
|
||||
We discuss the exercises in further detail over this chapter as we learn more about the lifecycle hooks.
|
||||
|
||||
a(id="peek-a-boo")
|
||||
.l-main-section
|
||||
:marked
|
||||
## Peek-a-boo: all hooks
|
||||
The `PeekABooComponent` demonstrates all of the hooks in one component.
|
||||
|
||||
In real life, we'd rarely if ever implement all of the interfaces like this.
|
||||
We do so in peek-a-boo in order to watch Angular call the hooks in the expected order.
|
||||
|
||||
In this snapshot, we clicked the *Create...* button and then the *Destroy...* button.
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/lifecycle-hooks/peek-a-boo.png" alt="Peek-a-boo")
|
||||
:marked
|
||||
The sequence of log messages follows the prescribed hook calling order:
|
||||
`OnChanges`, `OnInit`, `DoCheck` (3x), `AfterContentInit`, `AfterContentChecked` (3x),
|
||||
`AfterViewInit`, `AfterViewChecked` (3x), and `OnDestroy`.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The constructor isn't an Angular hook *per se*.
|
||||
We log in it to confirm that input properties (the `name` property in this case) have no assigned values at construction.
|
||||
:marked
|
||||
Had we clicked the *Update Hero* button, we'd have seen another `OnChanges` and two more triplets of
|
||||
`DoCheck, `AfterContentChecked` and `AfterViewChecked`.
|
||||
Clearly these three hooks fire a *lot* and we must keep the logic we put in these hooks
|
||||
as lean as possible!
|
||||
|
||||
Our next examples focus on hook details.
|
||||
|
||||
.a(id="spy")
|
||||
.l-main-section
|
||||
:marked
|
||||
## Spying *OnInit* and *OnDestroy*
|
||||
|
||||
We're going undercover for these two hooks. We want to know when an element is initialized or destroyed,
|
||||
but we don't want *it* to know we're watching.
|
||||
|
||||
This is the perfect infiltration job for a directive.
|
||||
Our heroes will never know it's there.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Kidding aside, we're emphasizing two key points:
|
||||
|
||||
1. Angular calls hook methods for *directives* as well as components.
|
||||
|
||||
2. A spy directive can gives us insight into a DOM object that we cannot change directly.
|
||||
Obviously we can't change the implementation of a native `div`.
|
||||
We can't modify a third party component either.
|
||||
But we can watch both with a directive.
|
||||
|
||||
|
||||
:marked
|
||||
Our sneaky spy directive is simple, consisting almost entirely of `ngOnInit` and `ngOnDestroy` hooks
|
||||
that log messages to the parent via an injected `LoggerService`.
|
||||
|
||||
+makeExample('lifecycle-hooks/ts/app/spy.directive.ts', 'spy-directive')(format=".")
|
||||
|
||||
:marked
|
||||
We can apply the spy to any native or component element and it'll be initialized and destroyed
|
||||
at the same time as that element.
|
||||
Here we attach it to the repeated hero `<div>`
|
||||
+makeExample('lifecycle-hooks/ts/app/spy.component.ts', 'template')(format=".")
|
||||
|
||||
:marked
|
||||
Each spy's birth and death marks the birth and death of the attached hero `<div>`
|
||||
with an entry in the *Hook Log* as we see here:
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/lifecycle-hooks/spy-directive.gif' alt="Spy Directive")
|
||||
|
||||
:marked
|
||||
Adding a hero results in a new hero `<div>`. The spy's `ngOnit` logs that event.
|
||||
We see a new entry for each hero.
|
||||
|
||||
The *Reset* button clears the `heroes` list.
|
||||
Angular removes all hero divs from the DOM and destroys their spy directives at the same time.
|
||||
The spy's `ngOnDestroy` method reports its last moments.
|
||||
|
||||
The `ngOnInit` and `ngOnDestroy` methods have more vital roles to play in real applications.
|
||||
Let's see why we need them.
|
||||
|
||||
### OnInit
|
||||
|
||||
We turn to `ngOnInit` for two main reasons:
|
||||
1. To perform complex initializations shortly after construction
|
||||
1. To set up the component after Angular sets the input properties
|
||||
|
||||
An `ngOnInit` often fetches data for the component as shown in the
|
||||
[Tutorial](../tutorial/toh-pt4.html#oninit) and [HTTP](server-communication.html#oninit) chapters.
|
||||
|
||||
We don't fetch data in a component constructor. Why?
|
||||
Because experienced developers agree that components should be cheap and safe to construct.
|
||||
We shouldn't worry that a new component will try to contact a remote server when
|
||||
created under test or before we decide to display it.
|
||||
Constructors should do no more than set the initial local variables to simple values.
|
||||
|
||||
When a component must start working _soon_ after creation,
|
||||
we can count on Angular to call the `ngOnit` method to jumpstart it.
|
||||
That's where the heavy initialization logic belongs.
|
||||
|
||||
Remember also that a directive's data-bound input properties are not set until _after construction_.
|
||||
That's a problem if we need to initialize the directive based on those properties.
|
||||
They'll have been set when our `ngOninit` runs.
|
||||
.l-sub-section
|
||||
:marked
|
||||
Our first opportunity to access those properties is the `ngOnChanges` method which
|
||||
Angular calls before `ngOnit`. But Angular calls `ngOnChanges` many times after that.
|
||||
It only calls `ngOnit` once.
|
||||
:marked
|
||||
### OnDestroy
|
||||
|
||||
Put cleanup logic in `ngOnDestroy`, the logic that *must* run before Angular destroys the directive.
|
||||
|
||||
This is the time to notify another part of the application that this component is going away.
|
||||
|
||||
This is the place to free resources that won't be garbage collected automatically.
|
||||
Unsubscribe from observables and DOM events. Stop interval timers.
|
||||
Unregister all callbacks that this directive registered with global or application services.
|
||||
We risk memory leaks if we neglect to do so.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## OnChanges
|
||||
|
||||
We monitor the `OnChanges` hook in this example.
|
||||
Angular calls its `ngOnChanges` method whenever it detects changes to ***input properties*** of the component (or directive).
|
||||
|
||||
Here is our implementation of the hook.
|
||||
+makeExample('lifecycle-hooks/ts/app/on-changes.component.ts', 'ng-on-changes', 'OnChangesComponent (ngOnChanges)')(format=".")
|
||||
:marked
|
||||
The `ngOnChanges` method takes an object that maps each changed property name to a
|
||||
[SimpleChange](../api/core/SimpleChange-class.html) object with the current and previous property values.
|
||||
We iterate over the changed properties and log them.
|
||||
|
||||
The input properties for our example `OnChangesComponent` are `hero` and `power`.
|
||||
+makeExample('lifecycle-hooks/ts/app/on-changes.component.ts', 'inputs')(format=".")
|
||||
:marked
|
||||
The parent binds to them like this:
|
||||
|
||||
+makeExample('lifecycle-hooks/ts/app/on-changes-parent.component.html', 'on-changes')
|
||||
:marked
|
||||
Here's the sample in action as we make changes.
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/lifecycle-hooks/on-changes-anim.gif' alt="OnChanges")
|
||||
|
||||
:marked
|
||||
We see log entries as the string value of the *power* property changes. But the `ngOnChanges` did not catch changes to `hero.name`
|
||||
That's surprising at first.
|
||||
|
||||
Angular only calls the hook when the value of the input property changes.
|
||||
The value of the `hero` property is the *reference to the hero object*.
|
||||
Angular doesn't care that the hero's own `name` property changed.
|
||||
The hero object *reference* didn't change so, from Angular's perspective, there is no change to report!
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## DoCheck
|
||||
|
||||
We can take over the change detection with the `DoCheck` hook when Angular doesn't
|
||||
catch an important change on its own.
|
||||
|
||||
The *DoCheck* sample extends the *OnChanges* sample with this implementation of `DoCheck`:
|
||||
+makeExample('lifecycle-hooks/ts/app/do-check.component.ts', 'ng-do-check', 'DoCheckComponent (ngDoCheck)')(format=".")
|
||||
:marked
|
||||
We manually check everything that we care about, capturing and comparing against previous values.
|
||||
We write a special message to the log when there are no substantive changes
|
||||
to the hero or the power so we can keep an eye on the method's performance characteristics.
|
||||
|
||||
The results are illuminating:
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/lifecycle-hooks/do-check-anim.gif' alt="DoCheck")
|
||||
:marked
|
||||
We now are able to detect when the hero's `name` has changed. But we must be careful.
|
||||
|
||||
The `ngDoCheck` hook is called with enormous frequency —
|
||||
after _every_ change detection cycle no matter where the change occurred.
|
||||
It's called over twenty time in this example before the user can do anything.
|
||||
|
||||
Most of these initial checks are triggered by Angular's first rendering of *unrelated data elsewhere on the page*.
|
||||
Mere mousing into another input box triggers a call.
|
||||
Relatively few calls reveal actual changes to pertinent data.
|
||||
Clearly our implementation must be very lightweight or the user experience may suffer.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
We see also that the `ngOnChanges` method is called in contradiction of the
|
||||
[incorrect API documentation](/docs/ts/latest/api/core/DoCheck-interface.html).
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## AfterView
|
||||
The *AfterView* sample explores the `AfterViewInit` and `AfterViewChecked` hooks that Angular calls
|
||||
*after* Angular creates a component's child views.
|
||||
|
||||
Here's a child view that displays a hero's name in an input box:
|
||||
+makeExample('lifecycle-hooks/ts/app/after-view.component.ts', 'child-view', 'ChildComponent')(format=".")
|
||||
:marked
|
||||
The `AfterViewComponent` displays this child view *within its template*:
|
||||
+makeExample('lifecycle-hooks/ts/app/after-view.component.ts', 'template', 'AfterViewComponent (template)')(format=".")
|
||||
:marked
|
||||
The following hooks take action based on changing values *within the child view*
|
||||
which we can only reach by querying for the child view via the property decorated with
|
||||
[@ViewChild](../api/core/ViewChild-var.html).
|
||||
|
||||
+makeExample('lifecycle-hooks/ts/app/after-view.component.ts', 'hooks', 'AfterViewComponent (class excerpts)')(format=".")
|
||||
.a(id="wait-a-tick")
|
||||
:marked
|
||||
### Abide by the unidirectional data flow rule
|
||||
The `_doSomething` method updates the screen when the hero name exceeds 10 characters.
|
||||
|
||||
+makeExample('lifecycle-hooks/ts/app/after-view.component.ts', 'do-something', 'AfterViewComponent (_doSomething)')(format=".")
|
||||
:marked
|
||||
Why does the `_doSomething` method waits a tick w/ `setTimeout` before updating `comment`?
|
||||
|
||||
We must adhere to Angular's unidirectional data flow rule which says that
|
||||
we may not update the view *after* it has been composed.
|
||||
Both hooks fire after the component's view has been composed.
|
||||
|
||||
Angular throws an error if we update component's data-bound `comment` property immediately (try it!).
|
||||
The `setTimeout` postpones the update one turn of the of the browser's JavaScript cycle ... and that's long enough.
|
||||
|
||||
Here's *AfterView* in action
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/lifecycle-hooks/after-view-anim.gif' alt="AfterView")
|
||||
:marked
|
||||
Notice that Angular frequently calls `AfterViewChecked`, often when there are no changes of interest.
|
||||
Write lean hook methods to avoid performance problems.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## AfterContent
|
||||
The *AfterContent* sample explores the `AfterContentInit` and `AfterContentChecked` hooks that Angular calls
|
||||
*after* Angular projects external content into the component.
|
||||
|
||||
### Content projection
|
||||
*Content projection* is a way to import HTML content from outside the component and insert that content
|
||||
into the component's template in a designated spot.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Angular 1 developers know this technique as *transclusion*.
|
||||
|
||||
:marked
|
||||
We'll illustrate with a variation on the [previous](#afterview) example
|
||||
whose behavior and output is almost the same.
|
||||
|
||||
This time, instead of including the child view within the template, we'll import it from
|
||||
the `AfterContentComponent`'s parent. Here's the parent's template.
|
||||
+makeExample('lifecycle-hooks/ts/app/after-content.component.ts', 'parent-template', 'AfterContentParentComponent (template excerpt)')(format=".")
|
||||
:marked
|
||||
Notice that the `<child-view>` tag is tucked between the `<after-content>` tags.
|
||||
We never put content between a component's element tags *unless we intend to project that content
|
||||
into the component*.
|
||||
|
||||
Now look at the component's template:
|
||||
+makeExample('lifecycle-hooks/ts/app/after-content.component.ts', 'template', 'AfterContentComponent (template)')(format=".")
|
||||
:marked
|
||||
The `<ngContent>` tags are the *placeholder* for the external content.
|
||||
They tell Angular where to insert that content.
|
||||
In this case, the projected content is the `<child-view>` from the parent.
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/lifecycle-hooks/projected-child-view.png' width="230" alt="Projected Content")
|
||||
:marked
|
||||
.l-sub-section
|
||||
:marked
|
||||
The tell-tale signs of *content projection* are (a) HTML between component element tags
|
||||
and (b) the presence of `<ngContent>` tags in the component's template.
|
||||
:marked
|
||||
### AfterContent hooks
|
||||
*AfterContent* hooks are similar to the *AfterView* hooks. The key difference is the kind of child component
|
||||
that we're looking for.
|
||||
|
||||
* The *AfterView* hooks concern `ViewChildren`, the child components whose element tags
|
||||
appear *within* the component's template.
|
||||
|
||||
* The *AfterContent* hooks concern `ContentChildren`, the child components that Angular
|
||||
projected into the component.
|
||||
|
||||
The following *AfterContent* hooks take action based on changing values in a *content child*
|
||||
which we can only reach by querying for it via the property decorated with
|
||||
[@ContentChild](../api/core/ContentChild-var.html).
|
||||
|
||||
+makeExample('lifecycle-hooks/ts/app/after-content.component.ts', 'hooks', 'AfterContentComponent (class excerpts)')(format=".")
|
||||
|
||||
:marked
|
||||
### No unidirectional flow worries
|
||||
|
||||
This component's `_doSomething` method update's the component's data-bound `comment` property immediately.
|
||||
There's no [need to wait](#wait-a-tick).
|
||||
|
||||
Recall that Angular calls both *AfterContent* hooks before calling either of the *AfterView* hooks.
|
||||
Angular completes composition of the projected content *before* finishing the composition of this component's view.
|
||||
We still have a window of opportunity to modify that view.
|
||||
|
|
|
@ -84,6 +84,9 @@ figure.image-display
|
|||
|
||||
Below the button is an optional error message.
|
||||
|
||||
a(id="oninit")
|
||||
a(id="HeroListComponent")
|
||||
:marked
|
||||
### The *HeroListComponent* class
|
||||
We [inject](dependency-injection.html) the `HeroService` into the constructor.
|
||||
That's the instance of the `HeroService` that we provided in the parent shell `TohComponent`.
|
||||
|
|
|
@ -228,6 +228,8 @@ code-example(format="." language="html").
|
|||
:marked
|
||||
We don't really need a dedicated method to wrap one line. We write it anyway:
|
||||
+makeExample('toh-4/ts/app/app.component.1.ts', 'getHeroes', 'app.component.ts (getHeroes)')(format=".")
|
||||
|
||||
a.(id="oninit")
|
||||
:marked
|
||||
### The *ngOnInit* Lifecycle Hook
|
||||
`AppComponent` should fetch and display heroes without a fuss.
|
||||
|
|
After Width: | Height: | Size: 216 KiB |
After Width: | Height: | Size: 190 KiB |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 83 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 89 KiB |