docs(pipes) clarify impure pipes; explain missing filter/orderBy pipes
adds flying-heroes example and its test adds random-pipe example
This commit is contained in:
parent
16b3c940dd
commit
5b27178083
|
@ -5,19 +5,19 @@ describe('Pipes', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open correctly', function () {
|
it('should open correctly', function () {
|
||||||
expect(element.all(by.css('h4')).get(0).getText()).toEqual('Hero Birthday v.1');
|
expect(element.all(by.tagName('h1')).get(0).getText()).toEqual('Pipes');
|
||||||
expect(element(by.css('body > hero-birthday p')).getText()).toEqual("The hero's birthday is Apr 15, 1988");
|
expect(element(by.css('hero-birthday p')).getText()).toEqual("The hero's birthday is Apr 15, 1988");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show delayed message', function () {
|
it('should show an async hero message', function () {
|
||||||
expect(element.all(by.css('hero-message')).get(0).getText()).toEqual('Message: You are my Hero!');
|
expect(element.all(by.tagName('hero-message')).get(0).getText()).toContain('hero');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show 4 heroes', function () {
|
it('should show 4 heroes', function () {
|
||||||
expect(element.all(by.css('hero-list div')).count()).toEqual(4);
|
expect(element.all(by.css('hero-list div')).count()).toEqual(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show 4 heroes in json', function () {
|
it('should show a familiar hero in json', function () {
|
||||||
expect(element(by.cssContainingText('hero-list p', 'Heroes as JSON')).getText()).toContain('Bombasto');
|
expect(element(by.cssContainingText('hero-list p', 'Heroes as JSON')).getText()).toContain('Bombasto');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -27,9 +27,9 @@ describe('Pipes', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to toggle birthday formats', function () {
|
it('should be able to toggle birthday formats', function () {
|
||||||
var birthDayEle = element(by.css('my-app > hero-birthday > p'));
|
var birthDayEle = element(by.css('hero-birthday2 > p'));
|
||||||
expect(birthDayEle.getText()).toEqual("The hero's birthday is 4/15/1988");
|
expect(birthDayEle.getText()).toEqual("The hero's birthday is 4/15/1988");
|
||||||
var buttonEle = element(by.cssContainingText('my-app > hero-birthday > button', "Toggle Format"));
|
var buttonEle = element(by.cssContainingText('hero-birthday2 > button', "Toggle Format"));
|
||||||
expect(buttonEle.isDisplayed()).toBe(true);
|
expect(buttonEle.isDisplayed()).toBe(true);
|
||||||
buttonEle.click().then(function() {
|
buttonEle.click().then(function() {
|
||||||
expect(birthDayEle.getText()).toEqual("The hero's birthday is Friday, April 15, 1988");
|
expect(birthDayEle.getText()).toEqual("The hero's birthday is Friday, April 15, 1988");
|
||||||
|
@ -66,5 +66,52 @@ describe('Pipes', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should support flying heroes (pure) ', function () {
|
||||||
|
var nameEle = element(by.css('flying-heroes input[type="text"]'));
|
||||||
|
var canFlyCheckEle = element(by.css('flying-heroes #can-fly'));
|
||||||
|
var mutateCheckEle = element(by.css('flying-heroes #mutate'));
|
||||||
|
var resetEle = element(by.css('flying-heroes button'));
|
||||||
|
var flyingHeroesEle = element.all(by.css('flying-heroes #flyers div'));
|
||||||
|
|
||||||
|
expect(canFlyCheckEle.getAttribute('checked')).toEqual('true', 'should default to "can fly"');
|
||||||
|
expect(mutateCheckEle.getAttribute('checked')).toEqual('true', 'should default to mutating array');
|
||||||
|
expect(flyingHeroesEle.count()).toEqual(2, 'only two of the original heroes can fly');
|
||||||
|
|
||||||
|
return sendKeys(nameEle, "test1\n")
|
||||||
|
.then(function(){
|
||||||
|
expect(flyingHeroesEle.count()).toEqual(2, 'no change while mutating array');
|
||||||
|
return mutateCheckEle.click();
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
return sendKeys(nameEle, "test2\n");
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
expect(flyingHeroesEle.count()).toEqual(4, 'not mutating; should see both adds');
|
||||||
|
expect(flyingHeroesEle.get(2).getText()).toContain('test1');
|
||||||
|
expect(flyingHeroesEle.get(3).getText()).toContain('test2');
|
||||||
|
return resetEle.click();
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
expect(flyingHeroesEle.count()).toEqual(2, 'reset should restore orginal flying heroes');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should support flying heroes (impure) ', function () {
|
||||||
|
var nameEle = element(by.css('flying-heroes-impure input[type="text"]'));
|
||||||
|
var canFlyCheckEle = element(by.css('flying-heroes-impure #can-fly'));
|
||||||
|
var mutateCheckEle = element(by.css('flying-heroes-impure #mutate'));
|
||||||
|
var resetEle = element(by.css('flying-heroes-impure button'));
|
||||||
|
var flyingHeroesEle = element.all(by.css('flying-heroes-impure #flyers div'));
|
||||||
|
|
||||||
|
expect(canFlyCheckEle.getAttribute('checked')).toEqual('true', 'should default to "can fly"');
|
||||||
|
expect(mutateCheckEle.getAttribute('checked')).toEqual('true', 'should default to mutating array');
|
||||||
|
expect(flyingHeroesEle.count()).toEqual(2, 'only two of the original heroes can fly');
|
||||||
|
|
||||||
|
return sendKeys(nameEle, "test1\n")
|
||||||
|
.then(function(){
|
||||||
|
expect(flyingHeroesEle.count()).toEqual(3, 'new flying hero should show in mutating array');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,26 @@
|
||||||
<hr>
|
<a id="toc"></a>
|
||||||
<!-- async examples at the top so can see them in action -->
|
<h1>Pipes</h1>
|
||||||
<hero-message></hero-message>
|
<a href="#happy-birthday1">Happy Birthday v.1</a><br>
|
||||||
|
<a href="#birthday-date-pipe">Birthday DatePipe</a><br>
|
||||||
|
<a href="#happy-birthday2">Happy Birthday v.2</a><br>
|
||||||
|
<a href="#birthday-pipe-chaining">Birthday Pipe Chaining</a><br>
|
||||||
|
<a href="#power-booster">Power Booster custom pipe</a><br>
|
||||||
|
<a href="#power-boost-calc">Power Boost Calculator custom pipe with params</a><br>
|
||||||
|
<a href="#flying-heroes">Flying Heroes filter pipe (pure)</a><br>
|
||||||
|
<a href="#flying-heroes-impure">Flying Heroes filter pipe (impure)</a><br>
|
||||||
|
<a href="#hero-message">Async Hero Message and AsyncPipe</a><br>
|
||||||
|
<a href="#hero-list">Hero List with caching FetchJsonPipe</a><br>
|
||||||
|
<a href="#random-pipe">Random Pipe (pure pipe / impure function)</a><br>
|
||||||
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<hero-list></hero-list>
|
<a id="happy-birthday1"></a>
|
||||||
|
<h2>Hero Birthday v.1</h2>
|
||||||
|
<hero-birthday></hero-birthday>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
<a id="birthday-date-pipe"></a>
|
||||||
|
<h2>Birthday DatePipe</h2>
|
||||||
<!-- #docregion hero-birthday-template -->
|
<!-- #docregion hero-birthday-template -->
|
||||||
<p>The hero's birthday is {{ birthday | date }}</p>
|
<p>The hero's birthday is {{ birthday | date }}</p>
|
||||||
<!-- #enddocregion hero-birthday-template-->
|
<!-- #enddocregion hero-birthday-template-->
|
||||||
|
@ -15,33 +30,59 @@
|
||||||
<!-- #enddocregion format-birthday-->
|
<!-- #enddocregion format-birthday-->
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<h4>Hero Birthday v.2</h4>
|
<a id="happy-birthday2"></a>
|
||||||
<hero-birthday>loading...</hero-birthday>
|
<h2>Hero Birthday v.2</h2>
|
||||||
|
<hero-birthday2></hero-birthday2>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
<a id="birthday-pipe-chaining"></a>
|
||||||
|
<h2>Birthday Pipe Chaining</h2>
|
||||||
<!-- #docregion chained-birthday -->
|
|
||||||
<p>
|
<p>
|
||||||
|
<!-- #docregion chained-birthday -->
|
||||||
The chained hero's birthday is
|
The chained hero's birthday is
|
||||||
{{ birthday | date | uppercase}}
|
{{ birthday | date | uppercase}}
|
||||||
</p>
|
|
||||||
<!-- #enddocregion chained-birthday -->
|
<!-- #enddocregion chained-birthday -->
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- #docregion chained-parameter-birthday -->
|
|
||||||
<p>
|
<p>
|
||||||
|
<!-- #docregion chained-parameter-birthday -->
|
||||||
The chained hero's birthday is
|
The chained hero's birthday is
|
||||||
{{ birthday | date:'fullDate' | uppercase}}
|
{{ birthday | date:'fullDate' | uppercase}}
|
||||||
</p>
|
|
||||||
<!-- #enddocregion chained-parameter-birthday -->
|
<!-- #enddocregion chained-parameter-birthday -->
|
||||||
<!-- #docregion chained-parameter-birthday-parens -->
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
<!-- #docregion chained-parameter-birthday-parens -->
|
||||||
The chained hero's birthday is
|
The chained hero's birthday is
|
||||||
{{ ( birthday | date:'fullDate' ) | uppercase}}
|
{{ ( birthday | date:'fullDate' ) | uppercase}}
|
||||||
</p>
|
|
||||||
<!-- #enddocregion chained-parameter-birthday-parens -->
|
<!-- #enddocregion chained-parameter-birthday-parens -->
|
||||||
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<power-booster>loading...</power-booster>
|
<a id="power-booster"></a>
|
||||||
|
<power-booster></power-booster>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<power-boost-calculator>loading ..</power-boost-calculator>
|
<a id="power-boost-calc"></a>
|
||||||
|
<power-boost-calculator>loading</power-boost-calculator>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<a id="flying-heroes"></a>
|
||||||
|
<flying-heroes></flying-heroes>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<a id="flying-heroes-impure"></a>
|
||||||
|
<flying-heroes-impure></flying-heroes-impure>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<a id="hero-message"></a>
|
||||||
|
<!-- async examples at the top so can see them in action -->
|
||||||
|
<hero-message></hero-message>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<a id="hero-list"></a>
|
||||||
|
<hero-list></hero-list>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<a id="random-pipe"></a>
|
||||||
|
<random-pipe></random-pipe>
|
||||||
|
|
||||||
|
<div style="margin-top:12em;"></div>
|
||||||
|
|
|
@ -1,20 +1,30 @@
|
||||||
// #docregion
|
// #docregion
|
||||||
import {Component} from 'angular2/core';
|
import {Component} from 'angular2/core';
|
||||||
|
import {HTTP_PROVIDERS} from 'angular2/http';
|
||||||
|
|
||||||
|
import {FlyingHeroesComponent,
|
||||||
|
FlyingHeroesImpureComponent} from './flying-heroes.component';
|
||||||
import {HeroAsyncMessageComponent} from './hero-async-message.component';
|
import {HeroAsyncMessageComponent} from './hero-async-message.component';
|
||||||
import {HeroBirthday} from './hero-birthday2.component';
|
import {HeroBirthday} from './hero-birthday1.component';
|
||||||
|
import {HeroBirthday2} from './hero-birthday2.component';
|
||||||
import {HeroListComponent} from './hero-list.component';
|
import {HeroListComponent} from './hero-list.component';
|
||||||
import {PowerBooster} from './power-booster.component';
|
import {PowerBooster} from './power-booster.component';
|
||||||
import {PowerBoostCalculator} from './power-boost-calculator.component';
|
import {PowerBoostCalculator} from './power-boost-calculator.component';
|
||||||
|
import {RandomPipeComponent} from './random-pipe.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
templateUrl: 'app/app.component.html',
|
templateUrl: 'app/app.component.html',
|
||||||
directives:[
|
directives:[
|
||||||
|
FlyingHeroesComponent, FlyingHeroesImpureComponent,
|
||||||
HeroAsyncMessageComponent,
|
HeroAsyncMessageComponent,
|
||||||
HeroBirthday,
|
HeroBirthday,
|
||||||
|
HeroBirthday2,
|
||||||
HeroListComponent,
|
HeroListComponent,
|
||||||
PowerBooster, PowerBoostCalculator
|
PowerBooster, PowerBoostCalculator,
|
||||||
]
|
RandomPipeComponent
|
||||||
|
],
|
||||||
|
providers:[HTTP_PROVIDERS]
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
birthday = new Date(1988,3,15); // April 15, 1988
|
birthday = new Date(1988,3,15); // April 15, 1988
|
||||||
|
|
|
@ -11,8 +11,8 @@ import {Pipe, PipeTransform} from 'angular2/core';
|
||||||
*/
|
*/
|
||||||
@Pipe({name: 'exponentialStrength'})
|
@Pipe({name: 'exponentialStrength'})
|
||||||
export class ExponentialStrengthPipe implements PipeTransform {
|
export class ExponentialStrengthPipe implements PipeTransform {
|
||||||
|
transform(value:number, [exponent]) : number {
|
||||||
transform(value:number, args:string[]) : any {
|
var exp = parseFloat(exponent);
|
||||||
return Math.pow(value, parseInt(args[0] || '1', 10));
|
return Math.pow(value, isNaN(exp) ? 1 : exp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/// <reference path='./window.extension.d.ts'/>
|
|
||||||
// #docregion
|
// #docregion
|
||||||
import {Pipe, PipeTransform} from 'angular2/core';
|
import {Pipe, PipeTransform} from 'angular2/core';
|
||||||
|
import {Http} from 'angular2/http';
|
||||||
|
|
||||||
// #docregion pipe-metadata
|
// #docregion pipe-metadata
|
||||||
@Pipe({
|
@Pipe({
|
||||||
|
@ -9,16 +9,20 @@ import {Pipe, PipeTransform} from 'angular2/core';
|
||||||
})
|
})
|
||||||
// #enddocregion pipe-metadata
|
// #enddocregion pipe-metadata
|
||||||
export class FetchJsonPipe implements PipeTransform{
|
export class FetchJsonPipe implements PipeTransform{
|
||||||
private fetchedValue:any;
|
private fetched:any = null;
|
||||||
private fetchPromise:Promise<any>;
|
private prevUrl = '';
|
||||||
|
|
||||||
transform(value:string, args:string[]):any {
|
constructor(private _http: Http) { }
|
||||||
if (!this.fetchPromise) {
|
|
||||||
this.fetchPromise = window.fetch(value)
|
transform(url:string):any {
|
||||||
.then((result:any) => result.json())
|
if (url !== this.prevUrl) {
|
||||||
.then((json:any) => this.fetchedValue = json);
|
this.prevUrl = url;
|
||||||
|
this.fetched = null;
|
||||||
|
this._http.get(url)
|
||||||
|
.map( result => result.json() )
|
||||||
|
.subscribe( result => this.fetched = result )
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.fetchedValue;
|
return this.fetched;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!-- #docplaster-->
|
||||||
|
<!-- #docregion -->
|
||||||
|
<h2>{{title}}</h2>
|
||||||
|
<p>
|
||||||
|
<!-- #docregion template-1 -->
|
||||||
|
New hero:
|
||||||
|
<input type="text" #box
|
||||||
|
(keyup.enter)="addHero(box.value); box.value=''"
|
||||||
|
placeholder="hero name">
|
||||||
|
<!-- #enddocregion template-1 -->
|
||||||
|
<input id="can-fly" type="checkbox" [(ngModel)]="canFly"> can fly
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input id="mutate" type="checkbox" [(ngModel)]="mutate">Mutate array
|
||||||
|
<!-- #docregion template-1 -->
|
||||||
|
<button (click)="reset()">Reset</button>
|
||||||
|
<!-- #enddocregion template-1 -->
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Heroes who fly (piped)</h4>
|
||||||
|
<div id="flyers">
|
||||||
|
<!-- #docregion template-flying-heroes -->
|
||||||
|
<div *ngFor="#hero of (heroes | flyingHeroes)">
|
||||||
|
{{hero.name}}
|
||||||
|
</div>
|
||||||
|
<!-- #enddocregion template-flying-heroes -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>All Heroes (no pipe)</h4>
|
||||||
|
<div id="all">
|
||||||
|
<!-- #docregion template-1 -->
|
||||||
|
<!-- #docregion template-all-heroes -->
|
||||||
|
<div *ngFor="#hero of heroes">
|
||||||
|
{{hero.name}}
|
||||||
|
</div>
|
||||||
|
<!-- #enddocregion template-all-heroes -->
|
||||||
|
<!-- #enddocregion template-1 -->
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,64 @@
|
||||||
|
// #docplaster
|
||||||
|
// #docregion
|
||||||
|
import {Component} from 'angular2/core';
|
||||||
|
import {FlyingHeroesPipe,
|
||||||
|
FlyingHeroesImpurePipe} from './flying-heroes.pipe';
|
||||||
|
import {HEROES} from './heroes';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'flying-heroes',
|
||||||
|
templateUrl: 'app/flying-heroes.component.html',
|
||||||
|
styles: ['#flyers, #all {font-style: italic}'],
|
||||||
|
pipes: [FlyingHeroesPipe]
|
||||||
|
})
|
||||||
|
// #docregion v1
|
||||||
|
export class FlyingHeroesComponent {
|
||||||
|
heroes:any[] = [];
|
||||||
|
canFly = true;
|
||||||
|
// #enddocregion v1
|
||||||
|
mutate = true;
|
||||||
|
title = 'Flying Heroes (pure pipe)';
|
||||||
|
|
||||||
|
// #docregion v1
|
||||||
|
constructor() { this.reset(); }
|
||||||
|
|
||||||
|
addHero(name:string) {
|
||||||
|
name = name.trim();
|
||||||
|
if (!name) { return; }
|
||||||
|
let hero = {name, canFly: this.canFly};
|
||||||
|
// #enddocregion v1
|
||||||
|
if (this.mutate) {
|
||||||
|
// Pure pipe won't update display because heroes array reference is unchanged
|
||||||
|
// Impure pipe will display
|
||||||
|
// #docregion v1
|
||||||
|
// #docregion push
|
||||||
|
this.heroes.push(hero)
|
||||||
|
// #enddocregion push
|
||||||
|
// #enddocregion v1
|
||||||
|
} else {
|
||||||
|
// Pipe updates display because heroes array is a new object
|
||||||
|
// #docregion concat
|
||||||
|
this.heroes = this.heroes.concat(hero);
|
||||||
|
// #enddocregion concat
|
||||||
|
}
|
||||||
|
// #docregion v1
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() { this.heroes = HEROES.slice(); }
|
||||||
|
}
|
||||||
|
// #enddocregion v1
|
||||||
|
|
||||||
|
////// Identical except for impure pipe //////
|
||||||
|
// #docregion impure-component
|
||||||
|
@Component({
|
||||||
|
selector: 'flying-heroes-impure',
|
||||||
|
templateUrl: 'app/flying-heroes.component.html',
|
||||||
|
// #enddocregion impure-component
|
||||||
|
styles: ['.flyers, .all {font-style: italic}'],
|
||||||
|
// #docregion impure-component
|
||||||
|
pipes: [FlyingHeroesImpurePipe]
|
||||||
|
})
|
||||||
|
export class FlyingHeroesImpureComponent extends FlyingHeroesComponent {
|
||||||
|
title = 'Flying Heroes (impure pipe)';
|
||||||
|
}
|
||||||
|
// #docregion impure-component
|
|
@ -0,0 +1,25 @@
|
||||||
|
// #docregion
|
||||||
|
// #docregion pure
|
||||||
|
import {Flyer} from './heroes';
|
||||||
|
import {Pipe, PipeTransform} from 'angular2/core';
|
||||||
|
|
||||||
|
@Pipe({ name: 'flyingHeroes' })
|
||||||
|
export class FlyingHeroesPipe implements PipeTransform {
|
||||||
|
transform(allHeroes:Flyer[]) {
|
||||||
|
// #docregion filter
|
||||||
|
return allHeroes.filter(hero => hero.canFly);
|
||||||
|
// #enddocregion filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #enddocregion pure
|
||||||
|
|
||||||
|
/////// Identical except for the pure flag
|
||||||
|
// #docregion impure
|
||||||
|
// #docregion pipe-decorator
|
||||||
|
@Pipe({
|
||||||
|
name: 'flyingHeroes',
|
||||||
|
pure: false
|
||||||
|
})
|
||||||
|
// #enddocregion pipe-decorator
|
||||||
|
export class FlyingHeroesImpurePipe extends FlyingHeroesPipe {}
|
||||||
|
// #enddocregion impure
|
|
@ -1,15 +1,39 @@
|
||||||
// #docregion
|
// #docregion
|
||||||
import {Component} from 'angular2/core';
|
import {Component} from 'angular2/core';
|
||||||
|
import {Observable} from 'rxjs/Rx';
|
||||||
|
|
||||||
// Initial view: "Message: "
|
// Initial view: "Message: "
|
||||||
// After 500ms: Message: You are my Hero!"
|
// After 500ms: Message: You are my Hero!"
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hero-message',
|
selector: 'hero-message',
|
||||||
template: 'Message: {{delayedMessage | async}}',
|
template: `
|
||||||
|
<h2>Async Hero Message and AsyncPipe</h2>
|
||||||
|
|
||||||
|
<p>Message: {{ message$ | async }}</p>
|
||||||
|
|
||||||
|
<button (click)="resend()">Resend</button>`,
|
||||||
})
|
})
|
||||||
export class HeroAsyncMessageComponent {
|
export class HeroAsyncMessageComponent {
|
||||||
delayedMessage:Promise<string> = new Promise((resolve, reject) => {
|
message$:Observable<string>;
|
||||||
setTimeout(() => resolve('You are my Hero!'), 500);
|
|
||||||
});
|
constructor() { this.resend(); }
|
||||||
|
|
||||||
|
resend() {
|
||||||
|
this.message$ = Observable.interval(500)
|
||||||
|
.map(i => this.messages[i])
|
||||||
|
.take(this.messages.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private messages = [
|
||||||
|
'You are my hero!',
|
||||||
|
'You are the best hero!',
|
||||||
|
'Will you be my hero?'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// #enddocregion
|
||||||
|
|
||||||
|
// Alternative message$ formula:
|
||||||
|
// this.message$ = Observable.fromArray(this.messages)
|
||||||
|
// .map(message => Observable.timer(500).map(() => message))
|
||||||
|
// .concatAll();
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import {Component} from 'angular2/core'
|
import {Component} from 'angular2/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hero-birthday',
|
selector: 'hero-birthday2',
|
||||||
// #docregion template
|
// #docregion template
|
||||||
template: `
|
template: `
|
||||||
<p>The hero's birthday is {{ birthday | date:format }}</p>
|
<p>The hero's birthday is {{ birthday | date:format }}</p>
|
||||||
|
@ -12,13 +12,11 @@ import {Component} from 'angular2/core'
|
||||||
// #enddocregion template
|
// #enddocregion template
|
||||||
})
|
})
|
||||||
// #docregion class
|
// #docregion class
|
||||||
export class HeroBirthday {
|
export class HeroBirthday2 {
|
||||||
birthday = new Date(1988,3,15); // April 15, 1988
|
birthday = new Date(1988,3,15); // April 15, 1988
|
||||||
|
|
||||||
toggle = true; // start with true == shortDate
|
toggle = true; // start with true == shortDate
|
||||||
|
|
||||||
get format() { return this.toggle ? 'shortDate' : 'fullDate'}
|
get format() { return this.toggle ? 'shortDate' : 'fullDate'}
|
||||||
|
|
||||||
toggleFormat() { this.toggle = !this.toggle; }
|
toggleFormat() { this.toggle = !this.toggle; }
|
||||||
}
|
}
|
||||||
// #enddocregion class
|
// #enddocregion class
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {FetchJsonPipe} from './fetch-json.pipe';
|
||||||
selector: 'hero-list',
|
selector: 'hero-list',
|
||||||
// #docregion template
|
// #docregion template
|
||||||
template: `
|
template: `
|
||||||
<h4>Heroes from JSON File</h4>
|
<h2>Heroes from JSON File</h2>
|
||||||
|
|
||||||
<div *ngFor="#hero of ('heroes.json' | fetch) ">
|
<div *ngFor="#hero of ('heroes.json' | fetch) ">
|
||||||
{{hero.name}}
|
{{hero.name}}
|
||||||
|
@ -19,6 +19,4 @@ import {FetchJsonPipe} from './fetch-json.pipe';
|
||||||
// #enddocregion template
|
// #enddocregion template
|
||||||
pipes: [FetchJsonPipe]
|
pipes: [FetchJsonPipe]
|
||||||
})
|
})
|
||||||
export class HeroListComponent {
|
export class HeroListComponent { }
|
||||||
/* I've got nothing to do ;-) */
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface Flyer { canFly: boolean }
|
||||||
|
export const HEROES = [
|
||||||
|
{"name": "Windstorm", "canFly": true},
|
||||||
|
{"name": "Bombasto", "canFly": false},
|
||||||
|
{"name": "Magneto", "canFly": false},
|
||||||
|
{"name": "Tornado", "canFly": true}
|
||||||
|
];
|
|
@ -1,6 +1,6 @@
|
||||||
import {bootstrap} from 'angular2/platform/browser';
|
import {bootstrap} from 'angular2/platform/browser';
|
||||||
|
import 'rxjs/Rx';
|
||||||
|
|
||||||
import {AppComponent} from './app.component';
|
import {AppComponent} from './app.component';
|
||||||
import {HeroBirthday} from './hero-birthday1.component';
|
|
||||||
|
|
||||||
bootstrap(AppComponent);
|
bootstrap(AppComponent);
|
||||||
bootstrap(HeroBirthday); // v.1
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import {Component} from 'angular2/core';
|
||||||
|
import {Pipe, PipeTransform} from 'angular2/core';
|
||||||
|
|
||||||
|
// #docregion pipe
|
||||||
|
// Pure pipe
|
||||||
|
@Pipe({ name: 'randomizer' })
|
||||||
|
export class RandomizerPipe implements PipeTransform {
|
||||||
|
// Impure function
|
||||||
|
transform() { return Math.random() * 10 ;}
|
||||||
|
}
|
||||||
|
// #enddocregion pipe
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'random-pipe',
|
||||||
|
template: `
|
||||||
|
<h2>Random Pipe (pure pipe/impure function)</h2>
|
||||||
|
<input #box (input)="0">
|
||||||
|
<p>Input value: {{box.value}}</p>
|
||||||
|
<p>Random pipe output: {{box.value | randomizer}}</p>
|
||||||
|
`,
|
||||||
|
pipes: [RandomizerPipe]
|
||||||
|
})
|
||||||
|
export class RandomPipeComponent {
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
interface Window {
|
|
||||||
fetch(url: string, options? : {}) : Promise<any>
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
[
|
[
|
||||||
{"name": "Windstorm"},
|
{"name": "Windstorm", "canFly": true},
|
||||||
{"name": "Bombasto"},
|
{"name": "Bombasto", "canFly": false},
|
||||||
{"name": "Magneto"},
|
{"name": "Magneto", "canFly": false},
|
||||||
{"name": "Tornado"}
|
{"name": "Tornado", "canFly": true}
|
||||||
]
|
]
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
<script src="node_modules/systemjs/dist/system.src.js"></script>
|
<script src="node_modules/systemjs/dist/system.src.js"></script>
|
||||||
<script src="node_modules/rxjs/bundles/Rx.js"></script>
|
<script src="node_modules/rxjs/bundles/Rx.js"></script>
|
||||||
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
|
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
|
||||||
|
<script src="node_modules/angular2/bundles/http.dev.js"></script>
|
||||||
<script>
|
<script>
|
||||||
System.config({
|
System.config({
|
||||||
packages: {
|
packages: {
|
||||||
|
@ -29,9 +30,6 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h4>Hero Birthday v.1</h4>
|
|
||||||
<hero-birthday>hero-birthday loading...</hero-birthday>
|
|
||||||
|
|
||||||
<my-app>my-app loading ...</my-app>
|
<my-app>my-app loading ...</my-app>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
"description": "Pipes",
|
"description": "Pipes",
|
||||||
"files":[
|
"files":[
|
||||||
"!**/*.d.ts",
|
"!**/*.d.ts",
|
||||||
"!**/*.js",
|
"!**/*.js"],
|
||||||
"!**/*.d.ts"],
|
|
||||||
"tags": ["pipe"]
|
"tags": ["pipe"]
|
||||||
}
|
}
|
|
@ -245,10 +245,8 @@ table(width="100%")
|
||||||
td <a href="#docheck">DoCheck</a>
|
td <a href="#docheck">DoCheck</a>
|
||||||
td
|
td
|
||||||
:marked
|
:marked
|
||||||
Implements a `ngDoCheck` method with custom change detection
|
Implements a `ngDoCheck` method with custom change detection.
|
||||||
that works because the data behave in a particularly simple way.
|
See how often Angular calls this hook and watch it post changes to a log.
|
||||||
See how often Angular calls this hook and
|
|
||||||
watch it post changes to a log.
|
|
||||||
tr(style=top)
|
tr(style=top)
|
||||||
td <a href="#afterview">AfterView</a>
|
td <a href="#afterview">AfterView</a>
|
||||||
td
|
td
|
||||||
|
@ -437,8 +435,12 @@ figure.image-display
|
||||||
:marked
|
:marked
|
||||||
## DoCheck
|
## DoCheck
|
||||||
|
|
||||||
We can take over the change detection with the `DoCheck` hook when Angular doesn't
|
We can use the `DoCheck` hook to detect and act upon changes that Angular doesn't catch on its own.
|
||||||
catch an important change on its own.
|
.l-sub-section
|
||||||
|
:marked
|
||||||
|
With this method we can detect a change that Angular overlooked.
|
||||||
|
What we do with that information to refresh the display is a separate matter.
|
||||||
|
:marked
|
||||||
|
|
||||||
The *DoCheck* sample extends the *OnChanges* sample with this implementation of `DoCheck`:
|
The *DoCheck* sample extends the *OnChanges* sample with this implementation of `DoCheck`:
|
||||||
+makeExample('lifecycle-hooks/ts/app/do-check.component.ts', 'ng-do-check', 'DoCheckComponent (ngDoCheck)')(format=".")
|
+makeExample('lifecycle-hooks/ts/app/do-check.component.ts', 'ng-do-check', 'DoCheckComponent (ngDoCheck)')(format=".")
|
||||||
|
|
|
@ -26,7 +26,7 @@ include ../_util-fns
|
||||||
A pipe takes in data as input and transforms it to a desired output.
|
A pipe takes in data as input and transforms it to a desired output.
|
||||||
We'll illustrate by transforming a component's birthday property into
|
We'll illustrate by transforming a component's birthday property into
|
||||||
a human-friendly date:
|
a human-friendly date:
|
||||||
+makeExample('pipes/ts/app/hero-birthday1.component.ts', null, 'app/hero-birthday1.component.ts')
|
+makeExample('pipes/ts/app/hero-birthday1.component.ts', null, 'app/hero-birthday1.component.ts')(format='.')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Focus on the component's template.
|
Focus on the component's template.
|
||||||
|
@ -44,9 +44,11 @@ include ../_util-fns
|
||||||
They are all immediately available for use in any template.
|
They are all immediately available for use in any template.
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
Learn more about these and many other built-in pipes in the the [API Reference](../api/);
|
Learn more about these and many other built-in pipes in the the [API Reference](../api/#!?apiFilter=pipe);
|
||||||
filter for entries that include the word "pipe".
|
filter for entries that include the word "pipe".
|
||||||
|
|
||||||
|
Angular 2 doesn't have a `FilterPipe` or an `OrderByPipe` for reasons explained in an [appendix below](#no-filter-pipe)
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Parameterizing a Pipe
|
## Parameterizing a Pipe
|
||||||
|
@ -73,7 +75,7 @@ include ../_util-fns
|
||||||
We also added a button to the template and bound its click event to the component's `toggleFormat` method.
|
We also added a button to the template and bound its click event to the component's `toggleFormat` method.
|
||||||
That method toggles the component's `format` property between a short form
|
That method toggles the component's `format` property between a short form
|
||||||
('shortDate') and a longer form ('fullDate').
|
('shortDate') and a longer form ('fullDate').
|
||||||
+makeExample('pipes/ts/app/hero-birthday2.component.ts', 'class', 'app/hero-birthday2.component.ts (class)')
|
+makeExample('pipes/ts/app/hero-birthday2.component.ts', 'class', 'app/hero-birthday2.component.ts (class)')(format='.')
|
||||||
:marked
|
:marked
|
||||||
As we click the button, the displayed date alternates between
|
As we click the button, the displayed date alternates between
|
||||||
"**<span style="font-family:courier">04/15/1988</span>**" and
|
"**<span style="font-family:courier">04/15/1988</span>**" and
|
||||||
|
@ -103,12 +105,10 @@ figure.image-display
|
||||||
|
|
||||||
+makeExample('pipes/ts/app/app.component.html', 'chained-parameter-birthday')(format=".")
|
+makeExample('pipes/ts/app/app.component.html', 'chained-parameter-birthday')(format=".")
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
:marked
|
||||||
We can add parentheses to alter the evaluation order or
|
We can add parentheses to alter the evaluation order or
|
||||||
to provide extra clarity:
|
to provide extra clarity:
|
||||||
+makeExample('pipes/ts/app/app.component.html', 'chained-parameter-birthday-parens')(format=".")
|
+makeExample('pipes/ts/app/app.component.html', 'chained-parameter-birthday-parens')(format=".")
|
||||||
:marked
|
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
|
@ -118,14 +118,14 @@ figure.image-display
|
||||||
|
|
||||||
Here's a custom pipe named `ExponentialStrengthPipe` that can boost a hero's powers:
|
Here's a custom pipe named `ExponentialStrengthPipe` that can boost a hero's powers:
|
||||||
|
|
||||||
+makeExample('pipes/ts/app/exponential-strength.pipe.ts', null, 'app/exponential-strength.pipe.ts')
|
+makeExample('pipes/ts/app/exponential-strength.pipe.ts', null, 'app/exponential-strength.pipe.ts')(format=".")
|
||||||
:marked
|
:marked
|
||||||
This pipe definition reveals several key points
|
This pipe definition reveals several key points
|
||||||
|
|
||||||
* A pipe is a class decorated with pipe metadata.
|
* A pipe is a class decorated with pipe metadata.
|
||||||
|
|
||||||
* The pipe class implements the `PipeTransform` interface's `transform` method that
|
* The pipe class implements the `PipeTransform` interface's `transform` method that
|
||||||
takes an input value and an optional array of parameter strings and returns the transformed value.
|
accepts an input value and an optional array of parameters and returns the transformed value.
|
||||||
|
|
||||||
* There will be one item in the parameter array for each parameter passed to the pipe
|
* There will be one item in the parameter array for each parameter passed to the pipe
|
||||||
|
|
||||||
|
@ -141,12 +141,12 @@ figure.image-display
|
||||||
### The *PipeTransform* Interface
|
### The *PipeTransform* Interface
|
||||||
|
|
||||||
The `transform` method is essential to a pipe.
|
The `transform` method is essential to a pipe.
|
||||||
The `PipeTransform` interface defines that method and guides both tooling and the compiler.
|
The `PipeTransform` *interface* defines that method and guides both tooling and the compiler.
|
||||||
It is optional; Angular looks for and executes the `transform` method regardless.
|
It is technically optional; Angular looks for and executes the `transform` method regardless.
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Now we need a component to demonstrate our pipe.
|
Now we need a component to demonstrate our pipe.
|
||||||
+makeExample('pipes/ts/app/power-booster.component.ts',null,'app/power-booster.component.ts')
|
+makeExample('pipes/ts/app/power-booster.component.ts',null,'app/power-booster.component.ts')(format='.')
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src='/resources/images/devguide/pipes/power-booster.png' alt="Power Booster")
|
img(src='/resources/images/devguide/pipes/power-booster.png' alt="Power Booster")
|
||||||
|
|
||||||
|
@ -154,7 +154,7 @@ figure.image-display
|
||||||
Two things to note:
|
Two things to note:
|
||||||
1. We use our custom pipe the same way we use the built-in pipes.
|
1. We use our custom pipe the same way we use the built-in pipes.
|
||||||
|
|
||||||
1. We must list our pipe in the @Component decorator's `pipes` array.
|
1. We must list our pipe in the `pipes` array of the `@Component` decorator.
|
||||||
|
|
||||||
.callout.is-helpful
|
.callout.is-helpful
|
||||||
header Remember the pipes array!
|
header Remember the pipes array!
|
||||||
|
@ -172,85 +172,248 @@ figure.image-display
|
||||||
We could upgrade the example to a "Power Boost Calculator" that combines
|
We could upgrade the example to a "Power Boost Calculator" that combines
|
||||||
our pipe and two-way data binding with `ngModel`.
|
our pipe and two-way data binding with `ngModel`.
|
||||||
|
|
||||||
+makeExample('pipes/ts/app/power-boost-calculator.component.ts', null, '/app/power-boost-calculator.component.ts')
|
+makeExample('pipes/ts/app/power-boost-calculator.component.ts', null, '/app/power-boost-calculator.component.ts')(format='.')
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src='/resources/images/devguide/pipes/power-boost-calculator-anim.gif' alt="Power Boost Calculator")
|
img(src='/resources/images/devguide/pipes/power-boost-calculator-anim.gif' alt="Power Boost Calculator")
|
||||||
|
|
||||||
|
.l-main-section
|
||||||
|
a(id="change-detection")
|
||||||
:marked
|
:marked
|
||||||
|
## Pipes and Change Detection
|
||||||
|
Angular looks for changes to data-bound values through a *change detection* process that runs after every JavaScript event:
|
||||||
|
every keystroke, mouse move, timer tick, and server response. It could be expensive.
|
||||||
|
Angular strives to lower the cost whenever possible and appropriate.
|
||||||
|
|
||||||
|
Angular picks a simpler, faster change detection algorithm when we use a pipe. Let's see how.
|
||||||
|
|
||||||
|
### No pipe
|
||||||
|
The component in our next example uses the default, aggressive change detection strategy to monitor and update
|
||||||
|
its display of every hero in the `heroes` array. Here's the template:
|
||||||
|
+makeExample('pipes/ts/app/flying-heroes.component.html', 'template-1', 'app/flying-heroes.component.html (v1)')(format='.')
|
||||||
|
:marked
|
||||||
|
The companion component class provides heroes, pushes new heroes into the array, and can reset the array.
|
||||||
|
+makeExample('pipes/ts/app/flying-heroes.component.ts', 'v1', 'app/flying-heroes.component.ts (v1)')(format='.')
|
||||||
|
:marked
|
||||||
|
We can add a new hero and Angular updates the display when we do.
|
||||||
|
The `reset` button replaces `heroes` with a new array of the original heroes and Angular updates the display when we do.
|
||||||
|
If we added the ability to remove or change a hero, Angular would detect those changes too and update the display again.
|
||||||
|
add or remove heroes. It updates the display when we modify a hero.
|
||||||
|
|
||||||
|
### Flying Heroes pipe
|
||||||
|
Let's add a `FlyingHeroesPipe` to the `*ngFor` repeater that filters the list of heroes to just those heroes who can fly.
|
||||||
|
+makeExample('pipes/ts/app/flying-heroes.component.html', 'template-flying-heroes', 'app/flying-heroes.component.html (flyers)')(format='.')
|
||||||
|
:marked
|
||||||
|
Here's the `FlyingHeroesPipe` implementation which follows the pattern for custom pipes we saw earlier.
|
||||||
|
+makeExample('pipes/ts/app/flying-heroes.pipe.ts', 'pure', 'app/flying-heroes.pipe.ts')(format='.')
|
||||||
|
|
||||||
|
:marked
|
||||||
|
When we run the sample now we see odd behavior (try it in the [live example](/resources/live-examples/pipes/ts/plnkr.html)).
|
||||||
|
Every hero we add is a flying hero but none of them are displayed.
|
||||||
|
|
||||||
|
Although we're not getting the behavior we want, Angular isn't broken.
|
||||||
|
It's just using a different change detection algorithm — one that ignores changes to the list or any of its items.
|
||||||
|
|
||||||
|
Look at how we're adding a new hero:
|
||||||
|
+makeExample('pipes/ts/app/flying-heroes.component.ts', 'push')(format='.')
|
||||||
|
:marked
|
||||||
|
We're pushing the new hero into the `heroes` array. The object reference to the array hasn't changed.
|
||||||
|
It's the same array. That's all Angular cares about. From its perspective, *same array, no change, no display update*.
|
||||||
|
|
||||||
|
We can fix that. Let's use `concat` to create a new array with the new hero appended and assign that to `heroes`.
|
||||||
|
This time Angular detects that the array object reference has changed.
|
||||||
|
It executes the pipe and updates the display with the new array which includes the new flying hero.
|
||||||
|
|
||||||
|
*If we **mutate** the array, no pipe and no display update;
|
||||||
|
if we **replace** the array, the pipe executes and the display updates*.
|
||||||
|
|
||||||
|
The *Flying Heroes* in the [live example](/resources/live-examples/pipes/ts/plnkr.html) extends the
|
||||||
|
code with checkbox switches and additional displays to help us experience these effects.
|
||||||
|
figure.image-display
|
||||||
|
img(src='/resources/images/devguide/pipes/flying-heroes-anim.gif' alt="Flying Heroes")
|
||||||
|
|
||||||
|
:marked
|
||||||
|
Replacing the array is an efficient way to signal to Angular that it should update the display.
|
||||||
|
When do we replace the array? When the data change.
|
||||||
|
|
||||||
|
That's an easy rule to follow in *this toy* example
|
||||||
|
where the only way to change the data is by adding a new hero.
|
||||||
|
|
||||||
|
More often we don't know when the data have changed,
|
||||||
|
especially in applications that mutate data in many ways,
|
||||||
|
perhaps in application locations far away.
|
||||||
|
A component is such an application usually can't know about those changes.
|
||||||
|
|
||||||
|
Moreover, it's unwise to distort our component design to accommodate a pipe.
|
||||||
|
We strive as much as possible to keep the component class independent of the HTML.
|
||||||
|
The component should be unaware of pipes.
|
||||||
|
|
||||||
|
Perhaps we should consider a different kind of pipe for filtering flying heroes, an *impure pipe*.
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Stateful Pipes
|
## Pure and Impure Pipes
|
||||||
|
|
||||||
There are two categories of pipes, stateless and stateful.
|
There are two categories of pipes: **pure** and **impure**.
|
||||||
|
|
||||||
Stateless pipes are pure functions that flow input data
|
Pipes are pure by default. Every pipe we've seen so far has been pure.
|
||||||
through without remembering anything or causing detectable side-effects.
|
|
||||||
|
|
||||||
Most pipes are stateless. The `DatePipe` in our first example is a stateless pipe. So is our custom `ExponentialStrengthPipe`.
|
We make a pipe impure by setting its pure flag to false. We could make the `FlyingHeroesPipe`
|
||||||
|
impure with a flip of the switch:
|
||||||
|
|
||||||
Stateful pipes are conceptually similar to classes in object-oriented programming. They can manage the data they transform. A pipe that creates an HTTP request, stores the response and displays the output, is a stateful pipe.
|
+makeExample('pipes/ts/app/flying-heroes.pipe.ts', 'pipe-decorator')(format='.')
|
||||||
Pipes that retrieve or request data should be used cautiously, since working with network data tends to introduce error conditions that are better handled in JavaScript than in a template.
|
:marked
|
||||||
We can mitigate this risk by creating a custom pipe for a particular backend and bake-in the essential error-handling.
|
Before we do that, let's understand the difference between *pure* and *impure*, starting with a *pure* pipe.
|
||||||
|
|
||||||
|
### Pure pipes
|
||||||
|
|
||||||
|
Angular executes a *pure pipe* only when it detects a *pure change* to the input value.
|
||||||
|
|
||||||
|
A *pure change* is *either* a change to a primitive input value (`String`, `Number`, `Boolean`, `Symbol`)
|
||||||
|
*or* a changed object reference (`Date`, `Array`, `Function`, `Object`).
|
||||||
|
|
||||||
|
Angular ignores changes *within* the object itself.
|
||||||
|
It won't call a pure pipe if we change the input month, add to the input array, or update an input object property.
|
||||||
|
|
||||||
|
This may seem restrictive but is is also fast.
|
||||||
|
An object reference check is fast ... much faster than a deep check for differences.
|
||||||
|
... so Angular can quickly determine if it can skip both the pipe execution and a screen update.
|
||||||
|
|
||||||
|
For this reason, we prefer a pure pipe if we can live with the change detection strategy.
|
||||||
|
When we can't, we *may* turn to the impure pipe.
|
||||||
|
|
||||||
|
.l-sub-section
|
||||||
|
:marked
|
||||||
|
Or we might not use a pipe at all.
|
||||||
|
It may be better to pursue the pipe's purpose with a property of the component,
|
||||||
|
a point we take up later.
|
||||||
|
:marked
|
||||||
|
### Impure pipes
|
||||||
|
Angular executes an *impure pipe* during *every* component change detection cycle.
|
||||||
|
An impure pipe will be called a lot, as often as every keystroke or mouse-move.
|
||||||
|
|
||||||
|
If follows that we must implement an impure pipe with great care.
|
||||||
|
An expensive, long-running pipe could destroy the user experience.
|
||||||
|
|
||||||
|
<a id="impure-flying-heroes"></a>
|
||||||
|
### An impure *FlyingHeroesPipe*
|
||||||
|
|
||||||
|
A flip of the switch turns our `FlyingHeroesPipe` into a `FlyingHeroesImpurePipe`.
|
||||||
|
Here's the complete implementation:
|
||||||
|
+makeTabs(
|
||||||
|
'pipes/ts/app/flying-heroes.pipe.ts, pipes/ts/app/flying-heroes.pipe.ts',
|
||||||
|
'impure, pure',
|
||||||
|
'FlyingHeroesImpurePipe, FlyingHeroesPipe')(format='.')
|
||||||
|
:marked
|
||||||
|
We inherit from `FlyingHeroesPipe` to prove the point that nothing changed internally.
|
||||||
|
The only difference is the `pure` flag in the pipe metadata.
|
||||||
|
|
||||||
|
This is a good candidate for an impure pipe because the `transform` function is trivial and fast.
|
||||||
|
+makeExample('pipes/ts/app/flying-heroes.pipe.ts','filter')(format='.')
|
||||||
|
|
||||||
|
We can derive a `FlyingHeroesImpureComponent` that we derive from the `FlyingHeroesComponent`.
|
||||||
|
+makeExample('pipes/ts/app/flying-heroes.component.ts','impure-component',
|
||||||
|
'app/flying-heroes.component.ts (FlyingHeroesImpureComponent)')(format='.')
|
||||||
|
:marked
|
||||||
|
The only substantive change is the pipe.
|
||||||
|
We can confirm in the [live example](/resources/live-examples/pipes/ts/plnkr.html)
|
||||||
|
that the *flying heroes* display updates as we enter new heroes even when we mutate the `heroes` array.
|
||||||
|
|
||||||
<a id="async-pipe"></a>
|
<a id="async-pipe"></a>
|
||||||
## The stateful `AsyncPipe`
|
### The impure *AsyncPipe*
|
||||||
The Angular Async pipe is a remarkable example of a stateful pipe.
|
The Angular `AsyncPipe` is an interesting example of an impure pipe.
|
||||||
The Async pipe can receive a Promise or Observable as input
|
The `AsyncPipe` accepts a `Promise` or `Observable` as input
|
||||||
and subscribe to the input automatically, eventually returning the emitted value(s).
|
and subscribes to the input automatically, eventually returning the emitted value(s).
|
||||||
|
|
||||||
It is stateful because the pipe maintains a subscription to the input and its returned values depend on that subscription.
|
It is also stateful.
|
||||||
|
The pipe maintains a subscription to the input `Observable` and
|
||||||
|
keeps delivering values from that `Observable` as they arrive.
|
||||||
|
|
||||||
In the next example, we bind a simple promise to a view with the async pipe.
|
In this next example, we bind an `Observable` of message strings (`messages$`) to a view with the `async` pipe.
|
||||||
+makeExample('pipes/ts/app/hero-async-message.component.ts', null, 'app/hero-async-message.component.ts')
|
+makeExample('pipes/ts/app/hero-async-message.component.ts', null, 'app/hero-async-message.component.ts')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
The Async pipe saves boilerplate in the component code.
|
The Async pipe saves boilerplate in the component code.
|
||||||
The component doesn't have to subscribe to the async data source,
|
The component doesn't have to subscribe to the async data source,
|
||||||
it doesn't extract the resolved values and expose them for binding,
|
it doesn't extract the resolved values and expose them for binding,
|
||||||
and (in the case of Observable stream sources like `EventEmitter`)
|
and the component doesn't have to unsubscribe when it is destroyed
|
||||||
the component doesn't have to unsubscribe when it is destroyed
|
|
||||||
(a potent source of memory leaks).
|
(a potent source of memory leaks).
|
||||||
|
|
||||||
### Implementing a Stateful Pipe
|
### An impure caching pipe
|
||||||
Pipes are stateless by default.
|
|
||||||
We must declare a pipe to be stateful
|
Let's write one more impure pipe, a pipe that makes an http request to the server.
|
||||||
by setting the `pure` property of the `@Pipe` decorator to `false`.
|
|
||||||
This setting tells Angular’s change detection system to
|
Normally, that's a horrible idea.
|
||||||
check the output of this pipe each cycle, whether its input has changed or not.
|
It's probably a horrible idea no matter what we do.
|
||||||
|
We're forging ahead anyway to make a point.
|
||||||
|
|
||||||
|
Remember that impure pipes are called every few microseconds.
|
||||||
|
If we're not careful, this pipe will punish the server with requests.
|
||||||
|
|
||||||
|
We are careful. Our pipe only makes a server call if the request URL has changed.
|
||||||
|
It caches the request URL and waits for a result which it also caches when it arrives.
|
||||||
|
The pipe returns the cached result (which is null while a request is in flight)
|
||||||
|
after every Angular call and only contacts the server as necessary.
|
||||||
|
|
||||||
|
Here's the code, which uses the [Angular http](server-communication.html) facility
|
||||||
|
to retrieve a `heroes.json` file:
|
||||||
|
|
||||||
Here's how we'll decorate our new stateful `FetchJsonPipe` that
|
|
||||||
makes an HTTP `fetch` request and (eventually) displays the data in the server's response:
|
|
||||||
+makeExample('pipes/ts/app/fetch-json.pipe.ts', 'pipe-metadata','app/fetch-json.pipe.ts (metadata)')
|
|
||||||
:marked
|
|
||||||
Immediately below we have the finished pipe. Its input value is an url to an endpoint that returns a JSON file.
|
|
||||||
The pipe makes a one-time async request to the server and eventually receives the JSON response.
|
|
||||||
+makeExample('pipes/ts/app/fetch-json.pipe.ts', null, 'app/fetch-json.pipe.ts')
|
+makeExample('pipes/ts/app/fetch-json.pipe.ts', null, 'app/fetch-json.pipe.ts')
|
||||||
:marked
|
:marked
|
||||||
Next we demonstrate this pipe in a test component whose template defines two bindings
|
Then we demonstrate it in a harness component whose template defines two bindings to this pipe.
|
||||||
+makeExample('pipes/ts/app/hero-list.component.ts', 'template', 'app/hero-list.component.ts (template)')
|
+makeExample('pipes/ts/app/hero-list.component.ts', 'template', 'app/hero-list.component.ts (template)')
|
||||||
:marked
|
:marked
|
||||||
|
Despite the two bindings and what we know to be frequent pipe calls,
|
||||||
|
the nework tab in the browser developer tools confirms that there is only one request for the file.
|
||||||
|
|
||||||
The component renders like this:
|
The component renders like this:
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src='/resources/images/devguide/pipes/hero-list.png' alt="Hero List")
|
img(src='/resources/images/devguide/pipes/hero-list.png' alt="Hero List")
|
||||||
:marked
|
:marked
|
||||||
The first binding is straight forward. An `ngFor` repeater displays the hero names fetched from a json source file.
|
### *JsonPipe*
|
||||||
We're piping the literal file name, "heroes.json", through to the custom `fetch` pipe.
|
The second binding involving the `FetchPipe` uses more pipe chaining.
|
||||||
|
We take the same fetched results displayed in the first binding
|
||||||
### JsonPipe
|
and display them again, this time in JSON format by chaining through to the built-in `JsonPipe`.
|
||||||
The second binding uses more pipe chaining.
|
|
||||||
We take the same fetched results and display the raw hero data in JSON format
|
|
||||||
by piping to the built-in `JsonPipe`.
|
|
||||||
|
|
||||||
.callout.is-helpful
|
.callout.is-helpful
|
||||||
header Debugging with the json pipe
|
header Debugging with the json pipe
|
||||||
:marked
|
:marked
|
||||||
The [JsonPipe](../api/common/JsonPipe-class.html)
|
The [JsonPipe](../api/common/JsonPipe-class.html)
|
||||||
is an easy way to diagnosis a mysteriously failing data binding.
|
provides an easy way to diagnosis a mysteriously failing data binding or
|
||||||
|
inspect an object for future binding.
|
||||||
:marked
|
:marked
|
||||||
Here's the complete component implementation:
|
Here's the complete component implementation:
|
||||||
+makeExample('pipes/ts/app/hero-list.component.ts', null, 'app/hero-list.component.ts')
|
+makeExample('pipes/ts/app/hero-list.component.ts', null, 'app/hero-list.component.ts')
|
||||||
|
|
||||||
|
a(id="pure-pipe-pure-fn")
|
||||||
:marked
|
:marked
|
||||||
|
### Pure pipes vs. pure functions
|
||||||
|
|
||||||
|
When developers first hear of *pure pipes* many of them think these pipes must be *pure functions*.
|
||||||
|
|
||||||
|
Pure functions process inputs and return values without detectable side-effects.
|
||||||
|
Given the same input they should always return the same output.
|
||||||
|
|
||||||
|
Pure pipes _are_ typically implemented with pure function.
|
||||||
|
The pipes we saw earlier in this chapter were implemented with pure functions.
|
||||||
|
The built-in `DatePipe` is a pure pipe with a pure function implementation.
|
||||||
|
So is our `ExponentialStrengthPipe`.
|
||||||
|
So is our `FlyingHeroesPipe`.
|
||||||
|
|
||||||
|
But there is no necessary connection between a pure pipe and a pure function.
|
||||||
|
A few steps back we reviewed the `FlyingHeroesImpurePipe` — *an impure pipe with a pure function*.
|
||||||
|
|
||||||
|
We can also write *a pure pipe with an impure function* such as the
|
||||||
|
`RandomizerPipe` (also in the [live example](/resources/live-examples/pipes/ts/plnkr.html))
|
||||||
|
that ignores its input value and outputs a random number. Clearly an impure function!
|
||||||
|
Yet it's perfectly well behaved as a pure pipe.
|
||||||
|
+makeExample('pipes/ts/app/random-pipe.component.ts', 'pipe','app/random-pipe.component.ts (pipe)')(format='.')
|
||||||
|
:marked
|
||||||
|
When the input changes, it outputs a new value albeit never the same value for the same input.
|
||||||
|
|
||||||
|
Remember *purity in a pipe has nothing to do with pure functions!*
|
||||||
|
|
||||||
|
*Pipe purity* is more about the purity of the input values than of the pipe itself.
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
|
@ -261,5 +424,52 @@ figure.image-display
|
||||||
into our templates expressions to enrich the appeal and usability
|
into our templates expressions to enrich the appeal and usability
|
||||||
of our views.
|
of our views.
|
||||||
|
|
||||||
Explore Angular's inventory of built-in pipes in the [API Reference](../api/).
|
Explore Angular's inventory of built-in pipes in the [API Reference](../api/#!?apiFilter=pipe).
|
||||||
Try writing a custom pipe and perhaps contributing it to the community.
|
Try writing a custom pipe and perhaps contributing it to the community.
|
||||||
|
|
||||||
|
a(id="no-filter-pipe")
|
||||||
|
.l-main-section
|
||||||
|
:marked
|
||||||
|
## No *FilterPipe* or *OrderByPipe*
|
||||||
|
Angular does not ship with pipes for filtering or sorting lists.
|
||||||
|
Developers familiar with Angular 1 know these as `filter` and `orderBy`.
|
||||||
|
There are no equivalents in Angular 2.
|
||||||
|
|
||||||
|
This is not an oversight. Angular 2 is unlikely to offer such pipes because
|
||||||
|
(a) they perform poorly and (b) they prevent aggressive minification.
|
||||||
|
|
||||||
|
Both *filter* and *orderBy* require parameters that reference object properties.
|
||||||
|
We learned earlier that such pipes must be [*impure*](#pure-and-impure-pipes) and that
|
||||||
|
Angular calls impure pipes in almost every change detection cycle.
|
||||||
|
|
||||||
|
Filtering and especially sorting are expensive operations.
|
||||||
|
The user experience can degrade severely for even moderate sized lists when Angular calls these pipe methods many times per second.
|
||||||
|
The `filter` and `orderBy` have often been abused in Angular 1 apps, leading to complaints that Angular itself is slow.
|
||||||
|
That charge is fair in the indirect sense that Angular 1 prepared this performance trap
|
||||||
|
by offering `filter` and `orderBy` in the first place.
|
||||||
|
|
||||||
|
The minification hazard is also compelling if less obvious. Imagine a sorting pipe applied to a list of heroes.
|
||||||
|
We might sort the list by hero `name` and `planet` origin properties something like this:
|
||||||
|
code-example(format="." language="html")
|
||||||
|
<!-- NOT REAL CODE! -->
|
||||||
|
<div *ngFor="#hero of heroes | orderBy:'name,planet'"></div>
|
||||||
|
:marked
|
||||||
|
We identify the sort fields by text strings, expecting the pipe to reference a property value by indexing
|
||||||
|
(e.g., `hero['name']`).
|
||||||
|
Unfortunately, aggressive minification *munges* the `Hero` property names so that `Hero.name` and `Hero.planet`
|
||||||
|
becomes something like `Hero.a` and `Hero.b`. Clearly `hero['name']` is not going to work.
|
||||||
|
|
||||||
|
Some of us may not care to minify this aggressively. That's *our* choice.
|
||||||
|
But the Angular product should not prevent someone else from minifying aggressively.
|
||||||
|
Therefore, the Angular team decided that everything shipped in Angular will minify safely.
|
||||||
|
|
||||||
|
The Angular team and many experienced Angular developers strongly recommend that you move
|
||||||
|
filtering and sorting logic into the component itself.
|
||||||
|
The component can expose a `filteredHeroes` or `sortedHeroes` property and take control
|
||||||
|
over when and how often to execute the supporting logic.
|
||||||
|
Any capabilities that you would have put in a pipe and shared across the app can be
|
||||||
|
written in a filtering/sorting service and injected into the component.
|
||||||
|
|
||||||
|
If these performance and minification considerations do not apply to you, you can always create your own such pipes
|
||||||
|
(along the lines of the [FlyingHeroesPipe](#impure-flying-heroes)) or find them in the community.
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
Loading…
Reference in New Issue