This repository has been archived by the owner on Dec 4, 2017. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 879
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs(change-detection): add change detection dev guide
- Loading branch information
Showing
33 changed files
with
1,432 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
'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); | ||
}); | ||
}); | ||
|
||
}); | ||
|
||
|
||
|
||
|
||
}); | ||
|
||
}); |
Empty file.
115 changes: 115 additions & 0 deletions
115
public/docs/_examples/change-detection/ts/src/app/app.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: ` | ||
<h1>Angular Change Detection</h1> | ||
<h2>Basic Example</h2> | ||
<hero-counter> | ||
</hero-counter> | ||
<h2>Single-Pass</h2> | ||
<h3>Broken Example</h3> | ||
<hero-name-badge-broken [hero]="anonymousHero"> | ||
</hero-name-badge-broken> | ||
<h3>Fixed Example</h3> | ||
<hero-name-badge [hero]="secondAnonymousHero"> | ||
</hero-name-badge> | ||
<h2>OnPush</h2> | ||
<h3>Immutable Primitive Values</h3> | ||
<p>OnPush only runs detection when inputs change.</p> | ||
<hero-search-result [searchResult]="'Windstorm'" [searchTerm]="'indsto'"> | ||
</hero-search-result> | ||
<h3>Mutable Collection, Broken Example</h3> | ||
<p>OnPush does not detect changes inside array inputs.</p> | ||
<hero-manager-mutable> | ||
</hero-manager-mutable> | ||
<h3>Immutable Collection, Fixed Example</h3> | ||
<p>OnPush detects changes for array inputs as longs as they're treated as immutable values.</p> | ||
<hero-manager-immutable> | ||
</hero-manager-immutable> | ||
<h3>Events</h3> | ||
<p>OnPush detects changes when they originate in an event handler.</p> | ||
<hero-counter-onpush> | ||
</hero-counter-onpush> | ||
<h3>Explicit Change Marking, Broken Without</h3> | ||
<p>A counter incrementing with setTimeout() inside an OnPush component does not update.</p> | ||
<hero-counter-auto-broken> | ||
</hero-counter-auto-broken> | ||
<h3>Explicit Change Marking</h3> | ||
<p>This is fixed using markForCheck()</p> | ||
<hero-counter-auto> | ||
</hero-counter-auto> | ||
<h3>Explicit Change Marking with Library Callback</h3> | ||
<hero-name-badge-evented [hero]="heroModel"> | ||
</hero-name-badge-evented> | ||
<button (click)="renameHeroModel()">Rename</button> | ||
<h2>Detaching</h2> | ||
<h3>Permanently, "One-Time Binding"</h3> | ||
<p>By detaching a component's change detector at ngOnInit() we can do "one-time binding".</p> | ||
<hero-name-badge-detached [hero]="hero"> | ||
</hero-name-badge-detached> | ||
<button (click)="renameHero()">Rename detached</button> | ||
<h3>Temporarily, reattach</h3> | ||
<p>By detaching/reattaching a change detector we can toggle whether a component has "live updates".</p> | ||
<hero-counter-live> | ||
</hero-counter-live> | ||
<h3>Throttling with Internal detectChanges</h3> | ||
<p> | ||
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. | ||
</p> | ||
<hero-counter-throttled> | ||
</hero-counter-throttled> | ||
<h3>Flushing to DOM with Internal detectChanges</h3> | ||
<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> | ||
<hero-signature-form> | ||
</hero-signature-form> | ||
<h2>Escaping NgZone For Async Work</h2> | ||
<h3>Without</h3> | ||
<p>Many unnecessary change detections will be performed for this workflow because it is all inside NgZone.</p> | ||
<hero-async-workflow></hero-async-workflow> | ||
` | ||
}) | ||
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'); | ||
} | ||
|
||
} |
52 changes: 52 additions & 0 deletions
52
public/docs/_examples/change-detection/ts/src/app/app.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 as HeroCounterAutoBrokenComponent } from './onpush/hero-counter-auto.broken.component'; | ||
import { HeroCounterAutoComponent } from './onpush/hero-counter-auto.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 { } |
Oops, something went wrong.