feat(pipe): added the Pipe decorator and the pipe property to View

BREAKING CHANGE:
    Instead of configuring pipes via a Pipes object, now you can configure them by providing the pipes property to the View decorator.

    @Pipe({
      name: 'double'
    })
    class DoublePipe {
      transform(value, args) { return value * 2; }
    }

    @View({
      template: '{{ 10 | double}}'
      pipes: [DoublePipe]
    })
    class CustomComponent {}

Closes #3572
This commit is contained in:
vsavkin
2015-08-07 11:41:38 -07:00
committed by Victor Savkin
parent 02b7e61ef7
commit 5b5d31fa9a
62 changed files with 627 additions and 524 deletions
@@ -1,4 +1,4 @@
///<reference path="../../src/change_detection/pipes/pipe.ts"/>
///<reference path="../../src/change_detection/pipe_transform.ts"/>
import {
ddescribe,
describe,
@@ -29,8 +29,7 @@ import {
BindingRecord,
DirectiveRecord,
DirectiveIndex,
Pipes,
Pipe,
PipeTransform,
CHECK_ALWAYS,
CHECK_ONCE,
CHECKED,
@@ -45,6 +44,8 @@ import {
Locals,
ProtoChangeDetector
} from 'angular2/src/change_detection/change_detection';
import {Pipes} from 'angular2/src/change_detection/pipes';
import {JitProtoChangeDetector} from 'angular2/src/change_detection/jit_proto_change_detector';
import {getDefinition} from './change_detector_config';
@@ -809,19 +810,6 @@ export function main() {
expect(val.dispatcher.log).toEqual(['propName=Megatron state:1']);
});
it('should inject the ChangeDetectorRef ' +
'of the encompassing component into a pipe',
() => {
var registry = new FakePipes('pipe', () => new IdentityPipe());
var cd =
_createChangeDetector('name | pipe', new Person('bob'), registry).changeDetector;
cd.detectChanges();
expect(registry.cdRef).toBe(cd.ref);
});
});
it('should do nothing when no change', () => {
@@ -854,30 +842,30 @@ export function main() {
});
}
class CountingPipe implements Pipe {
class CountingPipe implements PipeTransform {
state: number = 0;
onDestroy() {}
transform(value, args = null) { return `${value} state:${this.state ++}`; }
}
class PipeWithOnDestroy implements Pipe {
class PipeWithOnDestroy implements PipeTransform {
destroyCalled: boolean = false;
onDestroy() { this.destroyCalled = true; }
transform(value, args = null) { return null; }
}
class IdentityPipe implements Pipe {
class IdentityPipe implements PipeTransform {
onDestroy() {}
transform(value, args = null) { return value; }
}
class WrappedPipe implements Pipe {
class WrappedPipe implements PipeTransform {
onDestroy() {}
transform(value, args = null) { return WrappedValue.wrap(value); }
}
class MultiArgPipe implements Pipe {
class MultiArgPipe implements PipeTransform {
transform(value, args = null) {
var arg1 = args[0];
var arg2 = args[1];
@@ -887,16 +875,14 @@ class MultiArgPipe implements Pipe {
onDestroy(): void {}
}
class FakePipes extends Pipes {
class FakePipes implements Pipes {
numberOfLookups = 0;
cdRef: any;
constructor(public pipeType: string, public factory: Function) { super(null, null); }
constructor(public pipeType: string, public factory: Function) {}
get(type: string, cdRef?) {
get(type: string) {
if (type != this.pipeType) return null;
this.numberOfLookups++;
this.cdRef = cdRef;
return this.factory();
}
}
@@ -1,220 +0,0 @@
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();
});
});
});
}
@@ -1,64 +0,0 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {DatePipe} from 'angular2/src/change_detection/pipes/date_pipe';
import {DateWrapper} from 'angular2/src/facade/lang';
export function main() {
describe("DatePipe", () => {
var date;
var pipe;
beforeEach(() => {
date = DateWrapper.create(2015, 6, 15, 21, 43, 11);
pipe = new DatePipe();
});
describe("supports", () => {
it("should support date", () => { expect(pipe.supports(date)).toBe(true); });
it("should support int", () => { expect(pipe.supports(123456789)).toBe(true); });
it("should not support other objects", () => {
expect(pipe.supports(new Object())).toBe(false);
expect(pipe.supports(null)).toBe(false);
});
});
describe("transform", () => {
it('should format each component correctly', () => {
expect(pipe.transform(date, ['y'])).toEqual('2015');
expect(pipe.transform(date, ['yy'])).toEqual('15');
expect(pipe.transform(date, ['M'])).toEqual('6');
expect(pipe.transform(date, ['MM'])).toEqual('06');
expect(pipe.transform(date, ['MMM'])).toEqual('Jun');
expect(pipe.transform(date, ['MMMM'])).toEqual('June');
expect(pipe.transform(date, ['d'])).toEqual('15');
expect(pipe.transform(date, ['E'])).toEqual('Mon');
expect(pipe.transform(date, ['EEEE'])).toEqual('Monday');
expect(pipe.transform(date, ['H'])).toEqual('21');
expect(pipe.transform(date, ['j'])).toEqual('9 PM');
expect(pipe.transform(date, ['m'])).toEqual('43');
expect(pipe.transform(date, ['s'])).toEqual('11');
});
it('should format common multi component patterns', () => {
expect(pipe.transform(date, ['yMEd'])).toEqual('Mon, 6/15/2015');
expect(pipe.transform(date, ['MEd'])).toEqual('Mon, 6/15');
expect(pipe.transform(date, ['MMMd'])).toEqual('Jun 15');
expect(pipe.transform(date, ['yMMMMEEEEd'])).toEqual('Monday, June 15, 2015');
expect(pipe.transform(date, ['jms'])).toEqual('9:43:11 PM');
expect(pipe.transform(date, ['ms'])).toEqual('43:11');
});
it('should format with pattern aliases', () => {
expect(pipe.transform(date, ['medium'])).toEqual('Jun 15, 2015, 9:43:11 PM');
expect(pipe.transform(date, ['short'])).toEqual('6/15/2015, 9:43 PM');
expect(pipe.transform(date, ['fullDate'])).toEqual('Monday, June 15, 2015');
expect(pipe.transform(date, ['longDate'])).toEqual('June 15, 2015');
expect(pipe.transform(date, ['mediumDate'])).toEqual('Jun 15, 2015');
expect(pipe.transform(date, ['shortDate'])).toEqual('6/15/2015');
expect(pipe.transform(date, ['mediumTime'])).toEqual('9:43:11 PM');
expect(pipe.transform(date, ['shortTime'])).toEqual('9:43 PM');
});
});
});
}
@@ -1,83 +0,0 @@
import {
ddescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach,
AsyncTestCompleter,
inject,
proxy,
SpyObject,
IS_DARTIUM
} from 'angular2/test_lib';
import {Json, RegExp, NumberWrapper, StringWrapper} from 'angular2/src/facade/lang';
import {JsonPipe} from 'angular2/src/change_detection/pipes/json_pipe';
export function main() {
describe("JsonPipe", () => {
var regNewLine = '\n';
var inceptionObj;
var inceptionObjString;
var pipe;
var collection: number[];
function normalize(obj: string): string { return StringWrapper.replace(obj, regNewLine, ''); }
beforeEach(() => {
inceptionObj = {dream: {dream: {dream: 'Limbo'}}};
inceptionObjString = "{\n" + " \"dream\": {\n" + " \"dream\": {\n" +
" \"dream\": \"Limbo\"\n" + " }\n" + " }\n" + "}";
pipe = new JsonPipe();
collection = [];
});
describe("transform", () => {
it("should return JSON-formatted string",
() => { expect(pipe.transform(inceptionObj)).toEqual(inceptionObjString); });
it("should return JSON-formatted string even when normalized", () => {
var dream1 = normalize(pipe.transform(inceptionObj));
var dream2 = normalize(inceptionObjString);
expect(dream1).toEqual(dream2);
});
it("should return JSON-formatted string similar to Json.stringify", () => {
var dream1 = normalize(pipe.transform(inceptionObj));
var dream2 = normalize(Json.stringify(inceptionObj));
expect(dream1).toEqual(dream2);
});
it("should return same ref when nothing has changed since the last call", () => {
expect(pipe.transform(inceptionObj)).toEqual(inceptionObjString);
expect(pipe.transform(inceptionObj)).toEqual(inceptionObjString);
});
it("should return a new value when something changed but the ref hasn't", () => {
var stringCollection = '[]';
var stringCollectionWith1 = '[\n' +
' 1' +
'\n]';
expect(pipe.transform(collection)).toEqual(stringCollection);
collection.push(1);
expect(pipe.transform(collection)).toEqual(stringCollectionWith1);
});
});
describe("onDestroy", () => {
it("should do nothing when no latest value",
() => { expect(() => pipe.onDestroy()).not.toThrow(); });
});
});
}
@@ -1,57 +0,0 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {LimitToPipe} from 'angular2/src/change_detection/pipes/limit_to_pipe';
export function main() {
describe("LimitToPipe", () => {
var list;
var str;
var pipe;
beforeEach(() => {
list = [1, 2, 3, 4, 5];
str = 'tuvwxyz';
pipe = new LimitToPipe();
});
describe("supports", () => {
it("should support strings", () => { expect(pipe.supports(str)).toBe(true); });
it("should support lists", () => { expect(pipe.supports(list)).toBe(true); });
it("should not support other objects", () => {
expect(pipe.supports(new Object())).toBe(false);
expect(pipe.supports(null)).toBe(false);
});
});
describe("transform", () => {
it('should return the first X items when X is positive', () => {
expect(pipe.transform(list, [3])).toEqual([1, 2, 3]);
expect(pipe.transform(str, [3])).toEqual('tuv');
});
it('should return the last X items when X is negative', () => {
expect(pipe.transform(list, [-3])).toEqual([3, 4, 5]);
expect(pipe.transform(str, [-3])).toEqual('xyz');
});
it('should return a copy of input array if X is exceeds array length', () => {
expect(pipe.transform(list, [20])).toEqual(list);
expect(pipe.transform(list, [-20])).toEqual(list);
});
it('should return the entire string if X exceeds input length', () => {
expect(pipe.transform(str, [20])).toEqual(str);
expect(pipe.transform(str, [-20])).toEqual(str);
});
it('should not modify the input list', () => {
expect(pipe.transform(list, [3])).toEqual([1, 2, 3]);
expect(list).toEqual([1, 2, 3, 4, 5]);
});
});
});
}
@@ -1,35 +0,0 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {LowerCasePipe} from 'angular2/src/change_detection/pipes/lowercase_pipe';
export function main() {
describe("LowerCasePipe", () => {
var upper;
var lower;
var pipe;
beforeEach(() => {
lower = 'something';
upper = 'SOMETHING';
pipe = new LowerCasePipe();
});
describe("transform", () => {
it("should return lowercase", () => {
var val = pipe.transform(upper);
expect(val).toEqual(lower);
});
it("should lowercase when there is a new value", () => {
var val = pipe.transform(upper);
expect(val).toEqual(lower);
var val2 = pipe.transform('WAT');
expect(val2).toEqual('wat');
});
it("should not support other objects",
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
});
});
}
@@ -1,61 +0,0 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {
DecimalPipe,
PercentPipe,
CurrencyPipe
} from 'angular2/src/change_detection/pipes/number_pipe';
export function main() {
describe("DecimalPipe", () => {
var pipe;
beforeEach(() => { pipe = new DecimalPipe(); });
describe("transform", () => {
it('should return correct value for numbers', () => {
expect(pipe.transform(12345, [])).toEqual('12,345');
expect(pipe.transform(123, ['.2'])).toEqual('123.00');
expect(pipe.transform(1, ['3.'])).toEqual('001');
expect(pipe.transform(1.1, ['3.4-5'])).toEqual('001.1000');
expect(pipe.transform(1.123456, ['3.4-5'])).toEqual('001.12346');
expect(pipe.transform(1.1234, [])).toEqual('1.123');
});
it("should not support other objects",
() => { expect(() => pipe.transform(new Object(), [])).toThrowError(); });
});
});
describe("PercentPipe", () => {
var pipe;
beforeEach(() => { pipe = new PercentPipe(); });
describe("transform", () => {
it('should return correct value for numbers', () => {
expect(pipe.transform(1.23, [])).toEqual('123%');
expect(pipe.transform(1.2, ['.2'])).toEqual('120.00%');
});
it("should not support other objects",
() => { expect(() => pipe.transform(new Object(), [])).toThrowError(); });
});
});
describe("CurrencyPipe", () => {
var pipe;
beforeEach(() => { pipe = new CurrencyPipe(); });
describe("transform", () => {
it('should return correct value for numbers', () => {
expect(pipe.transform(123, [])).toEqual('USD123');
expect(pipe.transform(12, ['EUR', false, '.2'])).toEqual('EUR12.00');
});
it("should not support other objects",
() => { expect(() => pipe.transform(new Object(), [])).toThrowError(); });
});
});
}
@@ -1,88 +0,0 @@
import {
ddescribe,
xdescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach
} from 'angular2/test_lib';
import {Injector, bind} from 'angular2/di';
import {Pipes} from 'angular2/src/change_detection/pipes/pipes';
import {Pipe} from 'angular2/src/change_detection/pipes/pipe';
class APipe implements Pipe {
transform(a, b) {}
onDestroy() {}
}
class AnotherPipe implements Pipe {
transform(a, b) {}
onDestroy() {}
}
export function main() {
describe("pipe registry", () => {
var injector;
beforeEach(() => { injector = Injector.resolveAndCreate([]); });
it("should instantiate a pipe", () => {
var r = new Pipes({"type": APipe}, injector);
expect(r.get("type", null)).toBeAnInstanceOf(APipe);
});
it("should instantiate a new pipe every time", () => {
var r = new Pipes({"type": APipe}, injector);
var p1 = r.get("type", null);
var p2 = r.get("type", null);
expect(p1).not.toBe(p2);
});
it("should throw when no matching type", () => {
var r = new Pipes({}, null);
expect(() => r.get("unknown", null)).toThrowError(`Cannot find pipe 'unknown'.`);
});
describe('.create()', () => {
it("should create a new Pipes object", () => {
var pipes = Pipes.create({'pipe': APipe}, null);
expect(pipes.config).toEqual({'pipe': APipe});
});
it("should merge pipes config", () => {
var pipes1 = Pipes.create({'pipe': APipe, 'pipe1': APipe}, null);
var pipes2 = Pipes.create({'pipe': AnotherPipe, 'pipe2': AnotherPipe}, null, pipes1);
expect(pipes2.config).toEqual({'pipe': AnotherPipe, 'pipe1': APipe, 'pipe2': AnotherPipe});
});
it("should not change parent's config", () => {
var pipes1 = Pipes.create({'pipe': APipe, 'pipe1': APipe}, null);
Pipes.create({'pipe': AnotherPipe, 'pipe2': AnotherPipe}, null, pipes1);
expect(pipes1.config).toEqual({'pipe': APipe, 'pipe1': APipe});
});
});
describe(".extend()", () => {
it('should create a factory that prepend new pipes to old', () => {
var pipes1 = Pipes.create({'pipe': APipe, 'pipe1': APipe}, null);
var binding = Pipes.extend({'pipe': AnotherPipe, 'pipe2': AnotherPipe});
var pipes: Pipes = binding.toFactory(pipes1, injector);
expect(pipes.config).toEqual({'pipe': AnotherPipe, 'pipe1': APipe, 'pipe2': AnotherPipe});
});
it('should throw if calling extend when creating root injector', () => {
var injector = Injector.resolveAndCreate([Pipes.extend({'pipe': APipe})]);
expect(() => injector.get(Pipes))
.toThrowErrorWith("Cannot extend Pipes without a parent injector");
});
});
});
}
@@ -1,36 +0,0 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {UpperCasePipe} from 'angular2/src/change_detection/pipes/uppercase_pipe';
export function main() {
describe("UpperCasePipe", () => {
var upper;
var lower;
var pipe;
beforeEach(() => {
lower = 'something';
upper = 'SOMETHING';
pipe = new UpperCasePipe();
});
describe("transform", () => {
it("should return uppercase", () => {
var val = pipe.transform(lower);
expect(val).toEqual(upper);
});
it("should uppercase when there is a new value", () => {
var val = pipe.transform(lower);
expect(val).toEqual(upper);
var val2 = pipe.transform('wat');
expect(val2).toEqual('WAT');
});
it("should not support other objects",
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
});
});
}