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

Commit e1071c7

Browse files
committed
docs(change-detection): add change detection dev guide
1 parent 4f067ab commit e1071c7

33 files changed

+1436
-0
lines changed

Diff for: public/docs/_examples/change-detection/e2e-spec.ts

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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) {
175+
differences++;
176+
}
177+
if (t2 !== t3) {
178+
differences++;
179+
}
180+
expect(differences).toBeLessThan(2);
181+
});
182+
});
183+
184+
});
185+
186+
187+
188+
189+
});
190+
191+
});

Diff for: 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 } from './onpush/hero-counter-auto.component';
16+
import { HeroCounterAutoComponent as HeroCounterAutoBrokenComponent } from './onpush/hero-counter-auto.broken.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 { }

0 commit comments

Comments
 (0)