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

docs(change-detection): add change detection dev guide #2858

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions public/docs/_examples/change-detection/e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

});




});

});
115 changes: 115 additions & 0 deletions public/docs/_examples/change-detection/ts/src/app/app.component.ts
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 public/docs/_examples/change-detection/ts/src/app/app.module.ts
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 } 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 { }
Loading