Skip to content

Commit a97a226

Browse files
committed
feat(change_detection): added async pipe
1 parent 8b3c808 commit a97a226

File tree

6 files changed

+260
-1
lines changed

6 files changed

+260
-1
lines changed

modules/angular2/src/change_detection/change_detection.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {DynamicProtoChangeDetector, JitProtoChangeDetector} from './proto_change
22
import {PipeRegistry} from './pipes/pipe_registry';
33
import {IterableChangesFactory} from './pipes/iterable_changes';
44
import {KeyValueChangesFactory} from './pipes/keyvalue_changes';
5+
import {AsyncPipeFactory} from './pipes/async_pipe';
56
import {NullPipeFactory} from './pipes/null_pipe';
67
import {DEFAULT} from './constants';
78
import {ChangeDetection, ProtoChangeDetector} from './interfaces';
@@ -27,9 +28,20 @@ export var iterableDiff = [
2728
new NullPipeFactory()
2829
];
2930

31+
/**
32+
* Async binding to such types as Observable.
33+
*
34+
* @exportedAs angular2/pipes
35+
*/
36+
export var async = [
37+
new AsyncPipeFactory(),
38+
new NullPipeFactory()
39+
];
40+
3041
export var defaultPipes = {
3142
"iterableDiff" : iterableDiff,
32-
"keyValDiff" : keyValDiff
43+
"keyValDiff" : keyValDiff,
44+
"async" : async
3345
};
3446

3547

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {Observable, ObservableWrapper} from 'angular2/src/facade/async';
2+
import {isBlank, isPresent} from 'angular2/src/facade/lang';
3+
import {Pipe, NO_CHANGE} from './pipe';
4+
import {ChangeDetectorRef} from '../change_detector_ref';
5+
6+
/**
7+
* Implements async bindings to Observable.
8+
*
9+
* # Example
10+
*
11+
* In this example we bind the description observable to the DOM. The async pipe will convert an observable to the
12+
* latest value it emitted. It will also request a change detection check when a new value is emitted.
13+
*
14+
* ```
15+
* @Component({
16+
* selector: "task-cmp",
17+
* changeDetection: ON_PUSH
18+
* })
19+
* @View({
20+
* inline: "Task Description {{description|async}}"
21+
* })
22+
* class Task {
23+
* description:Observable<string>;
24+
* }
25+
*
26+
* ```
27+
*
28+
* @exportedAs angular2/pipes
29+
*/
30+
export class AsyncPipe extends Pipe {
31+
_ref:ChangeDetectorRef;
32+
33+
_latestValue:Object;
34+
_latestReturnedValue:Object;
35+
36+
_subscription:Object;
37+
_observable:Observable;
38+
39+
constructor(ref:ChangeDetectorRef) {
40+
super();
41+
this._ref = ref;
42+
this._latestValue = null;
43+
this._latestReturnedValue = null;
44+
this._subscription = null;
45+
this._observable = null;
46+
}
47+
48+
supports(obs):boolean {
49+
return ObservableWrapper.isObservable(obs);
50+
}
51+
52+
onDestroy():void {
53+
if (isPresent(this._subscription)) {
54+
this._dispose();
55+
};
56+
}
57+
58+
transform(obs:Observable):any {
59+
if (isBlank(this._subscription)) {
60+
this._subscribe(obs);
61+
return null;
62+
}
63+
64+
if (obs !== this._observable) {
65+
this._dispose();
66+
return this.transform(obs);
67+
}
68+
69+
if (this._latestValue === this._latestReturnedValue) {
70+
return NO_CHANGE;
71+
} else {
72+
this._latestReturnedValue = this._latestValue;
73+
return this._latestValue;
74+
}
75+
}
76+
77+
_subscribe(obs:Observable):void {
78+
this._observable = obs;
79+
this._subscription = ObservableWrapper.subscribe(obs,
80+
value => this._updateLatestValue(value),
81+
e => {throw e;}
82+
);
83+
}
84+
85+
_dispose():void {
86+
ObservableWrapper.dispose(this._subscription);
87+
this._latestValue = null;
88+
this._latestReturnedValue = null;
89+
this._subscription = null;
90+
this._observable = null;
91+
}
92+
93+
_updateLatestValue(value:Object) {
94+
this._latestValue = value;
95+
this._ref.requestCheck();
96+
}
97+
}
98+
99+
/**
100+
* Provides a factory for [AsyncPipe].
101+
*
102+
* @exportedAs angular2/pipes
103+
*/
104+
export class AsyncPipeFactory {
105+
supports(obs):boolean {
106+
return ObservableWrapper.isObservable(obs);
107+
}
108+
109+
create(cdRef):Pipe {
110+
return new AsyncPipe(cdRef);
111+
}
112+
}

modules/angular2/src/facade/async.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ class ObservableWrapper {
3737
return s.listen(onNext, onError: onError, onDone: onComplete, cancelOnError: true);
3838
}
3939

40+
static bool isObservable(obs) {
41+
return obs is Stream;
42+
}
43+
44+
static void dispose(StreamSubscription s) {
45+
s.cancel();
46+
}
47+
4048
static void callNext(EventEmitter emitter, value) {
4149
emitter.add(value);
4250
}

modules/angular2/src/facade/async.es6

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ export class ObservableWrapper {
5858
return emitter.observer({next: onNext, throw: onThrow, return: onReturn});
5959
}
6060

61+
static dispose(subscription:any) {
62+
subscription.dispose();
63+
}
64+
65+
static isObservable(obs):boolean {
66+
return obs instanceof Observable;
67+
}
68+
6169
static callNext(emitter:EventEmitter, value:any) {
6270
emitter.next(value);
6371
}

modules/angular2/src/facade/async.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export class ObservableWrapper {
5454
return emitter.observer({next: onNext, throw: onThrow, return: onReturn});
5555
}
5656

57+
static isObservable(obs: any): boolean { return obs instanceof Observable; }
58+
59+
static dispose(subscription: any) { subscription.dispose(); }
60+
5761
static callNext(emitter: EventEmitter, value: any) { emitter.next(value); }
5862

5963
static callThrow(emitter: EventEmitter, error: any) { emitter.throw(error); }
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach,
2+
AsyncTestCompleter, inject, proxy, SpyObject} from 'angular2/test_lib';
3+
import {IMPLEMENTS} from 'angular2/src/facade/lang';
4+
5+
import {AsyncPipe} from 'angular2/src/change_detection/pipes/async_pipe';
6+
import {NO_CHANGE} from 'angular2/src/change_detection/pipes/pipe';
7+
import {ChangeDetectorRef} from 'angular2/src/change_detection/change_detector_ref';
8+
import {EventEmitter, Observable, ObservableWrapper, PromiseWrapper} from 'angular2/src/facade/async';
9+
10+
export function main() {
11+
describe("AsyncPipe", () => {
12+
var emitter;
13+
var pipe;
14+
var ref;
15+
var message = new Object();
16+
17+
beforeEach(() => {
18+
emitter = new EventEmitter();
19+
ref = new SpyChangeDetectorRef();
20+
pipe = new AsyncPipe(ref);
21+
});
22+
23+
describe("supports", () => {
24+
it("should support observables", () => {
25+
expect(pipe.supports(emitter)).toBe(true);
26+
});
27+
28+
it("should not support other objects", () => {
29+
expect(pipe.supports("string")).toBe(false);
30+
expect(pipe.supports(null)).toBe(false);
31+
});
32+
});
33+
34+
describe("transform", () => {
35+
it("should return null when subscribing to an observable", () => {
36+
expect(pipe.transform(emitter)).toBe(null);
37+
});
38+
39+
it("should return the latest available value", inject([AsyncTestCompleter], (async) => {
40+
pipe.transform(emitter);
41+
42+
ObservableWrapper.callNext(emitter, message);
43+
44+
PromiseWrapper.setTimeout(() => {
45+
expect(pipe.transform(emitter)).toEqual(message);
46+
async.done();
47+
}, 0)
48+
}));
49+
50+
it("should return NO_CHANGE when nothing has changed since the last call",
51+
inject([AsyncTestCompleter], (async) => {
52+
pipe.transform(emitter);
53+
ObservableWrapper.callNext(emitter, message);
54+
55+
PromiseWrapper.setTimeout(() => {
56+
pipe.transform(emitter);
57+
expect(pipe.transform(emitter)).toBe(NO_CHANGE);
58+
async.done();
59+
}, 0)
60+
}));
61+
62+
it("should dispose of the existing subscription when subscribing to a new observable",
63+
inject([AsyncTestCompleter], (async) => {
64+
pipe.transform(emitter);
65+
66+
var newEmitter = new EventEmitter();
67+
expect(pipe.transform(newEmitter)).toBe(null);
68+
69+
// this should not affect the pipe, so it should return NO_CHANGE
70+
ObservableWrapper.callNext(emitter, message);
71+
72+
PromiseWrapper.setTimeout(() => {
73+
expect(pipe.transform(newEmitter)).toBe(NO_CHANGE);
74+
async.done();
75+
}, 0)
76+
}));
77+
78+
it("should request a change detection check upon receiving a new value",
79+
inject([AsyncTestCompleter], (async) => {
80+
pipe.transform(emitter);
81+
ObservableWrapper.callNext(emitter, message);
82+
83+
PromiseWrapper.setTimeout(() => {
84+
expect(ref.spy('requestCheck')).toHaveBeenCalled();
85+
async.done();
86+
}, 0)
87+
}));
88+
});
89+
90+
describe("onDestroy", () => {
91+
it("should do nothing when no subscription", () => {
92+
pipe.onDestroy();
93+
});
94+
95+
it("should dispose of the existing subscription", inject([AsyncTestCompleter], (async) => {
96+
pipe.transform(emitter);
97+
pipe.onDestroy();
98+
99+
ObservableWrapper.callNext(emitter, message);
100+
101+
PromiseWrapper.setTimeout(() => {
102+
expect(pipe.transform(emitter)).toBe(null);
103+
async.done();
104+
}, 0)
105+
}));
106+
});
107+
});
108+
}
109+
110+
@proxy
111+
@IMPLEMENTS(ChangeDetectorRef)
112+
class SpyChangeDetectorRef extends SpyObject {
113+
constructor(){super(ChangeDetectorRef);}
114+
noSuchMethod(m){return super.noSuchMethod(m)}
115+
}

0 commit comments

Comments
 (0)