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 Jan 11, 2017
1 parent 42d08b7 commit 16e2f60
Show file tree
Hide file tree
Showing 31 changed files with 1,243 additions and 0 deletions.
112 changes: 112 additions & 0 deletions public/docs/_examples/change-detection/ts/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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</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/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 { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Component, DoCheck, NgZone } from '@angular/core';
import { Observable } from 'rxjs/Observable';

import 'rxjs/add/observable/interval';
import 'rxjs/add/observable/merge';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/reduce';
import 'rxjs/add/operator/take';

@Component({
selector: 'hero-async-workflow',
template: `
<button (click)="loadInsideZone()">Start loading inside NgZone</button>
<button (click)="loadOutsideZone()">Start loading outside NgZone</button>
Results:
<ul>
<li *ngFor="let itm of results">
{{ itm }}
</li>
</ul>
`
})
export class AsyncWorkflowComponent implements DoCheck {
results: string[];

// #docregion outside-zone
constructor(private ngZone: NgZone) { }
// #enddocregion outside-zone

// #docregion inside-zone
loadInsideZone() {
Observable.merge(loadHeroes(), loadMoreHeroes(), loadEvenMoreHeroes())
.reduce((heroes, hero) => [...heroes, hero], [])
.subscribe(heroes => this.results = heroes);
}
// #enddocregion inside-zone

// #docregion outside-zone
loadOutsideZone() {
// Escape NgZone before starting work.
// No change detection will be performed during this work.
this.ngZone.runOutsideAngular(() => {
Observable.merge(loadHeroes(), loadMoreHeroes(), loadEvenMoreHeroes())
.reduce((heroes, hero) => [...heroes, hero], [])
.subscribe(heroes => {
// Re-enter zone to process final result.
// Change detection will be performed.
this.ngZone.run(() => this.results = heroes);
});
});
}
// #enddocregion outside-zone

ngDoCheck() {
console.log('cd');
}

}

function loadHeroes() {
return Observable.interval(100)
.map(n => `hero a${n}`)
.do(v => console.log(v))
.take(3);
}

function loadMoreHeroes() {
return Observable.interval(150)
.map(n => `hero b${n}`)
.do(v => console.log(v))
.take(3)
}

function loadEvenMoreHeroes() {
return Observable.interval(200)
.map(n => `hero c${n}`)
.do(v => console.log(v))
.take(3)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';

// #docregion
@Component({
selector: 'hero-counter-live',
template: `
Number of heroes: {{ heroCount }}
<button (click)="toggleLive()">Toggle live update</button>
`
})
export class HeroCounterComponent implements OnInit, OnDestroy {
heroCount = 5;
private live = true;
private updateIntervalId: any;

constructor(private changeDetector: ChangeDetectorRef) { }

ngOnInit() {
// Increment counter ten times per second
this.updateIntervalId = setInterval(() => this.heroCount++, 100);
}

ngOnDestroy() {
clearInterval(this.updateIntervalId);
}

toggleLive() {
this.live = !this.live;
if (this.live) {
this.changeDetector.reattach();
} else {
this.changeDetector.detach();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';

// #docregion
@Component({
selector: 'hero-counter-throttled',
template: `
Number of heroes: {{ heroCount }}
`
})
export class HeroCounterComponent implements OnInit, OnDestroy {
heroCount = 5;

private dataUpdateIntervalId: any;
private viewUpdateIntervalId: any;

constructor(private changeDetector: ChangeDetectorRef) { }

ngOnInit() {
// Detach the change detector so it never runs unless we do it manually.
this.changeDetector.detach();
// Change data a hundred times per second...
this.dataUpdateIntervalId = setInterval(() => this.heroCount++, 10);
// ...but detect changes only once per second
this.viewUpdateIntervalId = setInterval(() => this.changeDetector.detectChanges(), 1000);
}

ngOnDestroy() {
clearInterval(this.dataUpdateIntervalId);
clearInterval(this.viewUpdateIntervalId);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ChangeDetectorRef, Component, Input, AfterViewInit } from '@angular/core';
import { Hero } from '../hero.model';

// #docregion
@Component({
selector: 'hero-name-badge-detached',
template: `
<h4>{{ hero.name }} details</h4>
<p>Name: {{ hero.name }}</p>
`
})
export class HeroNameBadgeComponent implements AfterViewInit {
@Input() hero: Hero;

constructor(private changeDetector: ChangeDetectorRef) { }

ngAfterViewInit() {
this.changeDetector.detach();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';

// #docregion
@Component({
selector: 'hero-signature-form',
template: `
<form #signatureForm method="POST" action="/sign" >
<input type="text" name="username" [(ngModel)]="username" />
<input type="hidden" name="secret" [value]="secret" />
<button (click)="sendForm()">Submit</button>
</form>
`
})
export class HeroSignatureFormComponent {
@ViewChild('signatureForm') signatureForm: ElementRef;

username: string;
secret: string;

constructor(private changeDetector: ChangeDetectorRef) { }

sendForm() {
this.secret = calculateSecret(this.username);
// Ensure the secret is flushed into the form field before we submit.
this.changeDetector.detectChanges();
this.signatureForm.nativeElement.submit();
}

}
// #enddocregion

function calculateSecret(username: string) {
return `SECRET FOR ${username}`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// #docregion
import { Component } from '@angular/core';

@Component({
selector: 'hero-counter',
template: `
Number of heroes:
<button (click)="decrease()">-</button>
{{ heroCount }} <!-- This expression always evaluates to the latest value -->
<button (click)="increase()">+</button>
`
})
export class HeroCounterComponent {
heroCount = 5;

// When we change data, we don't need to do anything to update the view.
increase() { this.heroCount = this.heroCount + 1; }
decrease() { this.heroCount = Math.max(this.heroCount - 1, 0); }

}
Loading

0 comments on commit 16e2f60

Please sign in to comment.