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');
|
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'));
|
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");
|
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);
|
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 () {
|
it('should support OnChanges hook', function () {
|
||||||
var onChangesViewEle = element.all(by.css('on-changes-parent my-hero div')).get(0);
|
var onChangesViewEle = element.all(by.css('on-changes div')).get(0);
|
||||||
var inputEles = element.all(by.css('on-changes-parent input'));
|
var inputEles = element.all(by.css('on-changes-parent input'));
|
||||||
var heroNameInputEle = inputEles.get(0);
|
var heroNameInputEle = inputEles.get(1);
|
||||||
var powerInputEle = inputEles.get(1);
|
var powerInputEle = inputEles.get(0);
|
||||||
var titleEle = onChangesViewEle.element(by.css('p'));
|
var titleEle = onChangesViewEle.element(by.css('p'));
|
||||||
expect(titleEle.getText()).toContain('Windstorm can sing');
|
|
||||||
var changeLogEles = onChangesViewEle.all(by.css('div'));
|
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 () {
|
// heroNameInputEle.sendKeys('-foo-').then(function () {
|
||||||
sendKeys(heroNameInputEle, '-foo-').then(function () {
|
sendKeys(heroNameInputEle, '-foo-').then(function () {
|
||||||
expect(titleEle.getText()).toContain('Windstorm-foo- can sing');
|
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.
|
// protractor bug with sendKeys means that line below does not work.
|
||||||
// return powerInputEle.sendKeys('-bar-');
|
// return powerInputEle.sendKeys('-bar-');
|
||||||
return sendKeys(powerInputEle, '-bar-');
|
return sendKeys(powerInputEle, '-bar-');
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
expect(titleEle.getText()).toContain('Windstorm-foo- can sing-bar-');
|
expect(titleEle.getText()).toContain('Windstorm-foo- can sing-bar-');
|
||||||
// 8 == 3 previously + length of '-bar-'
|
// 7 == 2 previously + length of '-bar-'
|
||||||
expect(changeLogEles.count()).toEqual(8, "should have 8 messages now");
|
expect(changeLogEles.count()).toEqual(7, "should have 7 messages now");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support after-view hooks', function () {
|
it('should support DoCheck hook', function () {
|
||||||
var inputEle = element(by.css('after-view-parent input'));
|
var doCheckViewEle = element.all(by.css('do-check div')).get(0);
|
||||||
var buttonEle = element(by.css('after-view-parent button'));
|
var inputEles = element.all(by.css('do-check-parent input'));
|
||||||
var logEles = element.all(by.css('after-view-parent h4 ~ div'));
|
var heroNameInputEle = inputEles.get(1);
|
||||||
var childViewTextEle = element(by.css('after-view-parent my-child .child'));
|
var powerInputEle = inputEles.get(0);
|
||||||
expect(childViewTextEle.getText()).toContain('Magneta is my hero');
|
var titleEle = doCheckViewEle.element(by.css('p'));
|
||||||
expect(logEles.count()).toBeGreaterThan(2);
|
var changeLogEles = doCheckViewEle.all(by.css('div'));
|
||||||
var logCount;
|
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) {
|
logEles.count().then(function(count) {
|
||||||
logCount = count;
|
logCount = count;
|
||||||
return sendKeys(inputEle, "-test-");
|
return sendKeys(childViewInputEle, "-test-");
|
||||||
}).then(function() {
|
}).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();
|
return logEles.count();
|
||||||
}).then(function(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;
|
logCount = count;
|
||||||
return buttonEle.click();
|
return buttonEle.click();
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
expect(childViewTextEle.isPresent()).toBe(false,"child view should no longer be part of the DOM");
|
expect(logEles.count()).toBeLessThan(logCount, "log should shrink after reset");
|
||||||
sendKeys(inputEle, "-foo-");
|
|
||||||
expect(logEles.count()).toEqual(logCount, "no additional log messages should have been added");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support after-content hooks', function () {
|
|
||||||
var inputEle = element(by.css('after-content-parent input'));
|
it('should support AfterContent hooks', function () {
|
||||||
var buttonEle = element(by.css('after-content-parent button'));
|
var parentEle = element(by.tagName('after-content-parent'));
|
||||||
var logEles = element.all(by.css('after-content-parent h4 ~ div'));
|
var buttonEle = parentEle.element(by.tagName('button')); // Reset
|
||||||
var childViewTextEle = element(by.css('after-content-parent my-child .child'));
|
var commentEle = parentEle.element(by.className('comment'));
|
||||||
expect(childViewTextEle.getText()).toContain('Magneta is my hero');
|
var logEles = parentEle.all(by.css('h4 ~ div'));
|
||||||
expect(logEles.count()).toBeGreaterThan(2);
|
var childViewInputEle = parentEle.element(by.css('my-child input'));
|
||||||
var logCount;
|
var logCount;
|
||||||
|
|
||||||
|
expect(childViewInputEle.getAttribute('value')).toContain('Magneta');
|
||||||
|
expect(commentEle.isPresent()).toBe(false, 'comment should not be in DOM');
|
||||||
|
|
||||||
logEles.count().then(function(count) {
|
logEles.count().then(function(count) {
|
||||||
logCount = count;
|
logCount = count;
|
||||||
return sendKeys(inputEle, "-test-");
|
return sendKeys(childViewInputEle, "-test-");
|
||||||
}).then(function() {
|
}).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();
|
return logEles.count();
|
||||||
}).then(function(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;
|
logCount = count;
|
||||||
return buttonEle.click();
|
return buttonEle.click();
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
expect(childViewTextEle.isPresent()).toBe(false,"child view should no longer be part of the DOM");
|
expect(logEles.count()).toBeLessThan(logCount, "log should shrink after reset");
|
||||||
sendKeys(inputEle, "-foo-");
|
|
||||||
expect(logEles.count()).toEqual(logCount, "no additional log messages should have been added");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support "spy" hooks', function () {
|
it('should support spy\'s OnInit & OnDestroy hooks', function () {
|
||||||
var inputEle = element(by.css('spy-parent input'));
|
var inputEle = element(by.css('spy-parent input'));
|
||||||
var addHeroButtonEle = element(by.cssContainingText('spy-parent button','Add Hero'));
|
var addHeroButtonEle = element(by.cssContainingText('spy-parent button','Add Hero'));
|
||||||
var resetHeroesButtonEle = element(by.cssContainingText('spy-parent button','Reset Heroes'));
|
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 updateCounterButtonEle = element(by.cssContainingText('counter-parent button','Update'));
|
||||||
var resetCounterButtonEle = element(by.cssContainingText('counter-parent button','Reset'));
|
var resetCounterButtonEle = element(by.cssContainingText('counter-parent button','Reset'));
|
||||||
var textEle = element(by.css('counter-parent my-counter > div'));
|
var textEle = element(by.css('counter-parent my-counter > div'));
|
||||||
|
|
|
@ -1,102 +1,119 @@
|
||||||
|
// #docplaster
|
||||||
// #docregion
|
// #docregion
|
||||||
import {
|
import {Component, AfterContentChecked, AfterContentInit, ContentChild} from 'angular2/core';
|
||||||
Component, Input, Output,
|
|
||||||
AfterContentChecked, AfterContentInit, ContentChild,
|
|
||||||
AfterViewInit, ViewChild
|
|
||||||
} from 'angular2/core';
|
|
||||||
|
|
||||||
import {ChildComponent} from './child.component';
|
|
||||||
import {LoggerService} from './logger.service';
|
import {LoggerService} from './logger.service';
|
||||||
|
|
||||||
|
//////////////////
|
||||||
|
@Component({
|
||||||
|
selector: 'my-child',
|
||||||
|
template: '<input [(ngModel)]="hero">'
|
||||||
|
})
|
||||||
|
export class ChildComponent {
|
||||||
|
hero = 'Magneta';
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'after-content',
|
selector: 'after-content',
|
||||||
template: `
|
// #docregion template
|
||||||
<div class="after-content">
|
template:`
|
||||||
<div>-- child content begins --</div>
|
<div>-- projected content begins --</div>
|
||||||
|
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
<div>-- projected content ends --</div>`
|
||||||
<div>-- child content ends --</div>
|
// #enddocregion template
|
||||||
</div>
|
+ `
|
||||||
`,
|
<p *ngIf="comment" class="comment">
|
||||||
styles: ['.after-content {background: LightCyan; padding: 8px;}'],
|
{{comment}}
|
||||||
|
</p>
|
||||||
|
`
|
||||||
})
|
})
|
||||||
export class AfterContentComponent
|
// #docregion hooks
|
||||||
implements AfterContentChecked, AfterContentInit, AfterViewInit {
|
export class AfterContentComponent implements AfterContentChecked, AfterContentInit {
|
||||||
|
private _prevHero = '';
|
||||||
private _logger:LoggerService;
|
|
||||||
|
|
||||||
constructor(logger:LoggerService){
|
|
||||||
this._logger = logger;
|
|
||||||
logger.log('AfterContent ctor: ' + this._getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query for a CONTENT child of type `ChildComponent`
|
// Query for a CONTENT child of type `ChildComponent`
|
||||||
@ContentChild(ChildComponent) contentChild: ChildComponent;
|
@ContentChild(ChildComponent) contentChild: ChildComponent;
|
||||||
|
|
||||||
// Query for a VIEW child of type`ChildComponent`
|
// #enddocregion hooks
|
||||||
// No such VIEW child exists!
|
constructor(private _logger:LoggerService){
|
||||||
// This component holds content but no view of that type.
|
this._logIt('AfterContent constructor');
|
||||||
@ViewChild(ChildComponent) viewChild: ChildComponent;
|
}
|
||||||
|
|
||||||
|
// #docregion hooks
|
||||||
///// Hooks
|
|
||||||
ngAfterContentInit() {
|
ngAfterContentInit() {
|
||||||
// contentChild is set after the content has been initialized
|
// viewChild is set after the view has been initialized
|
||||||
this._logger.log('AfterContentInit: ' + this._getMessage());
|
this._logIt('AfterContentInit');
|
||||||
|
this._doSomething();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
|
||||||
this._logger.log(`AfterViewInit: There is ${this.viewChild ? 'a' : 'no'} view child`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _prevHero:string;
|
|
||||||
ngAfterContentChecked() {
|
ngAfterContentChecked() {
|
||||||
// contentChild is updated after the content has been checked
|
// viewChild is updated after the view has been checked
|
||||||
// Called frequently; only report when the hero changes
|
if (this._prevHero === this.contentChild.hero) {
|
||||||
if (!this.contentChild || this._prevHero === this.contentChild.hero) {return;}
|
this._logIt('AfterContentChecked (no change)');
|
||||||
|
} else {
|
||||||
this._prevHero = this.contentChild.hero;
|
this._prevHero = this.contentChild.hero;
|
||||||
this._logger.log('AfterContentChecked: ' + this._getMessage());
|
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 {
|
private _logIt(method:string){
|
||||||
let cmp = this.contentChild;
|
let vc = this.contentChild;
|
||||||
return cmp ? `"${cmp.hero}" child content` : 'no child content';
|
let message = `${method}: ${vc ? vc.hero:'no'} child view`
|
||||||
|
this._logger.log(message);
|
||||||
}
|
}
|
||||||
|
// #docregion hooks
|
||||||
|
// ...
|
||||||
}
|
}
|
||||||
|
// #enddocregion hooks
|
||||||
|
|
||||||
/***************************************/
|
//////////////
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'after-content-parent',
|
selector: 'after-content-parent',
|
||||||
template: `
|
template: `
|
||||||
<div class="parent">
|
<div class="parent">
|
||||||
<h2>AfterContent</h2>
|
<h2>AfterContent</h2>
|
||||||
|
|
||||||
<after-content>
|
<div *ngIf="show">` +
|
||||||
<input [(ngModel)]="hero">
|
// #docregion parent-template
|
||||||
<button (click)="showChild = !showChild">Toggle child view</button>
|
`<after-content>
|
||||||
|
<my-child></my-child>
|
||||||
|
</after-content>`
|
||||||
|
// #enddocregion parent-template
|
||||||
|
+ `</div>
|
||||||
|
|
||||||
<my-child *ngIf="showChild" [hero]="hero"></my-child>
|
<h4>-- AfterContent Logs --</h4>
|
||||||
</after-content>
|
<p><button (click)="reset()">Reset</button></p>
|
||||||
|
<div *ngFor="#msg of logs">{{msg}}</div>
|
||||||
<h4>-- Lifecycle Hook Log --</h4>
|
|
||||||
<div *ngFor="#msg of hookLog">{{msg}}</div>
|
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: ['.parent {background: powderblue; padding: 8px; margin:100px 8px;}'],
|
styles: ['.parent {background: burlywood}'],
|
||||||
directives: [AfterContentComponent, ChildComponent],
|
providers:[LoggerService],
|
||||||
providers:[LoggerService]
|
directives: [AfterContentComponent, ChildComponent]
|
||||||
})
|
})
|
||||||
export class AfterContentParentComponent {
|
export class AfterContentParentComponent {
|
||||||
|
logs:string[];
|
||||||
hookLog:string[];
|
show = true;
|
||||||
hero = 'Magneta';
|
|
||||||
showChild = true;
|
|
||||||
|
|
||||||
constructor(logger:LoggerService){
|
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
|
// #docregion
|
||||||
import {
|
import {Component, AfterViewChecked, AfterViewInit, ViewChild} from 'angular2/core';
|
||||||
Component, Input, Output,
|
|
||||||
AfterContentInit, ContentChild,
|
|
||||||
AfterViewChecked, AfterViewInit, ViewChild
|
|
||||||
} from 'angular2/core';
|
|
||||||
|
|
||||||
import {ChildComponent} from './child.component';
|
|
||||||
import {LoggerService} from './logger.service';
|
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({
|
@Component({
|
||||||
selector: 'after-view-parent',
|
selector: 'after-view-parent',
|
||||||
template: `
|
template: `
|
||||||
<div class="parent">
|
<div class="parent">
|
||||||
<h2>AfterView</h2>
|
<h2>AfterView</h2>
|
||||||
|
|
||||||
<div>
|
<after-view *ngIf="show"></after-view>
|
||||||
<input [(ngModel)]="hero">
|
|
||||||
<button (click)="showChild = !showChild">Toggle child view</button>
|
|
||||||
|
|
||||||
<my-child *ngIf="showChild" [hero]="hero"></my-child>
|
<h4>-- AfterView Logs --</h4>
|
||||||
</div>
|
<p><button (click)="reset()">Reset</button></p>
|
||||||
|
<div *ngFor="#msg of logs">{{msg}}</div>
|
||||||
<h4>-- Lifecycle Hook Log --</h4>
|
|
||||||
<div *ngFor="#msg of hookLog">{{msg}}</div>
|
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: ['.parent {background: burlywood; padding: 8px; margin:100px 8px;}'],
|
styles: ['.parent {background: burlywood}'],
|
||||||
directives: [ChildComponent],
|
providers:[LoggerService],
|
||||||
providers:[LoggerService]
|
directives: [AfterViewComponent]
|
||||||
})
|
})
|
||||||
export class AfterViewParentComponent
|
export class AfterViewParentComponent {
|
||||||
implements AfterContentInit, AfterViewChecked, AfterViewInit {
|
logs:string[];
|
||||||
|
show = true;
|
||||||
private _logger:LoggerService;
|
|
||||||
|
|
||||||
constructor(logger:LoggerService){
|
constructor(logger:LoggerService){
|
||||||
this._logger = logger;
|
this.logs = logger.logs;
|
||||||
this.hookLog = logger.logs;
|
|
||||||
logger.log('AfterView ctor: ' + this._getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hookLog:string[];
|
reset() {
|
||||||
hero = 'Magneta';
|
this.logs.length=0;
|
||||||
showChild = true;
|
// quickly remove and reload AfterViewComponent which recreates it
|
||||||
|
this.show = false;
|
||||||
// Query for a CONTENT child of type `ChildComponent`
|
setTimeout(() => this.show = true, 0)
|
||||||
// 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';
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {AfterContentParentComponent} from './after-content.component';
|
||||||
import {AfterViewParentComponent} from './after-view.component';
|
import {AfterViewParentComponent} from './after-view.component';
|
||||||
import {CounterParentComponent} from './counter.component';
|
import {CounterParentComponent} from './counter.component';
|
||||||
|
import {DoCheckParentComponent} from './do-check.component';
|
||||||
import {OnChangesParentComponent} from './on-changes.component';
|
import {OnChangesParentComponent} from './on-changes.component';
|
||||||
import {PeekABooParentComponent} from './peek-a-boo-parent.component';
|
import {PeekABooParentComponent} from './peek-a-boo-parent.component';
|
||||||
import {SpyParentComponent} from './spy.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({
|
@Component({
|
||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
template: `
|
templateUrl: 'app/app.component.html',
|
||||||
<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: [
|
directives: [
|
||||||
AfterContentParentComponent,
|
AfterContentParentComponent,
|
||||||
AfterViewParentComponent,
|
AfterViewParentComponent,
|
||||||
|
CounterParentComponent,
|
||||||
|
DoCheckParentComponent,
|
||||||
OnChangesParentComponent,
|
OnChangesParentComponent,
|
||||||
PeekABooParentComponent,
|
PeekABooParentComponent,
|
||||||
SpyParentComponent,
|
SpyParentComponent,
|
||||||
CounterParentComponent
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
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 *ngFor="#msg of spyLog">{{msg}}</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: ['.parent {background: gold; padding: 10px; margin:100px 8px;}'],
|
styles: ['.parent {background: gold;}'],
|
||||||
directives: [MyCounter],
|
directives: [MyCounter],
|
||||||
providers: [LoggerService]
|
providers: [LoggerService]
|
||||||
})
|
})
|
||||||
|
@ -76,11 +76,13 @@ export class CounterParentComponent {
|
||||||
|
|
||||||
updateCounter() {
|
updateCounter() {
|
||||||
this.value += 1;
|
this.value += 1;
|
||||||
|
this._logger.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(){
|
reset(){
|
||||||
this._logger.log('-- reset --');
|
this._logger.log('-- reset --');
|
||||||
this.value=0;
|
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()
|
@Injectable()
|
||||||
export class LoggerService {
|
export class LoggerService {
|
||||||
logs:string[] = [];
|
logs:string[] = [];
|
||||||
|
prevMsg = '';
|
||||||
|
prevMsgCount = 1;
|
||||||
|
|
||||||
log(msg:string, noTick:boolean = false) {
|
log(msg:string) {
|
||||||
if (!noTick) { this.tick(); }
|
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);
|
this.logs.push(msg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clear() {this.logs.length = 0;}
|
clear() {this.logs.length = 0;}
|
||||||
|
|
||||||
|
// schedules a view refresh to ensure display catches up
|
||||||
tick() {
|
tick() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// console.log('tick')
|
// 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
|
// #docregion
|
||||||
import {
|
import {
|
||||||
Component, Input, Output,
|
Component, Input, ViewChild,
|
||||||
OnChanges, SimpleChange,
|
OnChanges, SimpleChange
|
||||||
} from 'angular2/core';
|
} from 'angular2/core';
|
||||||
|
|
||||||
|
|
||||||
export class Hero {
|
class Hero {
|
||||||
constructor(public name:string){}
|
constructor(public name:string){}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-hero',
|
selector: 'on-changes',
|
||||||
template: `
|
template: `
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<p>{{hero.name}} can {{power}}</p>
|
<p>{{hero.name}} can {{power}}</p>
|
||||||
|
@ -24,61 +24,51 @@ export class Hero {
|
||||||
'p {background: Yellow; padding: 8px; margin-top: 8px}'
|
'p {background: Yellow; padding: 8px; margin-top: 8px}'
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class MyHeroComponent implements OnChanges {
|
export class OnChangesComponent implements OnChanges {
|
||||||
|
// #docregion inputs
|
||||||
@Input() hero: Hero;
|
@Input() hero: Hero;
|
||||||
@Input() power: string;
|
@Input() power: string;
|
||||||
@Input() reset: {};
|
// #enddocregion inputs
|
||||||
|
|
||||||
changeLog:string[] = [];
|
changeLog:string[] = [];
|
||||||
|
|
||||||
|
// #docregion ng-on-changes
|
||||||
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
|
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) {
|
for (let propName in changes) {
|
||||||
let prop = changes[propName];
|
let prop = changes[propName];
|
||||||
let cur = JSON.stringify(prop.currentValue)
|
let cur = JSON.stringify(prop.currentValue)
|
||||||
let prev = JSON.stringify(prop.previousValue); // first time is {}; after is integer
|
let prev = JSON.stringify(prop.previousValue);
|
||||||
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
|
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// #enddocregion ng-on-changes
|
||||||
|
|
||||||
|
reset() { this.changeLog.length = 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************/
|
/***************************************/
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'on-changes-parent',
|
selector: 'on-changes-parent',
|
||||||
template: `
|
templateUrl:'app/on-changes-parent.component.html',
|
||||||
<div class="parent">
|
styles: ['.parent {background: Lavender;}'],
|
||||||
<h2>OnChanges</h2>
|
directives: [OnChangesComponent]
|
||||||
|
|
||||||
<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 {
|
export class OnChangesParentComponent {
|
||||||
hero:Hero;
|
hero:Hero;
|
||||||
power:string;
|
power:string;
|
||||||
resetTrigger = false;
|
title = 'OnChanges';
|
||||||
|
@ViewChild(OnChangesComponent) childView:OnChangesComponent;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(){
|
reset(){
|
||||||
// new Hero object every time; triggers onChange
|
// new Hero object every time; triggers onChanges
|
||||||
this.hero = new Hero('Windstorm');
|
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';
|
this.power = 'sing';
|
||||||
// always triggers onChange ... which is interpreted as a reset
|
this.childView && this.childView.reset();
|
||||||
this.resetTrigger = !this.resetTrigger;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {LoggerService} from './logger.service';
|
||||||
<div *ngFor="#msg of hookLog">{{msg}}</div>
|
<div *ngFor="#msg of hookLog">{{msg}}</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: ['.parent {background: moccasin; padding: 10px; margin:100px 8px}'],
|
styles: ['.parent {background: moccasin}'],
|
||||||
directives: [PeekABooComponent],
|
directives: [PeekABooComponent],
|
||||||
providers: [LoggerService]
|
providers: [LoggerService]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,21 +1,31 @@
|
||||||
// #docregion
|
|
||||||
// #docregion lc-imports
|
|
||||||
import {
|
import {
|
||||||
OnChanges, SimpleChange,
|
OnChanges, SimpleChange,
|
||||||
OnInit,
|
OnInit,
|
||||||
// DoCheck, // not demonstrated
|
DoCheck,
|
||||||
AfterContentInit,
|
AfterContentInit,
|
||||||
AfterContentChecked,
|
AfterContentChecked,
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
AfterViewChecked,
|
AfterViewChecked,
|
||||||
OnDestroy
|
OnDestroy
|
||||||
} from 'angular2/core';
|
} from 'angular2/core';
|
||||||
// #docregion lc-imports
|
|
||||||
import {Component, Input, Output} from 'angular2/core';
|
import {Component, Input, Output} from 'angular2/core';
|
||||||
import {LoggerService} from './logger.service';
|
import {LoggerService} from './logger.service';
|
||||||
|
|
||||||
let nextId = 1;
|
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({
|
@Component({
|
||||||
selector: 'peek-a-boo',
|
selector: 'peek-a-boo',
|
||||||
template: '<p>Now you see my hero, {{name}}</p>',
|
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
|
// Don't HAVE to mention the Lifecycle Hook interfaces
|
||||||
// unless we want typing and tool support.
|
// unless we want typing and tool support.
|
||||||
export class PeekABooComponent
|
export class PeekABooComponent extends PeekABoo implements
|
||||||
implements OnChanges, OnInit,AfterContentInit,AfterContentChecked,
|
OnChanges, OnInit, DoCheck,
|
||||||
AfterViewInit, AfterViewChecked, OnDestroy {
|
AfterContentInit,AfterContentChecked,
|
||||||
|
AfterViewInit, AfterViewChecked,
|
||||||
|
OnDestroy {
|
||||||
@Input() name:string;
|
@Input() name:string;
|
||||||
|
|
||||||
private _afterContentCheckedCounter = 1;
|
|
||||||
private _afterViewCheckedCounter = 1;
|
|
||||||
private _id = nextId++;
|
|
||||||
private _logger:LoggerService;
|
|
||||||
private _onChangesCounter = 1;
|
|
||||||
private _verb = 'initialized';
|
private _verb = 'initialized';
|
||||||
|
|
||||||
constructor(logger:LoggerService){
|
constructor(logger:LoggerService) {
|
||||||
this._logger = logger;
|
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}){
|
ngOnChanges(changes: {[propertyName: string]: SimpleChange}){
|
||||||
let changesMsgs:string[] = []
|
let changesMsgs:string[] = []
|
||||||
for (let propName in changes) {
|
for (let propName in changes) {
|
||||||
|
@ -50,49 +60,25 @@ export class PeekABooComponent
|
||||||
changesMsgs.push(propName + ' ' + this._verb);
|
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
|
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!
|
// Beware! Called frequently!
|
||||||
ngAfterContentChecked(){
|
// Called in every change detection cycle anywhere on the page
|
||||||
let counter = this._afterContentCheckedCounter++;
|
ngDoCheck(){ this._logIt(`DoCheck`); }
|
||||||
let msg = `afterContentChecked (${counter})`;
|
|
||||||
this._logIt(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(){
|
ngAfterContentInit(){ this._logIt(`AfterContentInit`); }
|
||||||
this._logIt(`afterViewInit`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called after every change detection check
|
|
||||||
// of the component (directive) VIEW
|
|
||||||
// Beware! Called frequently!
|
// Beware! Called frequently!
|
||||||
|
// Called in every change detection cycle anywhere on the page
|
||||||
|
ngAfterContentChecked(){ this._logIt(`AfterContentChecked`); }
|
||||||
|
|
||||||
ngAfterViewChecked(){
|
ngAfterViewInit(){ this._logIt(`AfterViewInit`); }
|
||||||
let counter = this._afterViewCheckedCounter++;
|
|
||||||
let msg = `afterViewChecked (${counter})`;
|
|
||||||
this._logIt(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
// Beware! Called frequently!
|
||||||
this._logIt(`onDestroy`);
|
// Called in every change detection cycle anywhere on the page
|
||||||
}
|
ngAfterViewChecked(){ this._logIt(`AfterViewChecked`); }
|
||||||
|
|
||||||
private _logIt(msg:string){
|
ngOnDestroy() { this._logIt(`OnDestroy`); }
|
||||||
// Don't tick or else
|
|
||||||
// the AfterContentChecked and AfterViewChecked recurse.
|
|
||||||
// Let parent call tick()
|
|
||||||
this._logger.log(`#${this._id } ${msg}`, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -8,22 +8,24 @@ import {Spy} from './spy.directive';
|
||||||
template: `
|
template: `
|
||||||
<div class="parent">
|
<div class="parent">
|
||||||
<h2>Spy Directive</h2>
|
<h2>Spy Directive</h2>
|
||||||
|
<p>
|
||||||
<input [(ngModel)]="newName" (keyup.enter)="addHero()">
|
<input [(ngModel)]="newName"
|
||||||
|
(keyup.enter)="addHero()"
|
||||||
|
placeholder="Hero name">
|
||||||
<button (click)="addHero()">Add Hero</button>
|
<button (click)="addHero()">Add Hero</button>
|
||||||
<button (click)="reset()">Reset Heroes</button>
|
<button (click)="reset()">Reset Heroes</button>
|
||||||
|
</p>` +
|
||||||
<p></p>
|
// #docregion template
|
||||||
<div *ngFor="#hero of heroes" my-spy class="heroes">
|
`<div *ngFor="#hero of heroes" my-spy class="heroes">
|
||||||
{{hero}}
|
{{hero}}
|
||||||
</div>
|
</div>`
|
||||||
|
// #enddocregion template
|
||||||
<h4>-- Spy Lifecycle Hook Log --</h4>
|
+ `<h4>-- Spy Lifecycle Hook Log --</h4>
|
||||||
<div *ngFor="#msg of spyLog">{{msg}}</div>
|
<div *ngFor="#msg of spyLog">{{msg}}</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
'.parent {background: khaki; padding: 10px; margin:100px 8px}',
|
'.parent {background: khaki;}',
|
||||||
'.heroes {background: LightYellow; padding: 0 8px}'
|
'.heroes {background: LightYellow; padding: 0 8px}'
|
||||||
],
|
],
|
||||||
directives: [Spy],
|
directives: [Spy],
|
||||||
|
@ -34,22 +36,24 @@ export class SpyParentComponent {
|
||||||
heroes:string[] = ['Windstorm', 'Magneta'];
|
heroes:string[] = ['Windstorm', 'Magneta'];
|
||||||
spyLog:string[];
|
spyLog:string[];
|
||||||
|
|
||||||
private _logger:LoggerService;
|
constructor(private _logger:LoggerService){
|
||||||
|
this.spyLog = _logger.logs;
|
||||||
constructor(logger:LoggerService){
|
|
||||||
this._logger = logger;
|
|
||||||
this.spyLog = logger.logs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addHero() {
|
addHero() {
|
||||||
if (this.newName.trim()) {
|
if (this.newName.trim()) {
|
||||||
this.heroes.push(this.newName.trim());
|
this.heroes.push(this.newName.trim());
|
||||||
this.newName = '';
|
this.newName = '';
|
||||||
|
this._logger.tick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
removeHero(hero:string) {
|
||||||
|
this.heroes.splice(this.heroes.indexOf(hero), 1);
|
||||||
|
this._logger.tick();
|
||||||
|
}
|
||||||
reset(){
|
reset(){
|
||||||
this._logger.log('-- reset --');
|
this._logger.log('-- reset --');
|
||||||
this.heroes.length = 0;
|
this.heroes.length = 0;
|
||||||
|
this._logger.tick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,23 @@
|
||||||
// #docregion
|
// #docregion
|
||||||
import {Directive, Input,
|
import {Directive, OnInit, OnDestroy} from 'angular2/core';
|
||||||
OnInit, OnDestroy} from 'angular2/core';
|
|
||||||
|
|
||||||
import {LoggerService} from './logger.service';
|
import {LoggerService} from './logger.service';
|
||||||
|
|
||||||
/***************************************/
|
|
||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
|
|
||||||
|
// #docregion spy-directive
|
||||||
// Spy on any element to which it is applied.
|
// Spy on any element to which it is applied.
|
||||||
// Usage: <div my-spy>...</div>
|
// Usage: <div my-spy>...</div>
|
||||||
@Directive({selector: '[my-spy]'})
|
@Directive({selector: '[my-spy]'})
|
||||||
export class Spy implements OnInit, OnDestroy {
|
export class Spy implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private _id = nextId++;
|
constructor(private _logger:LoggerService) { }
|
||||||
private _logger:LoggerService;
|
|
||||||
|
|
||||||
constructor(logger:LoggerService){
|
ngOnInit() { this._logIt(`onInit`); }
|
||||||
this._logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnDestroy() { this._logIt(`onDestroy`); }
|
||||||
this._logIt(`onInit`);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this._logIt(`onDestroy`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _logIt(msg:string){
|
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>
|
<title>Angular 2 Lifecycle Hooks</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link rel="stylesheet" href="sample.css">
|
||||||
|
|
||||||
<!-- IE required polyfills, in this exact order -->
|
<!-- IE required polyfills, in this exact order -->
|
||||||
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
|
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"description": "Lifecycle Hooks",
|
"description": "Lifecycle Hooks",
|
||||||
"files":["!**/*.d.ts", "!**/*.js"],
|
"files":["!**/*.d.ts", "!**/*.js"],
|
||||||
"tags": ["lifecycle", "hooks",
|
"tags": ["lifecycle", "hooks",
|
||||||
"onInit", "onDestroy", "onChange",
|
"OnInit", "OnDestroy", "OnChange", "DoCheck",
|
||||||
"ngOnInit", "ngOnDestroy", "ngOnChange"]
|
"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
|
include ../_util-fns
|
||||||
|
|
||||||
|
- var top="vertical-align:top"
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
# Component Lifecycle
|
# Component Lifecycle
|
||||||
A Component has a lifecycle managed by Angular itself. Angular creates it, renders it, creates and renders its children,
|
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.
|
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.
|
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.
|
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
|
https://github.com/angular/angular/blob/master/modules/angular2/src/core/linker/interfaces.ts
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
a(id="hooks-overview")
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## The Lifecycle Hooks
|
## Component lifecycle Hooks
|
||||||
Directive and component instances have a lifecycle
|
Directive and component instances have a lifecycle
|
||||||
as Angular creates, updates, and destroys them.
|
as Angular creates, updates, and destroys them.
|
||||||
|
|
||||||
Developers can tap into key moments in that lifecycle by implementing
|
Developers can tap into key moments in that lifecycle by implementing
|
||||||
one or more of the "Lifecycle Hook" interfaces, all of them available
|
one or more of the *Lifecycle Hook* interfaces in the `angular2/core` library.
|
||||||
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`.
|
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`.
|
For example, the `OnInit` interface has a hook method named `ngOnInit`.
|
||||||
|
We might implement it in a component class like this:
|
||||||
Angular calls these hook methods in the following order:
|
+makeExample('lifecycle-hooks/ts/app/peek-a-boo.component.ts', 'ngOnInit', 'peek-a-boo.component.ts (excerpt)')(format='.')
|
||||||
* `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
|
:marked
|
||||||
## Peek-a-boo
|
No directive or component will implement all of them and some of the hooks only make sense for components.
|
||||||
The `PeekABooComponent` demonstrates all of the hooks in the same component.
|
|
||||||
|
Angular only calls a directive/component hook method *if it is defined*.
|
||||||
|
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
Except for `DoCheck`. If our component superseded regular Angular change detection
|
### Interface optional?
|
||||||
with its own change detection processing
|
The interfaces are optional for JavaScript and Typescript developers from a purely technical perspective.
|
||||||
we would also add a `ngDoCheck` method. We would **not** implement `ngOnChanges`.
|
The JavaScript language doesn't have interfaces.
|
||||||
We write either `ngOnChanges` or `ngDoCheck`, not both.
|
Angular can't see TypeScript interfaces at runtime because they disappear from the transpiled JavaScript.
|
||||||
|
|
||||||
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.
|
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.
|
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
|
Angular instead inspects our directive and component classes and calls the hook methods *if they are defined*.
|
||||||
and calls the hook methods *if they are defined*.
|
|
||||||
Angular will find and call methods like `ngOnInit()`, with or without the interfaces.
|
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.
|
||||||
|
|
||||||
|
:marked
|
||||||
|
### Components only
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
a(id="other-lifecycles")
|
||||||
|
.l-main-section
|
||||||
|
:marked
|
||||||
|
## Other lifecycle hooks
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
Below the button is an optional error message.
|
||||||
|
|
||||||
|
a(id="oninit")
|
||||||
|
a(id="HeroListComponent")
|
||||||
|
:marked
|
||||||
### The *HeroListComponent* class
|
### The *HeroListComponent* class
|
||||||
We [inject](dependency-injection.html) the `HeroService` into the constructor.
|
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`.
|
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
|
:marked
|
||||||
We don't really need a dedicated method to wrap one line. We write it anyway:
|
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=".")
|
+makeExample('toh-4/ts/app/app.component.1.ts', 'getHeroes', 'app.component.ts (getHeroes)')(format=".")
|
||||||
|
|
||||||
|
a.(id="oninit")
|
||||||
:marked
|
:marked
|
||||||
### The *ngOnInit* Lifecycle Hook
|
### The *ngOnInit* Lifecycle Hook
|
||||||
`AppComponent` should fetch and display heroes without a fuss.
|
`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 |