feat(refactor): replaced ObservablePipe and PromisePipe with AsyncPipe

This commit is contained in:
vsavkin 2015-08-04 11:55:21 -07:00
parent bd498977bd
commit 106a28b8dc
9 changed files with 363 additions and 439 deletions

View File

@ -4,10 +4,9 @@
* This module provides advanced support for extending change detection.
*/
export {PromisePipe} from './src/change_detection/pipes/promise_pipe';
export {UpperCasePipe} from './src/change_detection/pipes/uppercase_pipe';
export {LowerCasePipe} from './src/change_detection/pipes/lowercase_pipe';
export {ObservablePipe} from './src/change_detection/pipes/observable_pipe';
export {AsyncPipe} from './src/change_detection/pipes/async_pipe';
export {JsonPipe} from './src/change_detection/pipes/json_pipe';
export {DatePipe} from './src/change_detection/pipes/date_pipe';
export {DecimalPipe, PercentPipe, CurrencyPipe} from './src/change_detection/pipes/number_pipe';

View File

@ -7,8 +7,7 @@ import {IterableDiffers, IterableDifferFactory} from './differs/iterable_differs
import {DefaultIterableDifferFactory} from './differs/default_iterable_differ';
import {KeyValueDiffers, KeyValueDifferFactory} from './differs/keyvalue_differs';
import {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
import {ObservablePipeFactory} from './pipes/observable_pipe';
import {PromisePipeFactory} from './pipes/promise_pipe';
import {AsyncPipeFactory} from './pipes/async_pipe';
import {UpperCasePipe} from './pipes/uppercase_pipe';
import {LowerCasePipe} from './pipes/lowercase_pipe';
import {JsonPipe} from './pipes/json_pipe';
@ -75,11 +74,8 @@ export const iterableDiff: IterableDifferFactory[] =
/**
* Async binding to such types as Observable.
*/
export const async: List<PipeFactory> = CONST_EXPR([
CONST_EXPR(new ObservablePipeFactory()),
CONST_EXPR(new PromisePipeFactory()),
CONST_EXPR(new NullPipeFactory())
]);
export const async: List<PipeFactory> =
CONST_EXPR([CONST_EXPR(new AsyncPipeFactory()), CONST_EXPR(new NullPipeFactory())]);
/**
* Uppercase text transform.

View File

@ -0,0 +1,134 @@
import {isBlank, isPresent, isPromise, CONST, BaseException} from 'angular2/src/facade/lang';
import {Observable, Promise, ObservableWrapper} from 'angular2/src/facade/async';
import {Pipe, WrappedValue, PipeFactory} from './pipe';
import {ChangeDetectorRef} from '../change_detector_ref';
class ObservableStrategy {
createSubscription(async: any, updateLatestValue: any): any {
return ObservableWrapper.subscribe(async, updateLatestValue, e => { throw e; });
}
dispose(subscription: any): void { ObservableWrapper.dispose(subscription); }
onDestroy(subscription: any): void { ObservableWrapper.dispose(subscription); }
}
class PromiseStrategy {
createSubscription(async: any, updateLatestValue: any): any {
return async.then(updateLatestValue);
}
dispose(subscription: any): void {}
onDestroy(subscription: any): void {}
}
var _promiseStrategy = new PromiseStrategy();
var _observableStrategy = new ObservableStrategy();
/**
* Implements async bindings to Observable and Promise.
*
* # Example
*
* In this example we bind the description observable to the DOM. The async pipe will convert an
*observable to the
* latest value it emitted. It will also request a change detection check when a new value is
*emitted.
*
* ```
* @Component({
* selector: "task-cmp",
* changeDetection: ON_PUSH
* })
* @View({
* template: "Task Description {{ description | async }}"
* })
* class Task {
* description:Observable<string>;
* }
*
* ```
*/
export class AsyncPipe implements Pipe {
_latestValue: Object = null;
_latestReturnedValue: Object = null;
_subscription: Object = null;
_obj: Observable | Promise<any> = null;
private _strategy: any = null;
constructor(public _ref: ChangeDetectorRef) {}
supports(obj: any): boolean { return true; }
onDestroy(): void {
if (isPresent(this._subscription)) {
this._dispose();
}
}
transform(obj: Observable | Promise<any>, args?: any[]): any {
if (isBlank(this._obj)) {
if (isPresent(obj)) {
this._subscribe(obj);
}
return null;
}
if (obj !== this._obj) {
this._dispose();
return this.transform(obj);
}
if (this._latestValue === this._latestReturnedValue) {
return this._latestReturnedValue;
} else {
this._latestReturnedValue = this._latestValue;
return WrappedValue.wrap(this._latestValue);
}
}
_subscribe(obj: Observable | Promise<any>): void {
this._obj = obj;
this._strategy = this._selectStrategy(obj);
this._subscription =
this._strategy.createSubscription(obj, value => this._updateLatestValue(obj, value));
}
_selectStrategy(obj: Observable | Promise<any>) {
if (isPromise(obj)) {
return _promiseStrategy;
} else if (ObservableWrapper.isObservable(obj)) {
return _observableStrategy;
} else {
throw new BaseException(`Async pipe does not support object '${obj}'`);
}
}
_dispose(): void {
this._strategy.dispose(this._subscription);
this._latestValue = null;
this._latestReturnedValue = null;
this._subscription = null;
this._obj = null;
}
_updateLatestValue(async: any, value: Object) {
if (async === this._obj) {
this._latestValue = value;
this._ref.requestCheck();
}
}
}
/**
* Provides a factory for [AsyncPipe].
*/
@CONST()
export class AsyncPipeFactory implements PipeFactory {
supports(obj: any): boolean { return true; }
create(cdRef: ChangeDetectorRef): Pipe { return new AsyncPipe(cdRef); }
}

View File

@ -1,94 +0,0 @@
import {Observable, ObservableWrapper} from 'angular2/src/facade/async';
import {isBlank, isPresent, CONST} from 'angular2/src/facade/lang';
import {Pipe, WrappedValue, PipeFactory} from './pipe';
import {ChangeDetectorRef} from '../change_detector_ref';
/**
* Implements async bindings to Observable.
*
* # Example
*
* In this example we bind the description observable to the DOM. The async pipe will convert an
*observable to the
* latest value it emitted. It will also request a change detection check when a new value is
*emitted.
*
* ```
* @Component({
* selector: "task-cmp",
* changeDetection: ON_PUSH
* })
* @View({
* template: "Task Description {{ description | async }}"
* })
* class Task {
* description:Observable<string>;
* }
*
* ```
*/
export class ObservablePipe implements Pipe {
_latestValue: Object = null;
_latestReturnedValue: Object = null;
_subscription: Object = null;
_observable: Observable = null;
constructor(public _ref: ChangeDetectorRef) {}
supports(obs: any): boolean { return ObservableWrapper.isObservable(obs); }
onDestroy(): void {
if (isPresent(this._subscription)) {
this._dispose();
}
}
transform(obs: Observable, args: List<any> = null): any {
if (isBlank(this._subscription)) {
this._subscribe(obs);
return null;
}
if (obs !== this._observable) {
this._dispose();
return this.transform(obs);
}
if (this._latestValue === this._latestReturnedValue) {
return this._latestReturnedValue;
} else {
this._latestReturnedValue = this._latestValue;
return WrappedValue.wrap(this._latestValue);
}
}
_subscribe(obs: Observable): void {
this._observable = obs;
this._subscription = ObservableWrapper.subscribe(obs, value => this._updateLatestValue(value),
e => { throw e; });
}
_dispose(): void {
ObservableWrapper.dispose(this._subscription);
this._latestValue = null;
this._latestReturnedValue = null;
this._subscription = null;
this._observable = null;
}
_updateLatestValue(value: Object) {
this._latestValue = value;
this._ref.requestCheck();
}
}
/**
* Provides a factory for [ObervablePipe].
*/
@CONST()
export class ObservablePipeFactory implements PipeFactory {
supports(obs: any): boolean { return ObservableWrapper.isObservable(obs); }
create(cdRef: ChangeDetectorRef): Pipe { return new ObservablePipe(cdRef); }
}

View File

@ -1,84 +0,0 @@
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {isBlank, isPresent, isPromise, CONST} from 'angular2/src/facade/lang';
import {Pipe, PipeFactory, WrappedValue} from './pipe';
import {ChangeDetectorRef} from '../change_detector_ref';
/**
* Implements async bindings to Promise.
*
* # Example
*
* In this example we bind the description promise to the DOM.
* The async pipe will convert a promise to the value with which it is resolved. It will also
* request a change detection check when the promise is resolved.
*
* ```
* @Component({
* selector: "task-cmp",
* changeDetection: ON_PUSH
* })
* @View({
* template: "Task Description {{ description | async }}"
* })
* class Task {
* description:Promise<string>;
* }
*
* ```
*/
export class PromisePipe implements Pipe {
_latestValue: Object = null;
_latestReturnedValue: Object = null;
_sourcePromise: Promise<any>;
constructor(public _ref: ChangeDetectorRef) {}
supports(promise: any): boolean { return isPromise(promise); }
onDestroy(): void {
if (isPresent(this._sourcePromise)) {
this._latestValue = null;
this._latestReturnedValue = null;
this._sourcePromise = null;
}
}
transform(promise: Promise<any>, args: List<any> = null): any {
if (isBlank(this._sourcePromise)) {
this._sourcePromise = promise;
promise.then((val) => {
if (this._sourcePromise === promise) {
this._updateLatestValue(val);
}
});
return null;
}
if (promise !== this._sourcePromise) {
this._sourcePromise = null;
return this.transform(promise);
}
if (this._latestValue === this._latestReturnedValue) {
return this._latestReturnedValue;
} else {
this._latestReturnedValue = this._latestValue;
return WrappedValue.wrap(this._latestValue);
}
}
_updateLatestValue(value: Object) {
this._latestValue = value;
this._ref.requestCheck();
}
}
/**
* Provides a factory for [PromisePipe].
*/
@CONST()
export class PromisePipeFactory implements PipeFactory {
supports(promise: any): boolean { return isPromise(promise); }
create(cdRef: ChangeDetectorRef): Pipe { return new PromisePipe(cdRef); }
}

View File

@ -1,5 +1,6 @@
import {
ChangeDetector,
ChangeDetectorRef,
ProtoChangeDetector,
DynamicChangeDetector
} from 'angular2/src/change_detection/change_detection';
@ -26,3 +27,7 @@ export class SpyPipeFactory extends SpyObject {}
export class SpyDependencyProvider extends SpyObject {}
export class SpyIterableDifferFactory extends SpyObject {}
export class SpyChangeDetectorRef extends SpyObject {
constructor() { super(ChangeDetectorRef); }
}

View File

@ -0,0 +1,220 @@
import {
ddescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach,
AsyncTestCompleter,
SpyChangeDetectorRef,
inject,
SpyObject
} from 'angular2/test_lib';
import {IMPLEMENTS, isBlank} from 'angular2/src/facade/lang';
import {WrappedValue} from 'angular2/src/change_detection/pipes/pipe';
import {AsyncPipe} from 'angular2/src/change_detection/pipes/async_pipe';
import {
EventEmitter,
ObservableWrapper,
PromiseWrapper,
TimerWrapper
} from 'angular2/src/facade/async';
import {DOM} from 'angular2/src/dom/dom_adapter';
export function main() {
describe("AsyncPipe", () => {
describe('Observable', () => {
var emitter;
var pipe;
var ref;
var message = new Object();
beforeEach(() => {
emitter = new EventEmitter();
ref = new SpyChangeDetectorRef();
pipe = new AsyncPipe(ref);
});
describe("transform", () => {
it("should return null when subscribing to an observable",
() => { expect(pipe.transform(emitter)).toBe(null); });
it("should return the latest available value wrapped",
inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter);
ObservableWrapper.callNext(emitter, message);
TimerWrapper.setTimeout(() => {
expect(pipe.transform(emitter)).toEqual(new WrappedValue(message));
async.done();
}, 0)
}));
it("should return same value when nothing has changed since the last call",
inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter);
ObservableWrapper.callNext(emitter, message);
TimerWrapper.setTimeout(() => {
pipe.transform(emitter);
expect(pipe.transform(emitter)).toBe(message);
async.done();
}, 0)
}));
it("should dispose of the existing subscription when subscribing to a new observable",
inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter);
var newEmitter = new EventEmitter();
expect(pipe.transform(newEmitter)).toBe(null);
// this should not affect the pipe
ObservableWrapper.callNext(emitter, message);
TimerWrapper.setTimeout(() => {
expect(pipe.transform(newEmitter)).toBe(null);
async.done();
}, 0)
}));
it("should request a change detection check upon receiving a new value",
inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter);
ObservableWrapper.callNext(emitter, message);
TimerWrapper.setTimeout(() => {
expect(ref.spy('requestCheck')).toHaveBeenCalled();
async.done();
}, 0)
}));
});
describe("onDestroy", () => {
it("should do nothing when no subscription",
() => { expect(() => pipe.onDestroy()).not.toThrow(); });
it("should dispose of the existing subscription", inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter);
pipe.onDestroy();
ObservableWrapper.callNext(emitter, message);
TimerWrapper.setTimeout(() => {
expect(pipe.transform(emitter)).toBe(null);
async.done();
}, 0)
}));
});
});
describe("Promise", () => {
var message = new Object();
var pipe;
var completer;
var ref;
// adds longer timers for passing tests in IE
var timer = (!isBlank(DOM) && DOM.getUserAgent().indexOf("Trident") > -1) ? 50 : 0;
beforeEach(() => {
completer = PromiseWrapper.completer();
ref = new SpyChangeDetectorRef();
pipe = new AsyncPipe(ref);
});
describe("transform", () => {
it("should return null when subscribing to a promise",
() => { expect(pipe.transform(completer.promise)).toBe(null); });
it("should return the latest available value", inject([AsyncTestCompleter], (async) => {
pipe.transform(completer.promise);
completer.resolve(message);
TimerWrapper.setTimeout(() => {
expect(pipe.transform(completer.promise)).toEqual(new WrappedValue(message));
async.done();
}, timer)
}));
it("should return unwrapped value when nothing has changed since the last call",
inject([AsyncTestCompleter], (async) => {
pipe.transform(completer.promise);
completer.resolve(message);
TimerWrapper.setTimeout(() => {
pipe.transform(completer.promise);
expect(pipe.transform(completer.promise)).toBe(message);
async.done();
}, timer)
}));
it("should dispose of the existing subscription when subscribing to a new promise",
inject([AsyncTestCompleter], (async) => {
pipe.transform(completer.promise);
var newCompleter = PromiseWrapper.completer();
expect(pipe.transform(newCompleter.promise)).toBe(null);
// this should not affect the pipe, so it should return WrappedValue
completer.resolve(message);
TimerWrapper.setTimeout(() => {
expect(pipe.transform(newCompleter.promise)).toBe(null);
async.done();
}, timer)
}));
it("should request a change detection check upon receiving a new value",
inject([AsyncTestCompleter], (async) => {
pipe.transform(completer.promise);
completer.resolve(message);
TimerWrapper.setTimeout(() => {
expect(ref.spy('requestCheck')).toHaveBeenCalled();
async.done();
}, timer)
}));
describe("onDestroy", () => {
it("should do nothing when no source",
() => { expect(() => pipe.onDestroy()).not.toThrow(); });
it("should dispose of the existing source", inject([AsyncTestCompleter], (async) => {
pipe.transform(completer.promise);
expect(pipe.transform(completer.promise)).toBe(null);
completer.resolve(message)
TimerWrapper.setTimeout(() => {
expect(pipe.transform(completer.promise)).toEqual(new WrappedValue(message));
pipe.onDestroy();
expect(pipe.transform(completer.promise)).toBe(null);
async.done();
}, timer);
}));
});
});
});
describe('null', () => {
it('should return null when given null', () => {
var pipe = new AsyncPipe(null);
expect(pipe.transform(null, [])).toEqual(null);
});
});
describe('other types', () => {
it('should throw when given an invalid object', () => {
var pipe = new AsyncPipe(null);
expect(() => pipe.transform(<any>"some bogus object", [])).toThrowError();
});
});
});
}

View File

@ -1,125 +0,0 @@
import {
ddescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach,
AsyncTestCompleter,
inject,
proxy,
SpyObject
} from 'angular2/test_lib';
import {IMPLEMENTS} from 'angular2/src/facade/lang';
import {WrappedValue} from 'angular2/src/change_detection/pipes/pipe';
import {ObservablePipe} from 'angular2/src/change_detection/pipes/observable_pipe';
import {ChangeDetectorRef} from 'angular2/src/change_detection/change_detector_ref';
import {EventEmitter, ObservableWrapper, TimerWrapper} from 'angular2/src/facade/async';
export function main() {
describe("ObservablePipe", () => {
var emitter;
var pipe;
var ref;
var message = new Object();
beforeEach(() => {
emitter = new EventEmitter();
ref = new SpyChangeDetectorRef();
pipe = new ObservablePipe(ref);
});
describe("supports", () => {
it("should support observables", () => { expect(pipe.supports(emitter)).toBe(true); });
it("should not support other objects", () => {
expect(pipe.supports("string")).toBe(false);
expect(pipe.supports(null)).toBe(false);
});
});
describe("transform", () => {
it("should return null when subscribing to an observable",
() => { expect(pipe.transform(emitter)).toBe(null); });
it("should return the latest available value wrapped",
inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter);
ObservableWrapper.callNext(emitter, message);
TimerWrapper.setTimeout(() => {
expect(pipe.transform(emitter)).toEqual(new WrappedValue(message));
async.done();
}, 0)
}));
it("should return same value when nothing has changed since the last call",
inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter);
ObservableWrapper.callNext(emitter, message);
TimerWrapper.setTimeout(() => {
pipe.transform(emitter);
expect(pipe.transform(emitter)).toBe(message);
async.done();
}, 0)
}));
it("should dispose of the existing subscription when subscribing to a new observable",
inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter);
var newEmitter = new EventEmitter();
expect(pipe.transform(newEmitter)).toBe(null);
// this should not affect the pipe
ObservableWrapper.callNext(emitter, message);
TimerWrapper.setTimeout(() => {
expect(pipe.transform(newEmitter)).toBe(null);
async.done();
}, 0)
}));
it("should request a change detection check upon receiving a new value",
inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter);
ObservableWrapper.callNext(emitter, message);
TimerWrapper.setTimeout(() => {
expect(ref.spy('requestCheck')).toHaveBeenCalled();
async.done();
}, 0)
}));
});
describe("onDestroy", () => {
it("should do nothing when no subscription",
() => { expect(() => pipe.onDestroy()).not.toThrow(); });
it("should dispose of the existing subscription", inject([AsyncTestCompleter], (async) => {
pipe.transform(emitter);
pipe.onDestroy();
ObservableWrapper.callNext(emitter, message);
TimerWrapper.setTimeout(() => {
expect(pipe.transform(emitter)).toBe(null);
async.done();
}, 0)
}));
});
});
}
@proxy
@IMPLEMENTS(ChangeDetectorRef)
class SpyChangeDetectorRef extends SpyObject {
constructor() { super(ChangeDetectorRef); }
noSuchMethod(m) { return super.noSuchMethod(m) }
}

View File

@ -1,127 +0,0 @@
import {
ddescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach,
AsyncTestCompleter,
inject,
proxy,
SpyObject
} from 'angular2/test_lib';
import {IMPLEMENTS, isBlank} from 'angular2/src/facade/lang';
import {PromisePipe} from 'angular2/src/change_detection/pipes/promise_pipe';
import {WrappedValue} from 'angular2/src/change_detection/pipes/pipe';
import {ChangeDetectorRef} from 'angular2/src/change_detection/change_detector_ref';
import {PromiseWrapper, TimerWrapper} from 'angular2/src/facade/async';
import {DOM} from 'angular2/src/dom/dom_adapter';
export function main() {
describe("PromisePipe", () => {
var message = new Object();
var pipe;
var completer;
var ref;
// adds longer timers for passing tests in IE
var timer = (!isBlank(DOM) && DOM.getUserAgent().indexOf("Trident") > -1) ? 50 : 0;
beforeEach(() => {
completer = PromiseWrapper.completer();
ref = new SpyChangeDetectorRef();
pipe = new PromisePipe(ref);
});
describe("supports", () => {
it("should support promises", () => { expect(pipe.supports(completer.promise)).toBe(true); });
it("should not support other objects", () => {
expect(pipe.supports("string")).toBe(false);
expect(pipe.supports(null)).toBe(false);
});
});
describe("transform", () => {
it("should return null when subscribing to a promise",
() => { expect(pipe.transform(completer.promise)).toBe(null); });
it("should return the latest available value", inject([AsyncTestCompleter], (async) => {
pipe.transform(completer.promise);
completer.resolve(message);
TimerWrapper.setTimeout(() => {
expect(pipe.transform(completer.promise)).toEqual(new WrappedValue(message));
async.done();
}, timer)
}));
it("should return unwrapped value when nothing has changed since the last call",
inject([AsyncTestCompleter], (async) => {
pipe.transform(completer.promise);
completer.resolve(message);
TimerWrapper.setTimeout(() => {
pipe.transform(completer.promise);
expect(pipe.transform(completer.promise)).toBe(message);
async.done();
}, timer)
}));
it("should dispose of the existing subscription when subscribing to a new promise",
inject([AsyncTestCompleter], (async) => {
pipe.transform(completer.promise);
var newCompleter = PromiseWrapper.completer();
expect(pipe.transform(newCompleter.promise)).toBe(null);
// this should not affect the pipe, so it should return WrappedValue
completer.resolve(message);
TimerWrapper.setTimeout(() => {
expect(pipe.transform(newCompleter.promise)).toBe(null);
async.done();
}, timer)
}));
it("should request a change detection check upon receiving a new value",
inject([AsyncTestCompleter], (async) => {
pipe.transform(completer.promise);
completer.resolve(message);
TimerWrapper.setTimeout(() => {
expect(ref.spy('requestCheck')).toHaveBeenCalled();
async.done();
}, timer)
}));
describe("onDestroy", () => {
it("should do nothing when no source",
() => { expect(() => pipe.onDestroy()).not.toThrow(); });
it("should dispose of the existing source", inject([AsyncTestCompleter], (async) => {
pipe.transform(completer.promise);
expect(pipe.transform(completer.promise)).toBe(null);
completer.resolve(message)
TimerWrapper.setTimeout(() => {
expect(pipe.transform(completer.promise)).toEqual(new WrappedValue(message));
pipe.onDestroy();
expect(pipe.transform(completer.promise)).toBe(null);
async.done();
}, timer);
}));
});
});
});
}
@proxy
@IMPLEMENTS(ChangeDetectorRef)
class SpyChangeDetectorRef extends SpyObject {
constructor() { super(ChangeDetectorRef); }
noSuchMethod(m) { return super.noSuchMethod(m) }
}