From d299ce4bcf20cb7da9378c8516d2e83a763e4864 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Wed, 14 Sep 2016 09:42:41 -0700 Subject: [PATCH] docs(lifecycle): update docs for lifecycle hooks --- .../core/src/metadata/lifecycle_hooks.ts | 426 +++--------------- .../core/ts/metadata/lifecycle_hooks_spec.ts | 150 ++++++ 2 files changed, 201 insertions(+), 375 deletions(-) create mode 100644 modules/@angular/examples/core/ts/metadata/lifecycle_hooks_spec.ts diff --git a/modules/@angular/core/src/metadata/lifecycle_hooks.ts b/modules/@angular/core/src/metadata/lifecycle_hooks.ts index 27f35983b7..594bcc63fe 100644 --- a/modules/@angular/core/src/metadata/lifecycle_hooks.ts +++ b/modules/@angular/core/src/metadata/lifecycle_hooks.ts @@ -36,447 +36,123 @@ export var LIFECYCLE_HOOKS_VALUES = [ ]; /** - * Lifecycle hooks are guaranteed to be called in the following order: - * - `OnChanges` (if any bindings have changed), - * - `OnInit` (after the first check only), - * - `DoCheck`, - * - `AfterContentInit`, - * - `AfterContentChecked`, - * - `AfterViewInit`, - * - `AfterViewChecked`, - * - `OnDestroy` (at the very end before destruction) - */ - -/** - * Implement this interface to get notified when any data-bound property of your directive changes. + * @whatItDoes Lifecycle hook that is called when any data-bound property of a directive changes. + * @howToUse + * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='OnChanges'} * + * @description * `ngOnChanges` is called right after the data-bound properties have been checked and before view * and content children are checked if at least one of them has changed. + * The `changes` parameter contains the changed properties. * - * The `changes` parameter contains an entry for each of the changed data-bound property. The key is - * the property name and the value is an instance of {@link SimpleChange}. + * See {@linkDocs guide/lifecycle-hooks#onchanges "Lifecycle Hooks Guide"}. * - * ### Example ([live example](http://plnkr.co/edit/AHrB6opLqHDBPkt4KpdT?p=preview)): - * - * ```typescript - * @Component({ - * selector: 'my-cmp', - * template: `

myProp = {{myProp}}

` - * }) - * class MyComponent implements OnChanges { - * @Input() myProp: any; - * - * ngOnChanges(changes: SimpleChanges) { - * console.log('ngOnChanges - myProp = ' + changes['myProp'].currentValue); - * } - * } - * - * @Component({ - * selector: 'app', - * template: ` - * - * `, - * directives: [MyComponent] - * }) - * export class App { - * value = 0; - * } - * ``` * @stable */ export abstract class OnChanges { abstract ngOnChanges(changes: SimpleChanges): void; } /** - * Implement this interface to execute custom initialization logic after your directive's - * data-bound properties have been initialized. + * @whatItDoes Lifecycle hook that is called after data-bound properties of a directive are + * initialized. + * @howToUse + * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='OnInit'} * + * @description * `ngOnInit` is called right after the directive's data-bound properties have been checked for the * first time, and before any of its children have been checked. It is invoked only once when the * directive is instantiated. * - * ### Example ([live example](http://plnkr.co/edit/1MBypRryXd64v4pV03Yn?p=preview)) + * See {@linkDocs guide/lifecycle-hooks "Lifecycle Hooks Guide"}. * - * ```typescript - * @Component({ - * selector: 'my-cmp', - * template: `

my-component

` - * }) - * class MyComponent implements OnInit, OnDestroy { - * ngOnInit() { - * console.log('ngOnInit'); - * } - * - * ngOnDestroy() { - * console.log('ngOnDestroy'); - * } - * } - * - * @Component({ - * selector: 'app', - * template: ` - * - * `, - * directives: [MyComponent, NgIf] - * }) - * export class App { - * hasChild = true; - * } - * ``` * @stable */ export abstract class OnInit { abstract ngOnInit(): void; } /** - * Implement this interface to supplement the default change detection algorithm in your directive. + * @whatItDoes Lifecycle hook that is called when Angular dirty checks a directive. + * @howToUse + * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='DoCheck'} * + * @description * `ngDoCheck` gets called to check the changes in the directives in addition to the default - * algorithm. - * - * The default change detection algorithm looks for differences by comparing bound-property values - * by reference across change detection runs. + * algorithm. The default change detection algorithm looks for differences by comparing + * bound-property values by reference across change detection runs. * * Note that a directive typically should not use both `DoCheck` and {@link OnChanges} to respond to - * changes on the same input. `ngOnChanges` will continue to be called when the default change - * detector - * detects changes, so it is usually unnecessary to respond to changes on the same input in both - * hooks. - * Reaction to the changes have to be handled from within the `ngDoCheck` callback. + * changes on the same input, as `ngOnChanges` will continue to be called when the default change + * detector detects changes. * - * You can use {@link KeyValueDiffers} and {@link IterableDiffers} to help add your custom check - * mechanisms. + * See {@link KeyValueDiffers} and {@link IterableDiffers} for implementing custom dirty checking + * for collections. * - * ### Example ([live demo](http://plnkr.co/edit/QpnIlF0CR2i5bcYbHEUJ?p=preview)) + * See {@linkDocs guide/lifecycle-hooks#docheck "Lifecycle Hooks Guide"}. * - * In the following example `ngDoCheck` uses an {@link IterableDiffers} to detect the updates to the - * array `list`: - * - * ```typescript - * @Component({ - * selector: 'custom-check', - * template: ` - *

Changes:

- * `, - * directives: [NgFor] - * }) - * class CustomCheckComponent implements DoCheck { - * @Input() list: any[]; - * differ: any; - * logs = []; - * - * constructor(differs: IterableDiffers) { - * this.differ = differs.find([]).create(null); - * } - * - * ngDoCheck() { - * var changes = this.differ.diff(this.list); - * - * if (changes) { - * changes.forEachAddedItem(r => this.logs.push('added ' + r.item)); - * changes.forEachRemovedItem(r => this.logs.push('removed ' + r.item)) - * } - * } - * } - * - * @Component({ - * selector: 'app', - * template: ` - * - * - * `, - * directives: [CustomCheckComponent] - * }) - * export class App { - * list = []; - * } - * ``` * @stable */ export abstract class DoCheck { abstract ngDoCheck(): void; } /** - * Implement this interface to get notified when your directive is destroyed. + * @whatItDoes Lifecycle hook that is called when a directive or pipe is destroyed. + * @howToUse + * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='OnDestroy'} * + * @description * `ngOnDestroy` callback is typically used for any custom cleanup that needs to occur when the - * instance is destroyed + * instance is destroyed. * - * ### Example ([live example](http://plnkr.co/edit/1MBypRryXd64v4pV03Yn?p=preview)) - * - * ```typesript - * @Component({ - * selector: 'my-cmp', - * template: `

my-component

` - * }) - * class MyComponent implements OnInit, OnDestroy { - * ngOnInit() { - * console.log('ngOnInit'); - * } - * - * ngOnDestroy() { - * console.log('ngOnDestroy'); - * } - * } - * - * @Component({ - * selector: 'app', - * template: ` - * - * `, - * directives: [MyComponent, NgIf] - * }) - * export class App { - * hasChild = true; - * } - * ``` - * - * - * To create a stateful Pipe, you should implement this interface and set the `pure` - * parameter to `false` in the {@link Pipe}. - * - * A stateful pipe may produce different output, given the same input. It is - * likely that a stateful pipe may contain state that should be cleaned up when - * a binding is destroyed. For example, a subscription to a stream of data may need to - * be disposed, or an interval may need to be cleared. - * - * ### Example ([live demo](http://plnkr.co/edit/i8pm5brO4sPaLxBx56MR?p=preview)) - * - * In this example, a pipe is created to countdown its input value, updating it every - * 50ms. Because it maintains an internal interval, it automatically clears - * the interval when the binding is destroyed or the countdown completes. - * - * ``` - * import {OnDestroy, Pipe, PipeTransform} from '@angular/core' - * @Pipe({name: 'countdown', pure: false}) - * class CountDown implements PipeTransform, OnDestroy { - * remainingTime:Number; - * interval:SetInterval; - * ngOnDestroy() { - * if (this.interval) { - * clearInterval(this.interval); - * } - * } - * transform(value: any, args: any[] = []) { - * if (!parseInt(value, 10)) return null; - * if (typeof this.remainingTime !== 'number') { - * this.remainingTime = parseInt(value, 10); - * } - * if (!this.interval) { - * this.interval = setInterval(() => { - * this.remainingTime-=50; - * if (this.remainingTime <= 0) { - * this.remainingTime = 0; - * clearInterval(this.interval); - * delete this.interval; - * } - * }, 50); - * } - * return this.remainingTime; - * } - * } - * ``` - * - * Invoking `{{ 10000 | countdown }}` would cause the value to be decremented by 50, - * every 50ms, until it reaches 0. + * See {@linkDocs guide/lifecycle-hooks "Lifecycle Hooks Guide"}. * * @stable */ export abstract class OnDestroy { abstract ngOnDestroy(): void; } /** - * Implement this interface to get notified when your directive's content has been fully + * + * @whatItDoes Lifecycle hook that is called after a directive's content has been fully * initialized. + * @howToUse + * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='AfterContentInit'} * - * ### Example ([live demo](http://plnkr.co/edit/plamXUpsLQbIXpViZhUO?p=preview)) + * @description + * See {@linkDocs guide/lifecycle-hooks#aftercontent "Lifecycle Hooks Guide"}. * - * ```typescript - * @Component({ - * selector: 'child-cmp', - * template: `{{where}} child` - * }) - * class ChildComponent { - * @Input() where: string; - * } - * - * @Component({ - * selector: 'parent-cmp', - * template: `` - * }) - * class ParentComponent implements AfterContentInit { - * @ContentChild(ChildComponent) contentChild: ChildComponent; - * - * constructor() { - * // contentChild is not initialized yet - * console.log(this.getMessage(this.contentChild)); - * } - * - * ngAfterContentInit() { - * // contentChild is updated after the content has been checked - * console.log('AfterContentInit: ' + this.getMessage(this.contentChild)); - * } - * - * private getMessage(cmp: ChildComponent): string { - * return cmp ? cmp.where + ' child' : 'no child'; - * } - * } - * - * @Component({ - * selector: 'app', - * template: ` - * - * - * `, - * directives: [ParentComponent, ChildComponent] - * }) - * export class App { - * } - * ``` * @stable */ export abstract class AfterContentInit { abstract ngAfterContentInit(): void; } /** - * Implement this interface to get notified after every check of your directive's content. + * @whatItDoes Lifecycle hook that is called after every check of a directive's content. + * @howToUse + * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='AfterContentChecked'} * - * ### Example ([live demo](http://plnkr.co/edit/tGdrytNEKQnecIPkD7NU?p=preview)) + * @description + * See {@linkDocs guide/lifecycle-hooks#aftercontent "Lifecycle Hooks Guide"}. * - * ```typescript - * @Component({selector: 'child-cmp', template: `{{where}} child`}) - * class ChildComponent { - * @Input() where: string; - * } - * - * @Component({selector: 'parent-cmp', template: ``}) - * class ParentComponent implements AfterContentChecked { - * @ContentChild(ChildComponent) contentChild: ChildComponent; - * - * constructor() { - * // contentChild is not initialized yet - * console.log(this.getMessage(this.contentChild)); - * } - * - * ngAfterContentChecked() { - * // contentChild is updated after the content has been checked - * console.log('AfterContentChecked: ' + this.getMessage(this.contentChild)); - * } - * - * private getMessage(cmp: ChildComponent): string { - * return cmp ? cmp.where + ' child' : 'no child'; - * } - * } - * - * @Component({ - * selector: 'app', - * template: ` - * - * - * - * `, - * directives: [NgIf, ParentComponent, ChildComponent] - * }) - * export class App { - * hasContent = true; - * } - * ``` * @stable */ export abstract class AfterContentChecked { abstract ngAfterContentChecked(): void; } /** - * Implement this interface to get notified when your component's view has been fully initialized. + * @whatItDoes Lifecycle hook that is called after a component's view has been fully + * initialized. + * @howToUse + * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='AfterViewInit'} * - * ### Example ([live demo](http://plnkr.co/edit/LhTKVMEM0fkJgyp4CI1W?p=preview)) + * @description + * See {@linkDocs guide/lifecycle-hooks#afterview "Lifecycle Hooks Guide"}. * - * ```typescript - * @Component({selector: 'child-cmp', template: `{{where}} child`}) - * class ChildComponent { - * @Input() where: string; - * } - * - * @Component({ - * selector: 'parent-cmp', - * template: ``, - * directives: [ChildComponent] - * }) - * class ParentComponent implements AfterViewInit { - * @ViewChild(ChildComponent) viewChild: ChildComponent; - * - * constructor() { - * // viewChild is not initialized yet - * console.log(this.getMessage(this.viewChild)); - * } - * - * ngAfterViewInit() { - * // viewChild is updated after the view has been initialized - * console.log('ngAfterViewInit: ' + this.getMessage(this.viewChild)); - * } - * - * private getMessage(cmp: ChildComponent): string { - * return cmp ? cmp.where + ' child' : 'no child'; - * } - * } - * - * @Component({ - * selector: 'app', - * template: ``, - * directives: [ParentComponent] - * }) - * export class App { - * } - * ``` * @stable */ export abstract class AfterViewInit { abstract ngAfterViewInit(): void; } /** - * Implement this interface to get notified after every check of your component's view. + * @whatItDoes Lifecycle hook that is called after every check of a component's view. + * @howToUse + * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='AfterViewChecked'} * - * ### Example ([live demo](http://plnkr.co/edit/0qDGHcPQkc25CXhTNzKU?p=preview)) + * @description + * See {@linkDocs guide/lifecycle-hooks#afterview "Lifecycle Hooks Guide"}. * - * ```typescript - * @Component({selector: 'child-cmp', template: `{{where}} child`}) - * class ChildComponent { - * @Input() where: string; - * } - * - * @Component({ - * selector: 'parent-cmp', - * template: ` - * - * `, - * directives: [NgIf, ChildComponent] - * }) - * class ParentComponent implements AfterViewChecked { - * @ViewChild(ChildComponent) viewChild: ChildComponent; - * showView = true; - * - * constructor() { - * // viewChild is not initialized yet - * console.log(this.getMessage(this.viewChild)); - * } - * - * ngAfterViewChecked() { - * // viewChild is updated after the view has been checked - * console.log('AfterViewChecked: ' + this.getMessage(this.viewChild)); - * } - * - * private getMessage(cmp: ChildComponent): string { - * return cmp ? cmp.where + ' child' : 'no child'; - * } - * } - * - * @Component({ - * selector: 'app', - * template: ``, - * directives: [ParentComponent] - * }) - * export class App { - * } - * ``` * @stable */ export abstract class AfterViewChecked { abstract ngAfterViewChecked(): void; } diff --git a/modules/@angular/examples/core/ts/metadata/lifecycle_hooks_spec.ts b/modules/@angular/examples/core/ts/metadata/lifecycle_hooks_spec.ts new file mode 100644 index 0000000000..76bfd99358 --- /dev/null +++ b/modules/@angular/examples/core/ts/metadata/lifecycle_hooks_spec.ts @@ -0,0 +1,150 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, Component, DoCheck, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, Type} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +export function main() { + describe('lifecycle hooks examples', () => { + it('should work with ngOnInit', () => { + // #docregion OnInit + @Component({selector: 'my-cmp', template: `...`}) + class MyComponent implements OnInit { + ngOnInit() { + // ... + } + } + // #enddocregion + + expect(createAndLogComponent(MyComponent)).toEqual([['ngOnInit', []]]); + }); + + it('should work with ngDoCheck', () => { + // #docregion DoCheck + @Component({selector: 'my-cmp', template: `...`}) + class MyComponent implements DoCheck { + ngDoCheck() { + // ... + } + } + // #enddocregion + + expect(createAndLogComponent(MyComponent)).toEqual([['ngDoCheck', []]]); + }); + + it('should work with ngAfterContentChecked', () => { + // #docregion AfterContentChecked + @Component({selector: 'my-cmp', template: `...`}) + class MyComponent implements AfterContentChecked { + ngAfterContentChecked() { + // ... + } + } + // #enddocregion + + expect(createAndLogComponent(MyComponent)).toEqual([['ngAfterContentChecked', []]]); + }); + + it('should work with ngAfterContentInit', () => { + // #docregion AfterContentInit + @Component({selector: 'my-cmp', template: `...`}) + class MyComponent implements AfterContentInit { + ngAfterContentInit() { + // ... + } + } + // #enddocregion + + expect(createAndLogComponent(MyComponent)).toEqual([['ngAfterContentInit', []]]); + }); + + it('should work with ngAfterViewChecked', () => { + // #docregion AfterViewChecked + @Component({selector: 'my-cmp', template: `...`}) + class MyComponent implements AfterViewChecked { + ngAfterViewChecked() { + // ... + } + } + // #enddocregion + + expect(createAndLogComponent(MyComponent)).toEqual([['ngAfterViewChecked', []]]); + }); + + it('should work with ngAfterViewInit', () => { + // #docregion AfterViewInit + @Component({selector: 'my-cmp', template: `...`}) + class MyComponent implements AfterViewInit { + ngAfterViewInit() { + // ... + } + } + // #enddocregion + + expect(createAndLogComponent(MyComponent)).toEqual([['ngAfterViewInit', []]]); + }); + + it('should work with ngOnDestroy', () => { + // #docregion OnDestroy + @Component({selector: 'my-cmp', template: `...`}) + class MyComponent implements OnDestroy { + ngOnDestroy() { + // ... + } + } + // #enddocregion + + expect(createAndLogComponent(MyComponent)).toEqual([['ngOnDestroy', []]]); + }); + + it('should work with ngOnChanges', () => { + // #docregion OnChanges + @Component({selector: 'my-cmp', template: `...`}) + class MyComponent implements OnChanges { + @Input() + prop: number; + + ngOnChanges(changes: SimpleChanges) { + // changes.prop contains the old and the new value... + } + } + // #enddocregion + + const log = createAndLogComponent(MyComponent, ['prop']); + expect(log.length).toBe(1); + expect(log[0][0]).toBe('ngOnChanges'); + const changes: SimpleChanges = log[0][1][0]; + expect(changes['prop'].currentValue).toBe(true); + }); + }); + + function createAndLogComponent(clazz: Type, inputs: string[] = []): any[] { + const log: any[] = []; + createLoggingSpiesFromProto(clazz, log); + + const inputBindings = inputs.map(input => `[${input}] = true`).join(' '); + + @Component({template: ``}) + class ParentComponent { + } + + + const fixture = TestBed.configureTestingModule({declarations: [ParentComponent, clazz]}) + .createComponent(ParentComponent); + fixture.detectChanges(); + fixture.destroy(); + return log; + } + + function createLoggingSpiesFromProto(clazz: Type, log: any[]) { + const proto = clazz.prototype; + Object.keys(proto).forEach((method) => { + proto[method] = (...args: any[]) => { log.push([method, args]); }; + }); + } +}