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

Commit

Permalink
docs(change-detection): add change detection dev guide
Browse files Browse the repository at this point in the history
  • Loading branch information
teropa committed Mar 7, 2017
1 parent 4f067ab commit b50a8d0
Show file tree
Hide file tree
Showing 33 changed files with 1,432 additions and 0 deletions.
187 changes: 187 additions & 0 deletions public/docs/_examples/change-detection/e2e-spec.ts
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 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 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 { }
Loading

0 comments on commit b50a8d0

Please sign in to comment.