Skip to content
This repository was archived by the owner on Dec 4, 2017. It is now read-only.

Commit 16e2f60

Browse files
committed
docs(change-detection): add change detection dev guide
1 parent 42d08b7 commit 16e2f60

31 files changed

+1243
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Component } from '@angular/core';
2+
import { Hero } from './hero.model';
3+
import { HeroModel } from './onpush/hero-evented.model';
4+
5+
@Component({
6+
moduleId: module.id,
7+
selector: 'hero-app',
8+
template: `
9+
<h1>Angular Change Detection</h1>
10+
11+
12+
<h2>Basic Example</h2>
13+
<hero-counter>
14+
</hero-counter>
15+
16+
<h2>Single-Pass</h2>
17+
18+
<h3>Broken Example</h3>
19+
<hero-name-badge-broken [hero]="anonymousHero">
20+
</hero-name-badge-broken>
21+
22+
<h3>Fixed Example</h3>
23+
<hero-name-badge [hero]="secondAnonymousHero">
24+
</hero-name-badge>
25+
26+
27+
<h2>OnPush</h2>
28+
29+
<h3>Immutable Primitive Values</h3>
30+
<p>OnPush only runs detection when inputs change.</p>
31+
<hero-search-result [searchResult]="'Windstorm'" [searchTerm]="'indsto'">
32+
</hero-search-result>
33+
34+
<h3>Mutable Collection, Broken Example</h3>
35+
<p>OnPush does not detect changes inside array inputs.</p>
36+
<hero-manager-mutable>
37+
</hero-manager-mutable>
38+
39+
<h3>Immutable Collection, Fixed Example</h3>
40+
<p>OnPush detects changes for array inputs as longs as they're treated as immutable values.</p>
41+
<hero-manager-immutable>
42+
</hero-manager-immutable>
43+
44+
<h3>Events</h3>
45+
<p>OnPush detects changes when they originate in an event handler.</p>
46+
<hero-counter-onpush>
47+
</hero-counter-onpush>
48+
49+
<h3>Explicit Change Marking, Broken Without</h3>
50+
<p>A counter incrementing with setTimeout() inside an OnPush component does not update.</p>
51+
<hero-counter-auto-broken>
52+
</hero-counter-auto-broken>
53+
54+
<h3>Explicit Change Marking</h3>
55+
<p>This is fixed using markForCheck()</p>
56+
<hero-counter-auto>
57+
</hero-counter-auto>
58+
59+
<h3>Explicit Change Marking with Library Callback</h3>
60+
<hero-name-badge-evented [hero]="heroModel">
61+
</hero-name-badge-evented>
62+
<button (click)="renameHeroModel()">Rename</button>
63+
64+
<h2>Detaching</h2>
65+
66+
<h3>Permanently, "One-Time Binding"</h3>
67+
<p>By detaching a component's change detector at ngOnInit() we can do "one-time binding".</p>
68+
<hero-name-badge-detached [hero]="hero">
69+
</hero-name-badge-detached>
70+
<button (click)="renameHero()">Rename</button>
71+
72+
<h3>Temporarily, reattach</h3>
73+
<p>By detaching/reattaching a change detector we can toggle whether a component has "live updates".</p>
74+
<hero-counter-live>
75+
</hero-counter-live>
76+
77+
<h3>Throttling with Internal detectChanges</h3>
78+
<p>
79+
By calling detectChanges() on a detached change detector we can choose when change detection is done.
80+
This can be used to update the view at a lower frequency than data changes.
81+
</p>
82+
<hero-counter-throttled>
83+
</hero-counter-throttled>
84+
85+
<h3>Flushing to DOM with Internal detectChanges</h3>
86+
<p>We can use detectChanges() to flush changes to the view immediately if we can't wait for the next turn of the zone.</p>
87+
<hero-signature-form>
88+
</hero-signature-form>
89+
90+
<h2>Escaping NgZone For Async Work</h2>
91+
92+
<h3>Without</h3>
93+
<p>Many unnecessary change detections will be performed for this workflow because it is all inside NgZone.</p>
94+
<hero-async-workflow></hero-async-workflow>
95+
`
96+
})
97+
export class AppComponent {
98+
hero: Hero = {name: 'Windstorm', onDuty: true};
99+
anonymousHero: Hero = {name: '', onDuty: false};
100+
secondAnonymousHero: Hero = {name: '', onDuty: false};
101+
102+
heroModel = new HeroModel('Windstorm');
103+
104+
renameHero() {
105+
this.hero.name = 'Magneta';
106+
}
107+
108+
renameHeroModel() {
109+
this.heroModel.setName('Magneta');
110+
}
111+
112+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { NgModule } from '@angular/core';
2+
import { BrowserModule } from '@angular/platform-browser';
3+
import { FormsModule } from '@angular/forms';
4+
5+
import { AppComponent } from './app.component';
6+
7+
import { HeroCounterComponent } from './hero-counter.component';
8+
import { HeroNameBadgeBrokenComponent } from './hero-name-badge.broken.component';
9+
import { HeroNameBadgeComponent } from './hero-name-badge.component';
10+
import { SearchResultComponent } from './onpush/search-result.component';
11+
import { HeroListComponent as HeroListOnpushComponent } from './onpush/hero-list.onpush.component';
12+
import { HeroManagerMutableComponent } from './onpush/hero-manager.mutable.component';
13+
import { HeroManagerImmutableComponent } from './onpush/hero-manager.immutable.component';
14+
import { HeroCounterComponent as HeroCounterOnPushComponent } from './onpush/hero-counter.onpush.component';
15+
import { HeroCounterAutoComponent as HeroCounterAutoBrokenComponent } from './onpush/hero-counter-auto.broken.component';
16+
import { HeroCounterAutoComponent } from './onpush/hero-counter-auto.component';
17+
import { HeroNameBadgeComponent as HeroNameBadgeEventedComponent } from './onpush/hero-name-badge-evented.component';
18+
import { HeroNameBadgeComponent as HeroNameBadgeDetachedComponent } from './detach/hero-name-badge-detached.component';
19+
import { HeroCounterComponent as HeroCounterLiveComponent } from './detach/hero-counter-live.component';
20+
import { HeroCounterComponent as HeroCounterThrottledComponent } from './detach/hero-counter-throttled.component';
21+
import { HeroSignatureFormComponent } from './detach/hero-signature-form.component';
22+
import { AsyncWorkflowComponent } from './async-workflow.component';
23+
24+
@NgModule({
25+
imports: [
26+
BrowserModule,
27+
FormsModule
28+
],
29+
declarations: [
30+
AppComponent,
31+
HeroCounterComponent,
32+
HeroNameBadgeBrokenComponent,
33+
HeroNameBadgeComponent,
34+
SearchResultComponent,
35+
HeroListOnpushComponent,
36+
HeroManagerMutableComponent,
37+
HeroManagerImmutableComponent,
38+
HeroCounterOnPushComponent,
39+
HeroCounterAutoBrokenComponent,
40+
HeroCounterAutoComponent,
41+
HeroNameBadgeEventedComponent,
42+
HeroNameBadgeDetachedComponent,
43+
HeroCounterLiveComponent,
44+
HeroCounterThrottledComponent,
45+
HeroSignatureFormComponent,
46+
AsyncWorkflowComponent
47+
],
48+
bootstrap: [
49+
AppComponent
50+
]
51+
})
52+
export class AppModule { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Component, DoCheck, NgZone } from '@angular/core';
2+
import { Observable } from 'rxjs/Observable';
3+
4+
import 'rxjs/add/observable/interval';
5+
import 'rxjs/add/observable/merge';
6+
import 'rxjs/add/operator/do';
7+
import 'rxjs/add/operator/map';
8+
import 'rxjs/add/operator/reduce';
9+
import 'rxjs/add/operator/take';
10+
11+
@Component({
12+
selector: 'hero-async-workflow',
13+
template: `
14+
<button (click)="loadInsideZone()">Start loading inside NgZone</button>
15+
<button (click)="loadOutsideZone()">Start loading outside NgZone</button>
16+
Results:
17+
<ul>
18+
<li *ngFor="let itm of results">
19+
{{ itm }}
20+
</li>
21+
</ul>
22+
`
23+
})
24+
export class AsyncWorkflowComponent implements DoCheck {
25+
results: string[];
26+
27+
// #docregion outside-zone
28+
constructor(private ngZone: NgZone) { }
29+
// #enddocregion outside-zone
30+
31+
// #docregion inside-zone
32+
loadInsideZone() {
33+
Observable.merge(loadHeroes(), loadMoreHeroes(), loadEvenMoreHeroes())
34+
.reduce((heroes, hero) => [...heroes, hero], [])
35+
.subscribe(heroes => this.results = heroes);
36+
}
37+
// #enddocregion inside-zone
38+
39+
// #docregion outside-zone
40+
loadOutsideZone() {
41+
// Escape NgZone before starting work.
42+
// No change detection will be performed during this work.
43+
this.ngZone.runOutsideAngular(() => {
44+
Observable.merge(loadHeroes(), loadMoreHeroes(), loadEvenMoreHeroes())
45+
.reduce((heroes, hero) => [...heroes, hero], [])
46+
.subscribe(heroes => {
47+
// Re-enter zone to process final result.
48+
// Change detection will be performed.
49+
this.ngZone.run(() => this.results = heroes);
50+
});
51+
});
52+
}
53+
// #enddocregion outside-zone
54+
55+
ngDoCheck() {
56+
console.log('cd');
57+
}
58+
59+
}
60+
61+
function loadHeroes() {
62+
return Observable.interval(100)
63+
.map(n => `hero a${n}`)
64+
.do(v => console.log(v))
65+
.take(3);
66+
}
67+
68+
function loadMoreHeroes() {
69+
return Observable.interval(150)
70+
.map(n => `hero b${n}`)
71+
.do(v => console.log(v))
72+
.take(3)
73+
}
74+
75+
function loadEvenMoreHeroes() {
76+
return Observable.interval(200)
77+
.map(n => `hero c${n}`)
78+
.do(v => console.log(v))
79+
.take(3)
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
2+
3+
// #docregion
4+
@Component({
5+
selector: 'hero-counter-live',
6+
template: `
7+
Number of heroes: {{ heroCount }}
8+
<button (click)="toggleLive()">Toggle live update</button>
9+
`
10+
})
11+
export class HeroCounterComponent implements OnInit, OnDestroy {
12+
heroCount = 5;
13+
private live = true;
14+
private updateIntervalId: any;
15+
16+
constructor(private changeDetector: ChangeDetectorRef) { }
17+
18+
ngOnInit() {
19+
// Increment counter ten times per second
20+
this.updateIntervalId = setInterval(() => this.heroCount++, 100);
21+
}
22+
23+
ngOnDestroy() {
24+
clearInterval(this.updateIntervalId);
25+
}
26+
27+
toggleLive() {
28+
this.live = !this.live;
29+
if (this.live) {
30+
this.changeDetector.reattach();
31+
} else {
32+
this.changeDetector.detach();
33+
}
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
2+
3+
// #docregion
4+
@Component({
5+
selector: 'hero-counter-throttled',
6+
template: `
7+
Number of heroes: {{ heroCount }}
8+
`
9+
})
10+
export class HeroCounterComponent implements OnInit, OnDestroy {
11+
heroCount = 5;
12+
13+
private dataUpdateIntervalId: any;
14+
private viewUpdateIntervalId: any;
15+
16+
constructor(private changeDetector: ChangeDetectorRef) { }
17+
18+
ngOnInit() {
19+
// Detach the change detector so it never runs unless we do it manually.
20+
this.changeDetector.detach();
21+
// Change data a hundred times per second...
22+
this.dataUpdateIntervalId = setInterval(() => this.heroCount++, 10);
23+
// ...but detect changes only once per second
24+
this.viewUpdateIntervalId = setInterval(() => this.changeDetector.detectChanges(), 1000);
25+
}
26+
27+
ngOnDestroy() {
28+
clearInterval(this.dataUpdateIntervalId);
29+
clearInterval(this.viewUpdateIntervalId);
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ChangeDetectorRef, Component, Input, AfterViewInit } from '@angular/core';
2+
import { Hero } from '../hero.model';
3+
4+
// #docregion
5+
@Component({
6+
selector: 'hero-name-badge-detached',
7+
template: `
8+
<h4>{{ hero.name }} details</h4>
9+
<p>Name: {{ hero.name }}</p>
10+
`
11+
})
12+
export class HeroNameBadgeComponent implements AfterViewInit {
13+
@Input() hero: Hero;
14+
15+
constructor(private changeDetector: ChangeDetectorRef) { }
16+
17+
ngAfterViewInit() {
18+
this.changeDetector.detach();
19+
}
20+
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
2+
3+
// #docregion
4+
@Component({
5+
selector: 'hero-signature-form',
6+
template: `
7+
<form #signatureForm method="POST" action="/sign" >
8+
<input type="text" name="username" [(ngModel)]="username" />
9+
<input type="hidden" name="secret" [value]="secret" />
10+
<button (click)="sendForm()">Submit</button>
11+
</form>
12+
`
13+
})
14+
export class HeroSignatureFormComponent {
15+
@ViewChild('signatureForm') signatureForm: ElementRef;
16+
17+
username: string;
18+
secret: string;
19+
20+
constructor(private changeDetector: ChangeDetectorRef) { }
21+
22+
sendForm() {
23+
this.secret = calculateSecret(this.username);
24+
// Ensure the secret is flushed into the form field before we submit.
25+
this.changeDetector.detectChanges();
26+
this.signatureForm.nativeElement.submit();
27+
}
28+
29+
}
30+
// #enddocregion
31+
32+
function calculateSecret(username: string) {
33+
return `SECRET FOR ${username}`;
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// #docregion
2+
import { Component } from '@angular/core';
3+
4+
@Component({
5+
selector: 'hero-counter',
6+
template: `
7+
Number of heroes:
8+
<button (click)="decrease()">-</button>
9+
{{ heroCount }} <!-- This expression always evaluates to the latest value -->
10+
<button (click)="increase()">+</button>
11+
`
12+
})
13+
export class HeroCounterComponent {
14+
heroCount = 5;
15+
16+
// When we change data, we don't need to do anything to update the view.
17+
increase() { this.heroCount = this.heroCount + 1; }
18+
decrease() { this.heroCount = Math.max(this.heroCount - 1, 0); }
19+
20+
}

0 commit comments

Comments
 (0)