diff --git a/public/docs/_examples/change-detection/e2e-spec.ts b/public/docs/_examples/change-detection/e2e-spec.ts new file mode 100644 index 0000000000..fae7bd5d4a --- /dev/null +++ b/public/docs/_examples/change-detection/e2e-spec.ts @@ -0,0 +1,191 @@ +'use strict'; // necessary for es6 output in node + +import { browser, element, by, ExpectedConditions as EC } from 'protractor'; + +describe('Change Detection guide', () => { + + beforeEach(() => { + + // setInterval() used in the code makes Protractor mistakenly think we're not + // finished loading unless we turn this on. + browser.ignoreSynchronization = true; + + browser.get('/'); + return browser.wait(EC.presenceOf(element(by.css('[ng-version]')))); + }); + + describe('Basic Example', () => { + + it('displays a counter that can be incremented and decremented', () => { + const component = element(by.tagName('hero-counter')); + const counter = component.element(by.css('span')); + + expect(counter.getText()).toEqual('5'); + + component.element(by.buttonText('+')).click(); + expect(counter.getText()).toEqual('6'); + component.element(by.buttonText('-')).click(); + expect(counter.getText()).toEqual('5'); + }); + + }); + + describe('Broken name badge example', () => { + + it('causes an error', () => { + const errorDump = element(by.id('bootstrapError')); + expect(errorDump.getText()).toContain('HeroNameBadgeBrokenComponent'); + expect(errorDump.getText()).toContain('Expression has changed after it was checked'); + }); + + it('still displays the bound data', () => { + const component = element(by.tagName('hero-name-badge-broken')); + expect(component.element(by.css('h4')).getText()).toEqual('Anonymous details'); + }); + + }); + + describe('Fixed name badge example', () => { + + it('displays the bound data', () => { + const component = element(by.tagName('hero-name-badge')); + expect(component.element(by.css('h4')).getText()).toEqual('details'); + expect(component.element(by.css('p')).getText()).toEqual('Name: Anonymous'); + }); + + }); + + describe('OnPush', () => { + + describe('with immutable string inputs', () => { + + it('displays the bound data', () => { + const component = element(by.tagName('hero-search-result')); + const match = component.element(by.css('.match')); + expect(match.getText()).toEqual('indsto'); + }); + + }); + + describe('with input mutations', () => { + + it('does not display the mutations', () => { + const component = element(by.tagName('hero-manager-mutable')); + + expect(component.element(by.cssContainingText('li', 'Windstorm')).isPresent()).toBe(true); + expect(component.element(by.cssContainingText('li', 'Magneta')).isPresent()).toBe(true); + component.element(by.buttonText('Add one more')).click(); + expect(component.element(by.cssContainingText('li', 'Bombasto')).isPresent()).toBe(false); + + }); + + }); + + describe('with immutable array input', () => { + + it('displays the changes', () => { + const component = element(by.tagName('hero-manager-immutable')); + + expect(component.element(by.cssContainingText('li', 'Windstorm')).isPresent()).toBe(true); + expect(component.element(by.cssContainingText('li', 'Magneta')).isPresent()).toBe(true); + component.element(by.buttonText('Add one more')).click(); + expect(component.element(by.cssContainingText('li', 'Bombasto')).isPresent()).toBe(true); + + }); + + }); + + describe('with events', () => { + + it('displays the changes', () => { + const component = element(by.tagName('hero-counter-onpush')); + const counter = component.element(by.css('span')); + + expect(counter.getText()).toEqual('5'); + + component.element(by.buttonText('+')).click(); + expect(counter.getText()).toEqual('6'); + component.element(by.buttonText('-')).click(); + expect(counter.getText()).toEqual('5'); + }); + + }); + + describe('with explicit markForDetection()', () => { + + it('does not detect setInterval() when not used', () => { + const component = element(by.tagName('hero-counter-auto-broken')); + browser.sleep(300); // There's an interval of 100ms inside the component. + expect(component.getText()).toEqual('Number of heroes: 5'); + }); + + it('does detect setInterval() when used', () => { + const component = element(by.tagName('hero-counter-auto')); + browser.sleep(300); // There's an interval of 100ms inside the component. + expect(component.getText()).not.toEqual('Number of heroes: 5'); + expect(component.getText()).toMatch(/Number of heroes: \d+/); + }); + + it('detects on evented library callbacks', () => { + const component = element(by.tagName('hero-name-badge-evented')); + expect(component.element(by.cssContainingText('h4', 'Windstorm details')).isPresent()).toBe(true); + element(by.buttonText('Rename')).click(); + expect(component.element(by.cssContainingText('h4', 'Magneta details')).isPresent()).toBe(true); + }); + + }); + + describe('detached', () => { + + it('does not pick up changes automatically', () => { + const component = element(by.tagName('hero-name-badge-detached')); + expect(component.element(by.css('h4')).getText()).toEqual('Windstorm details'); + element(by.buttonText('Rename detached')).click(); + expect(component.element(by.css('h4')).getText()).toEqual('Windstorm details'); + }); + + it('starts picking up changes again when reattached', () => { + const component = element(by.tagName('hero-counter-live')); + const count = component.element(by.css('.count')); + + const text1 = count.getText(); + browser.sleep(100); + component.element(by.buttonText('Toggle live update')).click(); + const text2 = count.getText(); + browser.sleep(100); + const text3 = count.getText(); + + expect(text1).not.toEqual(text2); + expect(text2).toEqual(text3); + }); + + it('can be used for throttling by explicitly detecting with an interval', () => { + const component = element(by.tagName('hero-counter-throttled')); + const count = component.element(by.css('.count')); + + const text1 = count.getText(); + browser.sleep(100); + const text2 = count.getText(); + browser.sleep(100); + const text3 = count.getText(); + + Promise.all([text1, text2, text3]).then(([t1, t2, t3]) => { + let differences = 0; + if (t1 !== t2) { + differences++; + } + if (t2 !== t3) { + differences++; + } + expect(differences).toBeLessThan(2); + }); + }); + + }); + + + + + }); + +}); diff --git a/public/docs/_examples/change-detection/ts/example-config.json b/public/docs/_examples/change-detection/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/change-detection/ts/src/app/app.component.ts b/public/docs/_examples/change-detection/ts/src/app/app.component.ts new file mode 100644 index 0000000000..e3d09ccce9 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/app.component.ts @@ -0,0 +1,115 @@ +import { Component } from '@angular/core'; +import { Hero } from './hero.model'; +import { HeroModel } from './onpush/hero-evented.model'; + +@Component({ + moduleId: module.id, + selector: 'hero-app', + template: ` +

Angular Change Detection

+ + +

Basic Example

+ + + +

Single-Pass

+ +

Broken Example

+ + + +

Fixed Example

+ + + + +

OnPush

+ +

Immutable Primitive Values

+

OnPush only runs detection when inputs change.

+ + + + +

Mutable Collection, Broken Example

+

OnPush does not detect changes inside array inputs.

+ + + +

Immutable Collection, Fixed Example

+

OnPush detects changes for array inputs as longs as they're treated as immutable values.

+ + + +

Events

+

OnPush detects changes when they originate in an event handler.

+ + + + +

Explicit Change Marking, Broken Without

+

A counter incrementing with setTimeout() inside an OnPush component does not update.

+ + + +

Explicit Change Marking

+

This is fixed using markForCheck()

+ + + +

Explicit Change Marking with Library Callback

+ + + + + +

Detaching

+ +

Permanently, "One-Time Binding"

+

By detaching a component's change detector at ngOnInit() we can do "one-time binding".

+ + + + +

Temporarily, reattach

+

By detaching/reattaching a change detector we can toggle whether a component has "live updates".

+ + + +

Throttling with Internal detectChanges

+

+ By calling detectChanges() on a detached change detector we can choose when change detection is done. + This can be used to update the view at a lower frequency than data changes. +

+ + + +

Flushing to DOM with Internal detectChanges

+

We can use detectChanges() to flush changes to the view immediately if we can't wait for the next turn of the zone.

+ + + +

Escaping NgZone For Async Work

+ +

Without

+

Many unnecessary change detections will be performed for this workflow because it is all inside NgZone.

+ + ` +}) +export class AppComponent { + hero: Hero = {name: 'Windstorm', onDuty: true}; + anonymousHero: Hero = {name: '', onDuty: false}; + secondAnonymousHero: Hero = {name: '', onDuty: false}; + + heroModel = new HeroModel('Windstorm'); + + renameHero() { + this.hero.name = 'Magneta'; + } + + renameHeroModel() { + this.heroModel.setName('Magneta'); + } + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/app.module.ts b/public/docs/_examples/change-detection/ts/src/app/app.module.ts new file mode 100644 index 0000000000..042770baf5 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/app.module.ts @@ -0,0 +1,52 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; + +import { AppComponent } from './app.component'; + +import { HeroCounterComponent } from './hero-counter.component'; +import { HeroNameBadgeBrokenComponent } from './hero-name-badge.broken.component'; +import { HeroNameBadgeComponent } from './hero-name-badge.component'; +import { SearchResultComponent } from './onpush/search-result.component'; +import { HeroListComponent as HeroListOnpushComponent } from './onpush/hero-list.onpush.component'; +import { HeroManagerMutableComponent } from './onpush/hero-manager.mutable.component'; +import { HeroManagerImmutableComponent } from './onpush/hero-manager.immutable.component'; +import { HeroCounterComponent as HeroCounterOnPushComponent } from './onpush/hero-counter.onpush.component'; +import { HeroCounterAutoComponent } from './onpush/hero-counter-auto.component'; +import { HeroCounterAutoComponent as HeroCounterAutoBrokenComponent } from './onpush/hero-counter-auto.broken.component'; +import { HeroNameBadgeComponent as HeroNameBadgeEventedComponent } from './onpush/hero-name-badge-evented.component'; +import { HeroNameBadgeComponent as HeroNameBadgeDetachedComponent } from './detach/hero-name-badge-detached.component'; +import { HeroCounterComponent as HeroCounterLiveComponent } from './detach/hero-counter-live.component'; +import { HeroCounterComponent as HeroCounterThrottledComponent } from './detach/hero-counter-throttled.component'; +import { HeroSignatureFormComponent } from './detach/hero-signature-form.component'; +import { AsyncWorkflowComponent } from './async-workflow.component'; + +@NgModule({ + imports: [ + BrowserModule, + FormsModule + ], + declarations: [ + AppComponent, + HeroCounterComponent, + HeroNameBadgeBrokenComponent, + HeroNameBadgeComponent, + SearchResultComponent, + HeroListOnpushComponent, + HeroManagerMutableComponent, + HeroManagerImmutableComponent, + HeroCounterOnPushComponent, + HeroCounterAutoBrokenComponent, + HeroCounterAutoComponent, + HeroNameBadgeEventedComponent, + HeroNameBadgeDetachedComponent, + HeroCounterLiveComponent, + HeroCounterThrottledComponent, + HeroSignatureFormComponent, + AsyncWorkflowComponent + ], + bootstrap: [ + AppComponent + ] +}) +export class AppModule { } diff --git a/public/docs/_examples/change-detection/ts/src/app/async-workflow.component.ts b/public/docs/_examples/change-detection/ts/src/app/async-workflow.component.ts new file mode 100644 index 0000000000..bfe16885c2 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/async-workflow.component.ts @@ -0,0 +1,73 @@ +import { Component, NgZone } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import 'rxjs/add/observable/interval'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/reduce'; +import 'rxjs/add/operator/take'; + +@Component({ + selector: 'hero-async-workflow', + template: ` + + + Results: + + ` +}) +export class AsyncWorkflowComponent { + results: string[]; + + // #docregion outside-zone + constructor(private ngZone: NgZone) { } + // #enddocregion outside-zone + + // #docregion inside-zone + loadInsideZone() { + Observable.merge(loadHeroes(), loadMoreHeroes(), loadEvenMoreHeroes()) + .reduce((heroes, hero) => [...heroes, hero], []) + .subscribe(heroes => this.results = heroes); + } + // #enddocregion inside-zone + + // #docregion outside-zone + loadOutsideZone() { + // Escape NgZone before starting work. + // No change detection will be performed during this work. + this.ngZone.runOutsideAngular(() => { + Observable.merge(loadHeroes(), loadMoreHeroes(), loadEvenMoreHeroes()) + .reduce((heroes, hero) => [...heroes, hero], []) + .subscribe(heroes => { + // Re-enter zone to process final result. + // Change detection will be performed. + this.ngZone.run(() => this.results = heroes); + }); + }); + } + // #enddocregion outside-zone + +} + +function loadHeroes() { + return Observable.interval(100) + .map(n => `hero a${n}`) + .take(3); +} + +function loadMoreHeroes() { + return Observable.interval(150) + .map(n => `hero b${n}`) + .take(3); +} + +function loadEvenMoreHeroes() { + return Observable.interval(200) + .map(n => `hero c${n}`) + .take(3); +} diff --git a/public/docs/_examples/change-detection/ts/src/app/detach/hero-counter-live.component.ts b/public/docs/_examples/change-detection/ts/src/app/detach/hero-counter-live.component.ts new file mode 100644 index 0000000000..86b1a517d8 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/detach/hero-counter-live.component.ts @@ -0,0 +1,35 @@ +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; + +// #docregion +@Component({ + selector: 'hero-counter-live', + template: ` + Number of heroes: {{ heroCount }} + + ` +}) +export class HeroCounterComponent implements OnInit, OnDestroy { + heroCount = 5; + private live = true; + private updateIntervalId: any; + + constructor(private changeDetector: ChangeDetectorRef) { } + + ngOnInit() { + // Increment counter ten times per second + this.updateIntervalId = setInterval(() => this.heroCount++, 100); + } + + ngOnDestroy() { + clearInterval(this.updateIntervalId); + } + + toggleLive() { + this.live = !this.live; + if (this.live) { + this.changeDetector.reattach(); + } else { + this.changeDetector.detach(); + } + } +} diff --git a/public/docs/_examples/change-detection/ts/src/app/detach/hero-counter-throttled.component.ts b/public/docs/_examples/change-detection/ts/src/app/detach/hero-counter-throttled.component.ts new file mode 100644 index 0000000000..76341f10ee --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/detach/hero-counter-throttled.component.ts @@ -0,0 +1,32 @@ +import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy } from '@angular/core'; + +// #docregion +@Component({ + selector: 'hero-counter-throttled', + template: ` + Number of heroes: {{ heroCount }} + ` +}) +export class HeroCounterComponent implements AfterViewInit, OnDestroy { + heroCount = 5; + + private dataUpdateIntervalId: any; + private viewUpdateIntervalId: any; + + constructor(private changeDetector: ChangeDetectorRef) { } + + ngAfterViewInit() { + // Detach the change detector so it never runs unless we do it manually. + this.changeDetector.detach(); + // Change data a hundred times per second... + this.dataUpdateIntervalId = setInterval(() => this.heroCount++, 10); + // ...but detect changes only once per second + this.viewUpdateIntervalId = setInterval(() => this.changeDetector.detectChanges(), 1000); + } + + ngOnDestroy() { + clearInterval(this.dataUpdateIntervalId); + clearInterval(this.viewUpdateIntervalId); + } + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/detach/hero-name-badge-detached.component.ts b/public/docs/_examples/change-detection/ts/src/app/detach/hero-name-badge-detached.component.ts new file mode 100644 index 0000000000..9bd4571828 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/detach/hero-name-badge-detached.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectorRef, Component, Input, AfterViewInit } from '@angular/core'; +import { Hero } from '../hero.model'; + +// #docregion +@Component({ + selector: 'hero-name-badge-detached', + template: ` +

{{ hero.name }} details

+

Name: {{ hero.name }}

+ ` +}) +export class HeroNameBadgeComponent implements AfterViewInit { + @Input() hero: Hero; + + constructor(private changeDetector: ChangeDetectorRef) { } + + ngAfterViewInit() { + this.changeDetector.detach(); + } + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/detach/hero-signature-form.component.ts b/public/docs/_examples/change-detection/ts/src/app/detach/hero-signature-form.component.ts new file mode 100644 index 0000000000..57a011c99c --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/detach/hero-signature-form.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core'; + +// #docregion +@Component({ + selector: 'hero-signature-form', + template: ` +
+ + + +
+ ` +}) +export class HeroSignatureFormComponent { + @ViewChild('signatureForm') signatureForm: ElementRef; + + username: string; + secret: string; + + constructor(private changeDetector: ChangeDetectorRef) { } + + sendForm() { + this.secret = calculateSecret(this.username); + // Ensure the secret is flushed into the form field before we submit. + this.changeDetector.detectChanges(); + this.signatureForm.nativeElement.submit(); + } + +} +// #enddocregion + +function calculateSecret(username: string) { + return `SECRET FOR ${username}`; +} diff --git a/public/docs/_examples/change-detection/ts/src/app/hero-counter.component.ts b/public/docs/_examples/change-detection/ts/src/app/hero-counter.component.ts new file mode 100644 index 0000000000..09267f0ecc --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/hero-counter.component.ts @@ -0,0 +1,20 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'hero-counter', + template: ` + Number of heroes: + + {{ heroCount }} + + ` +}) +export class HeroCounterComponent { + heroCount = 5; + + // When we change data, we don't need to do anything to update the view. + increase() { this.heroCount = this.heroCount + 1; } + decrease() { this.heroCount = Math.max(this.heroCount - 1, 0); } + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/hero-name-badge.broken.component.ts b/public/docs/_examples/change-detection/ts/src/app/hero-name-badge.broken.component.ts new file mode 100644 index 0000000000..4cb63f5458 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/hero-name-badge.broken.component.ts @@ -0,0 +1,24 @@ +import { Component, Input } from '@angular/core'; +import { Hero } from './hero.model'; + +// #docregion +@Component({ + selector: 'hero-name-badge-broken', + template: ` +

{{ hero.name }} details

+

Name: {{ getDisplayName() }}

+ ` +}) +export class HeroNameBadgeBrokenComponent { + @Input() hero: Hero; + + getDisplayName() { + if (!this.hero.name || this.hero.name.length === 0) { + // We're setting the name during change detection. + // This may cause errors in other bindings that refer to the same data! + this.hero.name = 'Anonymous'; + } + return this.hero.name; + } + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/hero-name-badge.component.ts b/public/docs/_examples/change-detection/ts/src/app/hero-name-badge.component.ts new file mode 100644 index 0000000000..f639c7b794 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/hero-name-badge.component.ts @@ -0,0 +1,25 @@ +import { Component, Input } from '@angular/core'; +import { Hero } from './hero.model'; + +// #docregion +@Component({ + selector: 'hero-name-badge', + template: ` +

{{ hero.name }} details

+

Name: {{ getDisplayName() }}

+ ` +}) +export class HeroNameBadgeComponent { + @Input() hero: Hero; + + getDisplayName() { + if (!this.hero.name || this.hero.name.length === 0) { + // Here we just return the value we want to see in the view, + // without mutating anything. + return 'Anonymous'; + } else { + return this.hero.name; + } + } + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/hero.model.ts b/public/docs/_examples/change-detection/ts/src/app/hero.model.ts new file mode 100644 index 0000000000..8fe8c9b97f --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/hero.model.ts @@ -0,0 +1,4 @@ +export interface Hero { + name: string; + onDuty: boolean; +} diff --git a/public/docs/_examples/change-detection/ts/src/app/main.ts b/public/docs/_examples/change-detection/ts/src/app/main.ts new file mode 100644 index 0000000000..2470c9595e --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/main.ts @@ -0,0 +1,4 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/public/docs/_examples/change-detection/ts/src/app/onpush/hero-counter-auto.broken.component.ts b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-counter-auto.broken.component.ts new file mode 100644 index 0000000000..1836e869c1 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-counter-auto.broken.component.ts @@ -0,0 +1,29 @@ +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit +} from '@angular/core'; + +// #docregion +@Component({ + selector: 'hero-counter-auto-broken', + template: ` + Number of heroes: {{ heroCount }} + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class HeroCounterAutoComponent implements OnInit, OnDestroy { + heroCount = 5; + private updateIntervalId: any; + + ngOnInit() { + // Changes made in the interval loop will not be detected! + this.updateIntervalId = setInterval(() => this.heroCount++, 100); + } + + ngOnDestroy() { + clearInterval(this.updateIntervalId); + } + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/onpush/hero-counter-auto.component.ts b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-counter-auto.component.ts new file mode 100644 index 0000000000..896ff0f984 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-counter-auto.component.ts @@ -0,0 +1,34 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit +} from '@angular/core'; + +// #docregion +@Component({ + selector: 'hero-counter-auto', + template: ` + Number of heroes: {{ heroCount }} + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class HeroCounterAutoComponent implements OnInit, OnDestroy { + heroCount = 5; + private updateIntervalId: any; + + constructor(private changeDetector: ChangeDetectorRef) { } + + ngOnInit() { + this.updateIntervalId = setInterval(() => { + this.heroCount++; + this.changeDetector.markForCheck(); + }, 100); + } + + ngOnDestroy() { + clearInterval(this.updateIntervalId); + } + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/onpush/hero-counter.onpush.component.ts b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-counter.onpush.component.ts new file mode 100644 index 0000000000..6364f044ea --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-counter.onpush.component.ts @@ -0,0 +1,20 @@ +// #docregion +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'hero-counter-onpush', + template: ` + Number of heroes: + + {{ heroCount }} + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class HeroCounterComponent { + heroCount = 5; + + increase() { this.heroCount = this.heroCount + 1; } + decrease() { this.heroCount = Math.max(this.heroCount - 1, 0); } + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/onpush/hero-evented.model.ts b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-evented.model.ts new file mode 100644 index 0000000000..cd184f416d --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-evented.model.ts @@ -0,0 +1,31 @@ +/* + * A faux "evented model" class that emulates the kind of patterns + * used by libraries like Knockout, Backbone, Breeze. + */ +export class HeroModel { + private changeListeners: (() => void)[] = []; + + constructor(private name: string) { } + + getName() { + return this.name; + } + + setName(newName: string) { + this.name = newName; + for (let changeListener of this.changeListeners) { + changeListener(); + } + } + + subscribeToChanges(listener: () => void) { + this.changeListeners.push(listener); + return () => { + const idx = this.changeListeners.indexOf(listener); + if (idx >= 0) { + this.changeListeners.splice(idx, 1); + } + }; + } + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/onpush/hero-list.onpush.component.ts b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-list.onpush.component.ts new file mode 100644 index 0000000000..e671683d56 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-list.onpush.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { Hero } from '../hero.model'; + +// #docregion +@Component({ + selector: 'hero-list-onpush', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class HeroListComponent { + @Input() heroes: Hero[]; +} diff --git a/public/docs/_examples/change-detection/ts/src/app/onpush/hero-manager.immutable.component.ts b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-manager.immutable.component.ts new file mode 100644 index 0000000000..3cf7516ba4 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-manager.immutable.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { Hero } from '../hero.model'; + +@Component({ + moduleId: module.id, + selector: 'hero-manager-immutable', + template: ` + + + + ` +}) +export class HeroManagerImmutableComponent { + heroes: Hero[] = [ + {name: 'Windstorm', onDuty: true}, + {name: 'Magneta', onDuty: false} + ]; + + // #docregion add-hero + addHero() { + this.heroes = [...this.heroes, {name: 'Bombasto', onDuty: true}]; + } + // #enddocregion add-hero + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/onpush/hero-manager.mutable.component.ts b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-manager.mutable.component.ts new file mode 100644 index 0000000000..8ccc326cce --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-manager.mutable.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { Hero } from '../hero.model'; + +@Component({ + moduleId: module.id, + selector: 'hero-manager-mutable', + template: ` + + + + ` +}) +export class HeroManagerMutableComponent { + heroes: Hero[] = [ + {name: 'Windstorm', onDuty: true}, + {name: 'Magneta', onDuty: false} + ]; + + // #docregion add-hero + addHero() { + // This will not be detected by the child component with OnPush! + this.heroes.push({name: 'Bombasto', onDuty: true}); + } + // #enddocregion add-hero + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/onpush/hero-name-badge-evented.component.ts b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-name-badge-evented.component.ts new file mode 100644 index 0000000000..e044be821d --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/onpush/hero-name-badge-evented.component.ts @@ -0,0 +1,37 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit +} from '@angular/core'; +import { HeroModel } from './hero-evented.model'; + +// #docregion +@Component({ + selector: 'hero-name-badge-evented', + template: ` +

{{ hero.getName() }} details

+

Name: {{ hero.getName() }}

+ `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class HeroNameBadgeComponent implements OnInit, OnDestroy { + @Input() hero: HeroModel; + private unsubscribe: () => void; + + constructor(private changeDetector: ChangeDetectorRef) { } + + ngOnInit() { + // Subscribe to changes in the hero model and run the component's change + // detector every time we get notified. + this.unsubscribe = + this.hero.subscribeToChanges(() => this.changeDetector.markForCheck()); + } + + ngOnDestroy() { + this.unsubscribe(); + } + +} diff --git a/public/docs/_examples/change-detection/ts/src/app/onpush/search-result.component.ts b/public/docs/_examples/change-detection/ts/src/app/onpush/search-result.component.ts new file mode 100644 index 0000000000..9ebb661834 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/app/onpush/search-result.component.ts @@ -0,0 +1,35 @@ +// #docregion +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +@Component({ + selector: 'hero-search-result', + template: ` + {{ getPrefix() }}{{ getMatch() }}{{ getSuffix() }} + `, + styles: [`.match { background-color: yellow; }`], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SearchResultComponent { + @Input() searchResult: string; + @Input() searchTerm: string; + + getPrefix() { + let matchIdx = this.getSearchTermMatchIndex(); + return this.searchResult.substring(0, matchIdx); + } + + getMatch() { + let matchIdx = this.getSearchTermMatchIndex(); + return this.searchResult.substring(matchIdx, matchIdx + this.searchTerm.length); + } + + getSuffix() { + let matchIdx = this.getSearchTermMatchIndex(); + return this.searchResult.substring(matchIdx + this.searchTerm.length); + } + + private getSearchTermMatchIndex() { + return this.searchResult.toLowerCase().indexOf(this.searchTerm.toLowerCase()); + } + +} diff --git a/public/docs/_examples/change-detection/ts/src/index.html b/public/docs/_examples/change-detection/ts/src/index.html new file mode 100644 index 0000000000..b21acaea33 --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/index.html @@ -0,0 +1,31 @@ + + + + + Angular Change Detection + + + + + + + + + + + + + + + + + Loading... + + +

Bootstrap error(s):

+
+ + + diff --git a/public/docs/_examples/change-detection/ts/src/main.ts b/public/docs/_examples/change-detection/ts/src/main.ts new file mode 100644 index 0000000000..de691cff2c --- /dev/null +++ b/public/docs/_examples/change-detection/ts/src/main.ts @@ -0,0 +1,7 @@ +// #docregion +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app/app.module'; + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch(e => document.querySelector('#bootstrapError').textContent = e); diff --git a/public/docs/_examples/change-detection/ts/src/sample.css b/public/docs/_examples/change-detection/ts/src/sample.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/ts/latest/guide/_data.json b/public/docs/ts/latest/guide/_data.json index e4a055b5ac..3bd21c75a6 100644 --- a/public/docs/ts/latest/guide/_data.json +++ b/public/docs/ts/latest/guide/_data.json @@ -116,6 +116,11 @@ "intro": "Browser support and polyfills guide." }, + "change-detection": { + "title": "Change Detection", + "intro": "Learn how Angular detects changes in your data to make sure the UI is always up to date." + }, + "component-styles": { "title": "Component Styles", "intro": "Learn how to apply CSS styles to components." diff --git a/public/docs/ts/latest/guide/change-detection.jade b/public/docs/ts/latest/guide/change-detection.jade new file mode 100644 index 0000000000..a9ed91c44f --- /dev/null +++ b/public/docs/ts/latest/guide/change-detection.jade @@ -0,0 +1,508 @@ +block includes + include ../_util-fns + +.l-main-section +:marked + Change detection is one of Angular's defining features. It allows you to display + data in your component views that's *bound* to your data models. When the data changes, + you don't have to worry about keeping the view up to date. Angular does that for you. + + Most of the time change detection just works and you don't really have to think about it. + But occasionally you may run into situations where you want to tweak something about it. + This is a guide for those situations. + + # Contents + + * [How Angular's Change Detection Works](#how-change-detection-works). + * [Change Detection is Performed for Data Bindings](#data-binding) + * [Change Detection Runs At Every Turn Of The NgZone](#zones) + * [Change Detection Runs Along The Component Tree](#tree) + * [Change Detection Is Single-Pass](#single-pass) + * [Strategies for Customizing Change Detection](#strategies) + * [The `OnPush` Strategy](#onpush) + * [`OnPush` with Immutable Inputs](#onpush-immutable) + * [`OnPush` with Event Bindings](#onpush-event) + * [`OnPush` with Explicit Change Detection Triggering](#onpush-manual) + * [Taking Control By Detaching Change Detectors](#detach) + * [One-Time Binding](#detach-onetime) + * [Temporarily Disabling Change Detection](#detach-reattach) + * [Throttling Change Detection](#detach-throttle) + * [Flushing Changes To The View Synchronously](#detach-flush) + * [The Difference between `markForCheck()` and `detectChanges()`](#markforcheck-vs-detectchanges) + * [Skipping Change Detection For Asynchronous Work By Escaping The NgZone](#escape-ngzone) + * [Appendix: A Peek Into Change Detector Code Generation](#codegen) + +a(id="how-change-detection-works") +.l-main-section +:marked + ## How Angular's Change Detection Works + + Change detection in Angular is performed by **change detectors** for all + **data bindings** in your components. These change detectors run after every turn of the + **NgZone**. Every component has its own change detector, and each detector + is **executed once per each turn** of the zone. + +a(id="data-binding") +:marked + ### Change Detection is Performed for Data Bindings + + Angular allows you to bind dynamic data into component views by using *expressions*. When you do that, + you can rest assured that the view will always display the latest version of the data. When the data changes, + there is no need to explicitly "tell" the component to update its view. + ++makeExample('change-detection/ts/src/app/hero-counter.component.ts')(format='.') + +:marked + Change detection is in effect anywhere you might use an expression: + + * When interpolating an expression into the view: `

{{ data }}

` + * When binding an expression to an attribute: `
` + * In structural directive expressions: `
  • ` + * When binding an expression to a component's host element: `@HostBinding('style.width.px') width: number` + + Common to all these examples is that you're referring to data that may + change over time. When the data changes, you'd like that change to be reflected + in your view without having to do manual work. This is the problem that Angular's + change detection solves. + + This job is done by **change detectors**. There's a change detector inside every + Angular component. When Angular compiles your component, it finds all the data binding + expressions you have used in that component, and then it'll automatically keep checking for + changes in those expressions to ensure that the latest data has always been applied. + +figure.image-display + img(src="/resources/images/devguide/change-detection/change-detection-sequence.gif" alt="A Change Detector syncs a component view with the component's data") + +:marked + Change detectors are usually not something you see in your application code. They just do their work + behind the scenes. But there are cases where you may want to manually access change detectors, + as we will see later in this guide. + +a(id="zones") +:marked + ### Change Detection Runs At Every Turn Of The NgZone + + Since Angular promises to *always* keep your UI up to date, it needs to know about all the + situations in which data *might* change. There are many such situations: + + * During the initial application load. + * As a response to user interactions such as clicks and keypresses. + * At scheduled times using JavaScript timeouts and intervals. + * When data is received from a server. + + Whatever the cause of the potential changes, Angular needs to know about it and make sure the changes + are detected. It achieves this with the help of **Zones**. + + Zones are a feature implemented by [the Zone.js library](https://github.com/angular/zone.js/), + which is always included in all Angular applications. Zones can be used to wrap code into an + *execution context* that can then be extended with different utilities. Angular makes use of this by + implementing a special **NgZone**, which is the execution context for all your Angular code. + The most important utility of the `NgZone` is that *whenever it's exited, change detection will be performed*. + + So, whenever something happens in your Angular application, the following sequence of events takes place: + +img(src="/resources/images/devguide/change-detection/change-detection-in-zone.png" alt="Change detection is one upon leaving the NgZone" style="max-width: 600px; float: right") + +:marked + 1. We enter the NgZone. + 2. We run the code that needs to be run. This might be a click handler in one your components, a `setTimeout` function, + an HTTP response handler, etc. + 3. When we're all done, Angular runs change detection just as the `NgZone` is exited. + 4. We exit the `NgZone`, relinquishing control to the browser until the next event occurs. + +a(id="tree") +:marked + ### Change Detection Runs Along The Component Tree + +img(src="/resources/images/devguide/change-detection/change-detection-tree.gif" alt="Change detection is performed along the component tree in a depth-first order" style="float: right; max-width: 400px; margin-left: 25px;") + +:marked + As mentioned, every Angular component has its own change detector, whose job is to + detect changes for all the data bindings inside that component. + + After every turn of the NgZone, *Angular runs all of these change detectors*. This always + happens in the same order. Angular begins from the application's root component(s) and + then walks the component tree using a *depth-first traversal* order, running all the change detectors + along the way. + + The order of the traversal is significant, since it matches the direction of Angular's + component data flow, which is always downward. This ensures that components always have the + freshest data available: When you pass data from a parent component to a child component, + the parent's change detector makes the data available to the child before the child's own + change detector runs. + +.l-sub-section + :marked + There are several [component lifecycle hooks](lifecycle-hooks.html) you can use + to hook your own code to various points relative to the change detection process: + `ngOnChanges`, `ngDoCheck`, `ngAfterViewChecked`, and `ngAfterContentChecked`. + +a(id="single-pass") +:marked + ### Change Detection Is Single-Pass + + Change detection is only performed **once for each expression** after every turn of the NgZone. + This allows it to be very fast. It also means that we cannot have binding expressions that change the + values of other expressions. For example, we cannot bind to a method that mutates data used in other + bindings. + ++makeExample('change-detection/ts/src/app/hero-name-badge.broken.component.ts')(format='.') + +:marked + When Angular is run in development mode and it notices a situation like this, it will throw an _"Expression has + changed after it was checked"_ error, notifying you that data has changed *during* change detection, which is not + allowed. + +.alert.is-helpful + :marked + In development mode, Angular always runs change detection twice in a row so that it + can tell you about these problems. In production mode, these illegal changes simply remain + undetected. + +:marked + What makes this kind of code doubly problematic is that you do not *always* get this error message. Whether you see an + error or not depends on the order in which expressions are evaluated. In the example above, + if `{{ getDisplayName() }}` was bound before `{{ hero.name }}` there would be no error because the mutation + would already have occurred by the time we checked `{{ hero.name }}` for the first time. But the error + might pop up some time later, for example when you decided to refactor your template code. + + For this reason, **always make data binding expressions free of side effects**. + Any code that is called from a bound Angular expression should not mutate data, initiate server calls, or do anything + else except return the value that is to be displayed. + ++makeExample('change-detection/ts/src/app/hero-name-badge.component.ts')(format='.') + +.l-sub-section + :marked + The "Expression has changed after it was checked" error may also come up in combination with the `ngAfterViewInit` + lifecycle hook, because that hook runs after change detection. See the + [Lifecycle Hooks guide](lifecycle-hooks.html#!#wait-a-tick) for more on this. + +a(id="strategies") +.l-main-section +:marked + ## Strategies for Customizing Change Detection + + The change detection mechanism described above is what is used in most applications most of the time. + We usually don't even have to think about it much, because it works and it [works fast](#codegen). + + But there are times when you want more control over the change detection process. + What follows are several strategies with which you can influence how change detection is performed. + +a(id="onpush") +:marked + ### The `OnPush` Strategy + + As we've seen, the way Angular's change detection usually works is by checking all + data bindings to see if any of their values have changed. This is the most flexible + and reliable strategy, since it will detect changes no matter what they are and how + they came to be. + + There are times though when you know that a component's data may only change in limited + circumstances: + + * When the component is given new data through its `@Input`s, + * When the component changes its internal state as a response to a user event, or + * When you explicitly tell the component that the data may have changed. + + The `OnPush` change detection strategy is designed for these circumstances. + It allows the change detection system to *do less work* by skipping change detection + for some of your components in situations that don't match the above criteria. In + some applications this may end up being most of the time. + +a(id="onpush-immutable") +:marked + #### `OnPush` with Immutable Inputs + + Some components are fully stateless, meaning that their behavior is fully driven + by their `@Input`s. This is most often the case for "leaf-level" components at the + lowest levels of your component trees, because they tend to receive simple primitive + values such as strings and numbers as their inputs. + + For example, you might have a `SearchResult` component whose job is to highlight search results. It + takes two string inputs, the search result and the search term, and it uses a background color + to highligh the search term inside the search result. You can use the `OnPush` strategy + for this component. + ++makeExample('change-detection/ts/src/app/onpush/search-result.component.ts')(format='.') + +:marked + If you hadn't used `ChangeDetectionStrategy.OnPush`, this component would have executed all the code in `getPrefix()`, + `getMatch()`, and `getSuffix()` every time the application's change detection runs. But since you did use + `OnPush`, those methods only get invoked when either of the `searchResult` or `searchTerm` inputs changes. + The behavior of the component is exactly identical in both cases, but with `OnPush` less work is done. + + This strategy applies to all kinds of inputs, not just primitives like strings and numbers. + But for changes to be detected, you need to treat the inputs as *immutable values*. This is because + `OnPush` uses a simple reference check (`oldValue !== newValue`) to see if an input + has changed. This means that for example, it will **not** detect items being added or removed inside an existing + array input. + + As an example, you might have a `HeroListComponent` that lists an array of heroes and uses `OnPush` + change detection: + ++makeExample('change-detection/ts/src/app/onpush/hero-list.onpush.component.ts')(format='.') + +:marked + If you bind an array to `heroes` from the component's parent, you *cannot* expect the component + to update its contents if you merely `push` a new item into it. + ++makeExample('change-detection/ts/src/app/onpush/hero-manager.mutable.component.ts', 'add-hero')(format='.') + +:marked + Although the *contents* of `this.heroes` change, the array itself does not. (`oldArray === newArray` will be `true`). + You must instead treat the array in an immutable fashion for changes like these to be detected. + This entails using methods and operators that return *new* arrays, such as `concat`, `slice`, + and the array spread operator. + ++makeExample('change-detection/ts/src/app/onpush/hero-manager.immutable.component.ts', 'add-hero')(format='.') + +.l-sub-section + :marked + There are specialized immutable collections libraries such as + [Immutable.js](https://facebook.github.io/immutable-js/) that aim to make this coding style more + natural and efficient than it is with native JavaScript arrays and objects. + +a(id="onpush-event") +:marked + #### `OnPush` with Event Bindings + + The `OnPush` strategy will also detect changes in the component when one of its + [event bindings](template-syntax.html#!#event-binding) gets executed. This means + any code that is executed using the `(event)="doSomething()"` syntax in the component's + template. + + For example, our earlier hero counter example will also work with the `OnPush` strategy, + even though its `heroCount` property is not an input. This is because the counter is + incremented and decremented as a response to click events, using `(click)` event bindings. + ++makeExample('change-detection/ts/src/app/onpush/hero-counter.onpush.component.ts')(format='.') + +a(id="onpush-manual") +:marked + #### `OnPush` with Explicit Change Detection Triggering + + If you switch the hero counter example to one that increments automatically using + a `setInterval`, you will see that changes are no longer detected! + ++makeExample('change-detection/ts/src/app/onpush/hero-counter-auto.broken.component.ts')(format='.') + +:marked + This is because neither of the requirement's for `OnPush` change detection apply: + The change is not coming from a component input, nor is it triggered by an event binding. + + In these cases, you can explicitly *tell the component's change detector that it should + be included in the next change detection*. You can do this by obtaining a reference to + the change detector by injecting a [ChangeDetectorRef](../api/core/index/ChangeDetectorRef-class.html), + and then calling its `markForCheck()` method when you want changes to be detected. + ++makeExample('change-detection/ts/src/app/onpush/hero-counter-auto.component.ts')(format='.') + +:marked + This strategy can be useful if you are integrating Angular with an evented data model + library such as Knockout, Backbone.js models, or Breeze. These libraries support callback + functions that always get invoked when the model has changed. + + You can combine such libraries with Angular's `OnPush` change detection strategy by calling + `markForCheck()` from those change callbacks. This way the component's change detector will never + get invoked unless the model library tells us something has changed. + ++makeExample('change-detection/ts/src/app/onpush/hero-name-badge-evented.component.ts')(format='.') + +.alert.is-helpful + :marked + Remember that if you used Angular's regular change detection instead of `OnPush`, changes would + still be detected without us having to subscribe to anything. You don't *have to* use the combination + of `OnPush` and `markForCheck()`. It is just an optimization strategy. + +a(id="detach") +:marked + ### Taking Control By Detaching Change Detectors + + We have seen how we can customize the behavior of Angular's change detection by switching the + change detection strategy between `Default` and `OnPush`. + +img(src="/resources/images/devguide/change-detection/change-detection-tree-detached.gif" alt="When a change detector is detached, it is not executed with the rest of the component tree" style="float: right; max-width: 400px; margin-left: 25px;") + +:marked + You can get even more control over when exactly change detection is performed by *detaching* a + component's change detector from the change detector tree. When you do this, the change detector + will never be called during Angular's regular change detection turn because it is simply not + connected to the rest of the component tree. + + Note that because change detectors are organized into a tree, detaching a change detector also + effectively detaches everything below it. None of the child components of this component will + have their changes detected either. + + What you can do instead is control these change detectors manually from your application code. There are + several things we can achive with this technique. + +a(id="detach-onetime") +:marked + #### One-Time Binding + + Sometimes you know that a component's data will never change as long as the + component exists. Running change detection on the component is entirely unnecessary + because there will never be changes. + + In these cases you can detach the component's change detector in the component's `ngAfterViewInit` + [lifecycle hook](lifecycle-hooks.html), which is called after the component's view has been rendered + for the first time. This is effectively a "one-time binding" strategy. The component's data bindings + are evaluated once right after the component is created and then never again. + ++makeExample('change-detection/ts/src/app/detach/hero-name-badge-detached.component.ts')(format='.') + +a(id="detach-reattach") +:marked + #### Temporarily Disabling Change Detection + + A detached change detector can also be *reattached* later. By combining `detach()` and `reattach()` + you can disable change detection *temporarily* for a time during which you don't need to keep the + component's UI up to date. + + For example, if you have data that changes with a high frequency, you can construct a component that + lets the user choose if they want a "live updating" view to the data or not. + ++makeExample('change-detection/ts/src/app/detach/hero-counter-live.component.ts')(format='.') + +a(id="detach-throttle") +:marked + #### Throttling Change Detection + + Another thing you can do with a detached change detector is to run it manually + at the time of your choosing. The `ChangeDetectorRef` class has a `detectChanges()` + method for exactly this purpose. + + For example, you can author a component that refreshes itself on the screen once + every second even though its data might change a hundred times per second. This + translates to less work done by the change detection system with the trade-off + of having slightly out-of-date information on the screen. + ++makeExample('change-detection/ts/src/app/detach/hero-counter-throttled.component.ts')(format='.') + +a(id="detach-flush") +:marked + #### Flushing Changes To The View Synchronously + + One additional thing to know about the `detectChanges()` method is that it runs change detection *immediately* + instead of waiting for the end of the current turn of the `NgZone`. + + For example, if you have an HTML form that you need to submit to a third-party service, and you + use data bindings within that form, you can use `detectChanges()` to ensure that the + form field values are up to date in the DOM before you submit. + ++makeExample('change-detection/ts/src/app/detach/hero-signature-form.component.ts')(format='.') + +:marked + If we did not call `detectChanges()` here, the `secret` field would not have the + freshly calculated secret in time for the form submission. + +a(id="markforcheck-vs-detectchanges") +:marked + ### The Difference between `markForCheck()` and `detectChanges()` + + We have seen two different methods for manually telling a change detector that it + should run: `markForCheck()` and `detectChanges()`. Although similar, these methods + have some important differences. + +table + colgroup + col(width = "50%") + col(width = "50%") + tr + th + :marked + `markForCheck()` + th + :marked + `detectChanges()` + tr + td + :marked + **Asynchronous**: Schedules change detection for this component to run during the next turn of the `NgZone`. + td + :marked + **Synchronous**: Performs change detection for this component *immediately*. + tr + td + :marked + **Upward**: Marks the current component and *all components up to the root* to be detected. + + Descendants may also be detected, but this depends on their own change detection strategy. + td + :marked + **Downward**: Only detects changes in the current component and its descendants. + tr + td + :marked + Most useful with change detectors that are **attached** but use the `OnPush` strategy. + Used to give a hint to Angular about changes inside the component. + + Does nothing when the change detector has been detached. + td + :marked + Most useful with **detached** change detectors, that never run otherwise. + + Can also be used with attached change detectors. + +a(id="escape-ngzone") +:marked + ### Skipping Change Detection For Asynchronous Work By Escaping The NgZone + + Sometimes you may have a need to skip change detection not for an entire component, + but for a specific asynchronous task that you want to perform. + + For example, you may have an Observable based workflow that comprises many asynchronous + tasks, but you are only interested in its final result: + ++makeExample('change-detection/ts/src/app/async-workflow.component.ts', 'inside-zone')(format='.') + +:marked + You know that you will only be interested in the final result of this workflow, and not + its intermediate steps. But because of the way zones work, Angular will still perform + change detection for the intermediate calculations because it cannot know when it is + safe to skip it. + + What you can do to skip these intermediate change detection executions is to *explicitly schedule + the work to be done outside the `NgZone`*, by using its `runOutsideAngular()` method. + That work and any asynchronous tasks that it includes will not trigger Angular change detection, + until we explicitly *re-enter the NgZone* at the end of the work, which we can do with the `run()` + method. + ++makeExample('change-detection/ts/src/app/async-workflow.component.ts', 'outside-zone')(format='.') + +a(id="codegen") +.l-main-section +:marked + ## Appendix: A Peek Into Change Detector Code Generation + + Before your Angular application starts, all the components inside it are processed by the Angular compiler. + As described in our [Ahead-of-Time compilation guide](../cookbook/aot-compiler.html), this may happen either + inside the browser just before the application starts ("JIT compilation"), or as a build step before the application + is shipped ("AoT compilation"). + + One of the main tasks of the compiler is to *generate the change detection code* for each component. It does so by + going through all the data binding expressions it finds in the component, and then by generating the JavaScript code + that will execute those expressions. + + We can peek into what the generated change detection code looks like by looking at the `*.ngfactory.js` + files that the compiler creates. These files can either be found on disk (for AoT compilation) or using browser + developer tools (for JIT compilation). In the latter case you can see them under the "Sources" tab of Chrome + Developer Tools. The change detection code will be inside the `detectChangesInternal` method of these generated + classes. + +figure.image-display + img(src="/resources/images/devguide/change-detection/jit-files.png" alt="The generated change detection code in Chrome Developer Tools") + +:marked + The way this code is laid out explains a lot about why Angular's change detection is so fast: + + * Data access is done using *non-computed property lookups* such as + "`self.context.heroCount`", as opposed to making reflective calls such as `self.context[attributeName]`. This + enables JavaScript engines to optimize the code at runtime using techniques likes *inline caching*. + * Instead of looping over expressions, a flat code structure is generated that just checks expressions + one after another. + + This generated code is, for the most part, the kind of code you would write manually to gain maximum + performance, if only it wasn't so tedious to write and maintain! Luckily Angular does it for you. + And it does it in a way that leaves very little runtime performance overhead caused by the framework. diff --git a/public/resources/images/devguide/change-detection/change-detection-in-zone.png b/public/resources/images/devguide/change-detection/change-detection-in-zone.png new file mode 100644 index 0000000000..081d74f66b Binary files /dev/null and b/public/resources/images/devguide/change-detection/change-detection-in-zone.png differ diff --git a/public/resources/images/devguide/change-detection/change-detection-sequence.gif b/public/resources/images/devguide/change-detection/change-detection-sequence.gif new file mode 100644 index 0000000000..f4719ad245 Binary files /dev/null and b/public/resources/images/devguide/change-detection/change-detection-sequence.gif differ diff --git a/public/resources/images/devguide/change-detection/change-detection-tree-detached.gif b/public/resources/images/devguide/change-detection/change-detection-tree-detached.gif new file mode 100644 index 0000000000..9065175a45 Binary files /dev/null and b/public/resources/images/devguide/change-detection/change-detection-tree-detached.gif differ diff --git a/public/resources/images/devguide/change-detection/change-detection-tree.gif b/public/resources/images/devguide/change-detection/change-detection-tree.gif new file mode 100644 index 0000000000..e761041b3d Binary files /dev/null and b/public/resources/images/devguide/change-detection/change-detection-tree.gif differ diff --git a/public/resources/images/devguide/change-detection/jit-files.png b/public/resources/images/devguide/change-detection/jit-files.png new file mode 100644 index 0000000000..c457ccf261 Binary files /dev/null and b/public/resources/images/devguide/change-detection/jit-files.png differ