Skip to content
This repository was archived by the owner on Dec 4, 2017. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b50a8d0

Browse files
committedMar 7, 2017
docs(change-detection): add change detection dev guide
1 parent 4f067ab commit b50a8d0

33 files changed

+1432
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
'use strict'; // necessary for es6 output in node
2+
3+
import { browser, element, by, ExpectedConditions as EC } from 'protractor';
4+
5+
describe('Change Detection guide', () => {
6+
7+
beforeEach(() => {
8+
9+
// setInterval() used in the code makes Protractor mistakenly think we're not
10+
// finished loading unless we turn this on.
11+
browser.ignoreSynchronization = true;
12+
13+
browser.get('/');
14+
return browser.wait(EC.presenceOf(element(by.css('[ng-version]'))));
15+
});
16+
17+
describe('Basic Example', () => {
18+
19+
it('displays a counter that can be incremented and decremented', () => {
20+
const component = element(by.tagName('hero-counter'));
21+
const counter = component.element(by.css('span'));
22+
23+
expect(counter.getText()).toEqual('5');
24+
25+
component.element(by.buttonText('+')).click();
26+
expect(counter.getText()).toEqual('6');
27+
component.element(by.buttonText('-')).click();
28+
expect(counter.getText()).toEqual('5');
29+
});
30+
31+
});
32+
33+
describe('Broken name badge example', () => {
34+
35+
it('causes an error', () => {
36+
const errorDump = element(by.id('bootstrapError'));
37+
expect(errorDump.getText()).toContain('HeroNameBadgeBrokenComponent');
38+
expect(errorDump.getText()).toContain('Expression has changed after it was checked');
39+
});
40+
41+
it('still displays the bound data', () => {
42+
const component = element(by.tagName('hero-name-badge-broken'));
43+
expect(component.element(by.css('h4')).getText()).toEqual('Anonymous details');
44+
});
45+
46+
});
47+
48+
describe('Fixed name badge example', () => {
49+
50+
it('displays the bound data', () => {
51+
const component = element(by.tagName('hero-name-badge'));
52+
expect(component.element(by.css('h4')).getText()).toEqual('details');
53+
expect(component.element(by.css('p')).getText()).toEqual('Name: Anonymous');
54+
});
55+
56+
});
57+
58+
describe('OnPush', () => {
59+
60+
describe('with immutable string inputs', () => {
61+
62+
it('displays the bound data', () => {
63+
const component = element(by.tagName('hero-search-result'));
64+
const match = component.element(by.css('.match'));
65+
expect(match.getText()).toEqual('indsto');
66+
});
67+
68+
});
69+
70+
describe('with input mutations', () => {
71+
72+
it('does not display the mutations', () => {
73+
const component = element(by.tagName('hero-manager-mutable'));
74+
75+
expect(component.element(by.cssContainingText('li', 'Windstorm')).isPresent()).toBe(true);
76+
expect(component.element(by.cssContainingText('li', 'Magneta')).isPresent()).toBe(true);
77+
component.element(by.buttonText('Add one more')).click();
78+
expect(component.element(by.cssContainingText('li', 'Bombasto')).isPresent()).toBe(false);
79+
80+
});
81+
82+
});
83+
84+
describe('with immutable array input', () => {
85+
86+
it('displays the changes', () => {
87+
const component = element(by.tagName('hero-manager-immutable'));
88+
89+
expect(component.element(by.cssContainingText('li', 'Windstorm')).isPresent()).toBe(true);
90+
expect(component.element(by.cssContainingText('li', 'Magneta')).isPresent()).toBe(true);
91+
component.element(by.buttonText('Add one more')).click();
92+
expect(component.element(by.cssContainingText('li', 'Bombasto')).isPresent()).toBe(true);
93+
94+
});
95+
96+
});
97+
98+
describe('with events', () => {
99+
100+
it('displays the changes', () => {
101+
const component = element(by.tagName('hero-counter-onpush'));
102+
const counter = component.element(by.css('span'));
103+
104+
expect(counter.getText()).toEqual('5');
105+
106+
component.element(by.buttonText('+')).click();
107+
expect(counter.getText()).toEqual('6');
108+
component.element(by.buttonText('-')).click();
109+
expect(counter.getText()).toEqual('5');
110+
});
111+
112+
});
113+
114+
describe('with explicit markForDetection()', () => {
115+
116+
it('does not detect setInterval() when not used', () => {
117+
const component = element(by.tagName('hero-counter-auto-broken'));
118+
browser.sleep(300); // There's an interval of 100ms inside the component.
119+
expect(component.getText()).toEqual('Number of heroes: 5');
120+
});
121+
122+
it('does detect setInterval() when used', () => {
123+
const component = element(by.tagName('hero-counter-auto'));
124+
browser.sleep(300); // There's an interval of 100ms inside the component.
125+
expect(component.getText()).not.toEqual('Number of heroes: 5');
126+
expect(component.getText()).toMatch(/Number of heroes: \d+/);
127+
});
128+
129+
it('detects on evented library callbacks', () => {
130+
const component = element(by.tagName('hero-name-badge-evented'));
131+
expect(component.element(by.cssContainingText('h4', 'Windstorm details')).isPresent()).toBe(true);
132+
element(by.buttonText('Rename')).click();
133+
expect(component.element(by.cssContainingText('h4', 'Magneta details')).isPresent()).toBe(true);
134+
});
135+
136+
});
137+
138+
describe('detached', () => {
139+
140+
it('does not pick up changes automatically', () => {
141+
const component = element(by.tagName('hero-name-badge-detached'));
142+
expect(component.element(by.css('h4')).getText()).toEqual('Windstorm details');
143+
element(by.buttonText('Rename detached')).click();
144+
expect(component.element(by.css('h4')).getText()).toEqual('Windstorm details');
145+
});
146+
147+
it('starts picking up changes again when reattached', () => {
148+
const component = element(by.tagName('hero-counter-live'));
149+
const count = component.element(by.css('.count'));
150+
151+
const text1 = count.getText();
152+
browser.sleep(100);
153+
component.element(by.buttonText('Toggle live update')).click();
154+
const text2 = count.getText();
155+
browser.sleep(100);
156+
const text3 = count.getText();
157+
158+
expect(text1).not.toEqual(text2);
159+
expect(text2).toEqual(text3);
160+
});
161+
162+
it('can be used for throttling by explicitly detecting with an interval', () => {
163+
const component = element(by.tagName('hero-counter-throttled'));
164+
const count = component.element(by.css('.count'));
165+
166+
const text1 = count.getText();
167+
browser.sleep(100);
168+
const text2 = count.getText();
169+
browser.sleep(100);
170+
const text3 = count.getText();
171+
172+
Promise.all([text1, text2, text3]).then(([t1, t2, t3]) => {
173+
let differences = 0;
174+
if (t1 !== t2) differences++;
175+
if (t2 !== t3) differences++;
176+
expect(differences).toBeLessThan(2);
177+
});
178+
});
179+
180+
});
181+
182+
183+
184+
185+
});
186+
187+
});

‎public/docs/_examples/change-detection/ts/example-config.json

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
35+
<h3>Mutable Collection, Broken Example</h3>
36+
<p>OnPush does not detect changes inside array inputs.</p>
37+
<hero-manager-mutable>
38+
</hero-manager-mutable>
39+
40+
<h3>Immutable Collection, Fixed Example</h3>
41+
<p>OnPush detects changes for array inputs as longs as they're treated as immutable values.</p>
42+
<hero-manager-immutable>
43+
</hero-manager-immutable>
44+
45+
<h3>Events</h3>
46+
<p>OnPush detects changes when they originate in an event handler.</p>
47+
<hero-counter-onpush>
48+
</hero-counter-onpush>
49+
50+
51+
<h3>Explicit Change Marking, Broken Without</h3>
52+
<p>A counter incrementing with setTimeout() inside an OnPush component does not update.</p>
53+
<hero-counter-auto-broken>
54+
</hero-counter-auto-broken>
55+
56+
<h3>Explicit Change Marking</h3>
57+
<p>This is fixed using markForCheck()</p>
58+
<hero-counter-auto>
59+
</hero-counter-auto>
60+
61+
<h3>Explicit Change Marking with Library Callback</h3>
62+
<hero-name-badge-evented [hero]="heroModel">
63+
</hero-name-badge-evented>
64+
<button (click)="renameHeroModel()">Rename</button>
65+
66+
67+
<h2>Detaching</h2>
68+
69+
<h3>Permanently, "One-Time Binding"</h3>
70+
<p>By detaching a component's change detector at ngOnInit() we can do "one-time binding".</p>
71+
<hero-name-badge-detached [hero]="hero">
72+
</hero-name-badge-detached>
73+
<button (click)="renameHero()">Rename detached</button>
74+
75+
<h3>Temporarily, reattach</h3>
76+
<p>By detaching/reattaching a change detector we can toggle whether a component has "live updates".</p>
77+
<hero-counter-live>
78+
</hero-counter-live>
79+
80+
<h3>Throttling with Internal detectChanges</h3>
81+
<p>
82+
By calling detectChanges() on a detached change detector we can choose when change detection is done.
83+
This can be used to update the view at a lower frequency than data changes.
84+
</p>
85+
<hero-counter-throttled>
86+
</hero-counter-throttled>
87+
88+
<h3>Flushing to DOM with Internal detectChanges</h3>
89+
<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>
90+
<hero-signature-form>
91+
</hero-signature-form>
92+
93+
<h2>Escaping NgZone For Async Work</h2>
94+
95+
<h3>Without</h3>
96+
<p>Many unnecessary change detections will be performed for this workflow because it is all inside NgZone.</p>
97+
<hero-async-workflow></hero-async-workflow>
98+
`
99+
})
100+
export class AppComponent {
101+
hero: Hero = {name: 'Windstorm', onDuty: true};
102+
anonymousHero: Hero = {name: '', onDuty: false};
103+
secondAnonymousHero: Hero = {name: '', onDuty: false};
104+
105+
heroModel = new HeroModel('Windstorm');
106+
107+
renameHero() {
108+
this.hero.name = 'Magneta';
109+
}
110+
111+
renameHeroModel() {
112+
this.heroModel.setName('Magneta');
113+
}
114+
115+
}
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,73 @@
1+
import { Component, 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 {
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+
}
56+
57+
function loadHeroes() {
58+
return Observable.interval(100)
59+
.map(n => `hero a${n}`)
60+
.take(3);
61+
}
62+
63+
function loadMoreHeroes() {
64+
return Observable.interval(150)
65+
.map(n => `hero b${n}`)
66+
.take(3);
67+
}
68+
69+
function loadEvenMoreHeroes() {
70+
return Observable.interval(200)
71+
.map(n => `hero c${n}`)
72+
.take(3);
73+
}
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: <span class="count">{{ heroCount }}</span>
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 { AfterViewInit, ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
2+
3+
// #docregion
4+
@Component({
5+
selector: 'hero-counter-throttled',
6+
template: `
7+
Number of heroes: <span class="count">{{ heroCount }}</span>
8+
`
9+
})
10+
export class HeroCounterComponent implements AfterViewInit, OnDestroy {
11+
heroCount = 5;
12+
13+
private dataUpdateIntervalId: any;
14+
private viewUpdateIntervalId: any;
15+
16+
constructor(private changeDetector: ChangeDetectorRef) { }
17+
18+
ngAfterViewInit() {
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+
<span class="count">{{ heroCount }}</span> <!-- 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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Component, Input } from '@angular/core';
2+
import { Hero } from './hero.model';
3+
4+
// #docregion
5+
@Component({
6+
selector: 'hero-name-badge-broken',
7+
template: `
8+
<h4>{{ hero.name }} details</h4>
9+
<p>Name: {{ getDisplayName() }}</p>
10+
`
11+
})
12+
export class HeroNameBadgeBrokenComponent {
13+
@Input() hero: Hero;
14+
15+
getDisplayName() {
16+
if (!this.hero.name || this.hero.name.length === 0) {
17+
// We're setting the name during change detection.
18+
// This may cause errors in other bindings that refer to the same data!
19+
this.hero.name = 'Anonymous';
20+
}
21+
return this.hero.name;
22+
}
23+
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Component, Input } from '@angular/core';
2+
import { Hero } from './hero.model';
3+
4+
// #docregion
5+
@Component({
6+
selector: 'hero-name-badge',
7+
template: `
8+
<h4>{{ hero.name }} details</h4>
9+
<p>Name: {{ getDisplayName() }}</p>
10+
`
11+
})
12+
export class HeroNameBadgeComponent {
13+
@Input() hero: Hero;
14+
15+
getDisplayName() {
16+
if (!this.hero.name || this.hero.name.length === 0) {
17+
// Here we just return the value we want to see in the view,
18+
// without mutating anything.
19+
return 'Anonymous';
20+
} else {
21+
return this.hero.name;
22+
}
23+
}
24+
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface Hero {
2+
name: string,
3+
onDuty: boolean
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
2+
import { AppModule } from './app.module';
3+
4+
platformBrowserDynamic().bootstrapModule(AppModule);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
OnDestroy,
5+
OnInit
6+
} from '@angular/core';
7+
8+
// #docregion
9+
@Component({
10+
selector: 'hero-counter-auto-broken',
11+
template: `
12+
Number of heroes: {{ heroCount }}
13+
`,
14+
changeDetection: ChangeDetectionStrategy.OnPush
15+
})
16+
export class HeroCounterAutoComponent implements OnInit, OnDestroy {
17+
heroCount = 5;
18+
private updateIntervalId: any;
19+
20+
ngOnInit() {
21+
// Changes made in the interval loop will not be detected!
22+
this.updateIntervalId = setInterval(() => this.heroCount++, 100);
23+
}
24+
25+
ngOnDestroy() {
26+
//clearInterval(this.updateIntervalId);
27+
}
28+
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
ChangeDetectorRef,
4+
Component,
5+
OnDestroy,
6+
OnInit
7+
} from '@angular/core';
8+
9+
// #docregion
10+
@Component({
11+
selector: 'hero-counter-auto',
12+
template: `
13+
Number of heroes: {{ heroCount }}
14+
`,
15+
changeDetection: ChangeDetectionStrategy.OnPush
16+
})
17+
export class HeroCounterAutoComponent implements OnInit, OnDestroy {
18+
heroCount = 5;
19+
private updateIntervalId: any;
20+
21+
constructor(private changeDetector: ChangeDetectorRef) { }
22+
23+
ngOnInit() {
24+
this.updateIntervalId = setInterval(() => {
25+
this.heroCount++;
26+
this.changeDetector.markForCheck();
27+
}, 100);
28+
}
29+
30+
ngOnDestroy() {
31+
clearInterval(this.updateIntervalId);
32+
}
33+
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// #docregion
2+
import { ChangeDetectionStrategy, Component } from '@angular/core';
3+
4+
@Component({
5+
selector: 'hero-counter-onpush',
6+
template: `
7+
Number of heroes:
8+
<button (click)="decrease()">-</button>
9+
<span>{{ heroCount }}</span>
10+
<button (click)="increase()">+</button>
11+
`,
12+
changeDetection: ChangeDetectionStrategy.OnPush
13+
})
14+
export class HeroCounterComponent {
15+
heroCount = 5;
16+
17+
increase() { this.heroCount = this.heroCount + 1; }
18+
decrease() { this.heroCount = Math.max(this.heroCount - 1, 0); }
19+
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* A faux "evented model" class that emulates the kind of patterns
3+
* used by libraries like Knockout, Backbone, Breeze.
4+
*/
5+
export class HeroModel {
6+
private changeListeners: (() => void)[] = [];
7+
8+
constructor(private name: string) { }
9+
10+
getName() {
11+
return this.name;
12+
}
13+
14+
setName(newName: string) {
15+
this.name = newName;
16+
for (let changeListener of this.changeListeners) {
17+
changeListener();
18+
}
19+
}
20+
21+
subscribeToChanges(listener: () => void) {
22+
this.changeListeners.push(listener);
23+
return () => {
24+
const idx = this.changeListeners.indexOf(listener);
25+
if (idx >= 0) {
26+
this.changeListeners.splice(idx, 1);
27+
}
28+
}
29+
}
30+
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2+
import { Hero } from '../hero.model';
3+
4+
// #docregion
5+
@Component({
6+
selector: 'hero-list-onpush',
7+
template: `
8+
<ul>
9+
<li *ngFor="let hero of heroes">
10+
{{ hero.name }}
11+
</li>
12+
</ul>
13+
`,
14+
changeDetection: ChangeDetectionStrategy.OnPush
15+
})
16+
export class HeroListComponent {
17+
@Input() heroes: Hero[];
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Component } from '@angular/core';
2+
import { Hero } from '../hero.model';
3+
4+
@Component({
5+
moduleId: module.id,
6+
selector: 'hero-manager-immutable',
7+
template: `
8+
<hero-list-onpush [heroes]="heroes">
9+
</hero-list-onpush>
10+
<button (click)="addHero()">Add one more</button>
11+
`
12+
})
13+
export class HeroManagerImmutableComponent {
14+
heroes: Hero[] = [
15+
{name: 'Windstorm', onDuty: true},
16+
{name: 'Magneta', onDuty: false}
17+
];
18+
19+
// #docregion add-hero
20+
addHero() {
21+
this.heroes = [...this.heroes, {name: 'Bombasto', onDuty: true}];
22+
}
23+
// #enddocregion add-hero
24+
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Component } from '@angular/core';
2+
import { Hero } from '../hero.model';
3+
4+
@Component({
5+
moduleId: module.id,
6+
selector: 'hero-manager-mutable',
7+
template: `
8+
<hero-list-onpush [heroes]="heroes">
9+
</hero-list-onpush>
10+
<button (click)="addHero()">Add one more</button>
11+
`
12+
})
13+
export class HeroManagerMutableComponent {
14+
heroes: Hero[] = [
15+
{name: 'Windstorm', onDuty: true},
16+
{name: 'Magneta', onDuty: false}
17+
];
18+
19+
// #docregion add-hero
20+
addHero() {
21+
// This will not be detected by the child component with OnPush!
22+
this.heroes.push({name: 'Bombasto', onDuty: true});
23+
}
24+
// #enddocregion add-hero
25+
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
ChangeDetectorRef,
4+
Component,
5+
Input,
6+
OnDestroy,
7+
OnInit
8+
} from '@angular/core';
9+
import { HeroModel } from './hero-evented.model';
10+
11+
// #docregion
12+
@Component({
13+
selector: 'hero-name-badge-evented',
14+
template: `
15+
<h4>{{ hero.getName() }} details</h4>
16+
<p>Name: {{ hero.getName() }}</p>
17+
`,
18+
changeDetection: ChangeDetectionStrategy.OnPush
19+
})
20+
export class HeroNameBadgeComponent implements OnInit, OnDestroy {
21+
@Input() hero: HeroModel;
22+
private unsubscribe: () => void;
23+
24+
constructor(private changeDetector: ChangeDetectorRef) { }
25+
26+
ngOnInit() {
27+
// Subscribe to changes in the hero model and run the component's change
28+
// detector every time we get notified.
29+
this.unsubscribe =
30+
this.hero.subscribeToChanges(() => this.changeDetector.markForCheck());
31+
}
32+
33+
ngOnDestroy() {
34+
this.unsubscribe();
35+
}
36+
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// #docregion
2+
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
3+
4+
@Component({
5+
selector: 'hero-search-result',
6+
template: `
7+
{{ getPrefix() }}<span class="match">{{ getMatch() }}</span>{{ getSuffix() }}
8+
`,
9+
styles: [`.match { background-color: yellow; }`],
10+
changeDetection: ChangeDetectionStrategy.OnPush
11+
})
12+
export class SearchResultComponent {
13+
@Input() searchResult: string;
14+
@Input() searchTerm: string;
15+
16+
getPrefix() {
17+
let matchIdx = this.getSearchTermMatchIndex();
18+
return this.searchResult.substring(0, matchIdx);
19+
}
20+
21+
getMatch() {
22+
let matchIdx = this.getSearchTermMatchIndex();
23+
return this.searchResult.substring(matchIdx, matchIdx + this.searchTerm.length);
24+
}
25+
26+
getSuffix() {
27+
let matchIdx = this.getSearchTermMatchIndex();
28+
return this.searchResult.substring(matchIdx + this.searchTerm.length);
29+
}
30+
31+
private getSearchTermMatchIndex() {
32+
return this.searchResult.toLowerCase().indexOf(this.searchTerm.toLowerCase());
33+
}
34+
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!DOCTYPE html>
2+
<!-- #docregion -->
3+
<html>
4+
<head>
5+
<title>Angular Change Detection</title>
6+
<meta charset="UTF-8">
7+
<meta name="viewport" content="width=device-width, initial-scale=1">
8+
<link rel="stylesheet" href="styles.css">
9+
<link rel="stylesheet" href="sample.css">
10+
11+
<!-- Polyfill(s) for older browsers -->
12+
<script src="node_modules/core-js/client/shim.min.js"></script>
13+
14+
<script src="node_modules/zone.js/dist/zone.js"></script>
15+
<script src="node_modules/systemjs/dist/system.src.js"></script>
16+
17+
<script src="systemjs.config.js"></script>
18+
<script>
19+
System.import('main.js').catch(function(err){ console.error(err); });
20+
</script>
21+
</head>
22+
23+
<body>
24+
<hero-app>Loading...</hero-app>
25+
26+
<!-- Expected errors will be appended here for Protractor test inspection -->
27+
<h2>Bootstrap error(s):</h2>
28+
<div id="bootstrapError"></div>
29+
</body>
30+
31+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// #docregion
2+
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3+
import { AppModule } from './app/app.module';
4+
5+
platformBrowserDynamic()
6+
.bootstrapModule(AppModule)
7+
.catch(e => document.querySelector('#bootstrapError').textContent = e);

‎public/docs/_examples/change-detection/ts/src/sample.css

Whitespace-only changes.

‎public/docs/ts/latest/guide/_data.json

+5
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@
116116
"intro": "Browser support and polyfills guide."
117117
},
118118

119+
"change-detection": {
120+
"title": "Change Detection",
121+
"intro": "Learn how Angular detects changes in your data to make sure the UI is always up to date."
122+
},
123+
119124
"component-styles": {
120125
"title": "Component Styles",
121126
"intro": "Learn how to apply CSS styles to components."

‎public/docs/ts/latest/guide/change-detection.jade

+508
Large diffs are not rendered by default.
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)
This repository has been archived.