From 749875858479ade503fa3c40ab635d149ff83cb5 Mon Sep 17 00:00:00 2001 From: Jeff Cross Date: Tue, 28 Apr 2015 13:39:45 -0700 Subject: [PATCH] feat(PromisePipe): add pipe for promises --- .../change_detection/pipes/promise_pipe.ts | 95 ++++++++++++++++++ .../pipes/promise_pipe_spec.js | 96 +++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 modules/angular2/src/change_detection/pipes/promise_pipe.ts create mode 100644 modules/angular2/test/change_detection/pipes/promise_pipe_spec.js diff --git a/modules/angular2/src/change_detection/pipes/promise_pipe.ts b/modules/angular2/src/change_detection/pipes/promise_pipe.ts new file mode 100644 index 0000000000..76df3aad46 --- /dev/null +++ b/modules/angular2/src/change_detection/pipes/promise_pipe.ts @@ -0,0 +1,95 @@ +import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; +import {isBlank, isPresent} from 'angular2/src/facade/lang'; +import {Pipe, WrappedValue} from './pipe'; +import {ChangeDetectorRef} from '../change_detector_ref'; + +// HACK: workaround for Traceur behavior. +// It expects all transpiled modules to contain this marker. +// TODO: remove this when we no longer use traceur +export var __esModule = true; + +/** + * 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({ + * inline: "Task Description {{description|promise}}" + * }) + * class Task { + * description:Promise; + * } + * + * ``` + * + * @exportedAs angular2/pipes + */ +export class PromisePipe extends Pipe { + _ref: ChangeDetectorRef; + _latestValue: Object; + _latestReturnedValue: Object; + _sourcePromise: Promise; + + constructor(ref: ChangeDetectorRef) { + super(); + this._ref = ref; + this._latestValue = null; + this._latestReturnedValue = null; + } + + supports(promise): boolean { return PromiseWrapper.isPromise(promise); } + + onDestroy(): void { + // NO-OP + } + + transform(promise: Promise): any { + var pipe = this; + if (isBlank(this._sourcePromise)) { + this._sourcePromise = promise; + promise.then((val) => { + if (pipe._sourcePromise === promise) { + pipe._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]. + * + * @exportedAs angular2/pipes + */ +export class PromisePipeFactory { + supports(promise): boolean { return PromiseWrapper.isPromise(promise); } + + create(cdRef): Pipe { return new PromisePipe(cdRef); } +} diff --git a/modules/angular2/test/change_detection/pipes/promise_pipe_spec.js b/modules/angular2/test/change_detection/pipes/promise_pipe_spec.js new file mode 100644 index 0000000000..57b3123684 --- /dev/null +++ b/modules/angular2/test/change_detection/pipes/promise_pipe_spec.js @@ -0,0 +1,96 @@ +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 {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} from 'angular2/src/facade/async'; + +export function main() { + describe("PromisePipe", () => { + var message = new Object(); + var pipe; + var completer; + var ref; + + 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); + + PromiseWrapper.setTimeout(() => { + expect(pipe.transform(completer.promise)).toEqual(new WrappedValue(message)); + async.done(); + }, 0) + })); + + it("should return unwrapped value when nothing has changed since the last call", + inject([AsyncTestCompleter], (async) => { + pipe.transform(completer.promise); + completer.resolve(message); + + PromiseWrapper.setTimeout(() => { + pipe.transform(completer.promise); + expect(pipe.transform(completer.promise)).toBe(message); + async.done(); + }, 0) + })); + + 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); + + PromiseWrapper.setTimeout(() => { + expect(pipe.transform(newCompleter.promise)).toBe(null); + async.done(); + }, 0) + })); + + it("should request a change detection check upon receiving a new value", + inject([AsyncTestCompleter], (async) => { + pipe.transform(completer.promise); + completer.resolve(message); + + PromiseWrapper.setTimeout(() => { + expect(ref.spy('requestCheck')).toHaveBeenCalled(); + async.done(); + }, 0) + })); + }); + }); +} + +@proxy +@IMPLEMENTS(ChangeDetectorRef) +class SpyChangeDetectorRef extends SpyObject { + constructor(){super(ChangeDetectorRef);} + noSuchMethod(m){return super.noSuchMethod(m)} +}