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,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");
});
});
});
}
@@ -22,7 +22,8 @@ import {Compiler, CompilerCache} from 'angular2/src/core/compiler/compiler';
import {AppProtoView} from 'angular2/src/core/compiler/view';
import {ElementBinder} from 'angular2/src/core/compiler/element_binder';
import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver';
import {Attribute, View, Component, Directive} from 'angular2/annotations';
import {PipeResolver} from 'angular2/src/core/compiler/pipe_resolver';
import {Attribute, View, Component, Directive, Pipe} from 'angular2/annotations';
import * as viewAnn from 'angular2/src/core/annotations_impl/view';
import {internalProtoView} from 'angular2/src/core/compiler/view_ref';
import {DirectiveBinding} from 'angular2/src/core/compiler/element_injector';
@@ -38,11 +39,14 @@ import {AppRootUrl} from 'angular2/src/services/app_root_url';
import * as renderApi from 'angular2/src/render/api';
// TODO(tbosch): Spys don't support named modules...
import {RenderCompiler} from 'angular2/src/render/api';
import {PipeBinding} from 'angular2/src/core/pipes/pipe_binding';
export function main() {
describe('compiler', function() {
var directiveResolver, tplResolver, renderCompiler, protoViewFactory, cmpUrlMapper,
rootProtoView;
var directiveResolver, pipeResolver, tplResolver, renderCompiler, protoViewFactory,
cmpUrlMapper, rootProtoView;
var renderCompileRequests: any[];
function createCompiler(renderCompileResults:
@@ -57,13 +61,14 @@ export function main() {
});
protoViewFactory = new FakeProtoViewFactory(protoViewFactoryResults);
return new Compiler(directiveResolver, new CompilerCache(), tplResolver, cmpUrlMapper,
urlResolver, renderCompiler, protoViewFactory,
return new Compiler(directiveResolver, pipeResolver, [SomeDefaultPipe], new CompilerCache(),
tplResolver, cmpUrlMapper, urlResolver, renderCompiler, protoViewFactory,
new AppRootUrl("http://www.app.com"));
}
beforeEach(() => {
directiveResolver = new DirectiveResolver();
pipeResolver = new PipeResolver();
tplResolver = new FakeViewResolver();
cmpUrlMapper = new RuntimeComponentUrlMapper();
renderCompiler = new SpyRenderCompiler();
@@ -304,6 +309,20 @@ export function main() {
});
}));
it('should pass the pipe bindings', inject([AsyncTestCompleter], (async) => {
tplResolver.setView(MainComponent,
new viewAnn.View({template: '<div></div>', pipes: [SomePipe]}));
var compiler =
createCompiler([createRenderProtoView()], [rootProtoView, createProtoView()]);
compiler.compileInHost(MainComponent)
.then((_) => {
var request = protoViewFactory.requests[1];
expect(request[3][0].key.token).toBe(SomeDefaultPipe);
expect(request[3][1].key.token).toBe(SomePipe);
async.done();
});
}));
it('should use the protoView of the ProtoViewFactory',
inject([AsyncTestCompleter], (async) => {
tplResolver.setView(MainComponent, new viewAnn.View({template: '<div></div>'}));
@@ -399,9 +418,9 @@ export function main() {
var reader: any = new SpyDirectiveResolver();
// create the compiler
var compiler =
new Compiler(reader, cache, tplResolver, cmpUrlMapper, new UrlResolver(),
renderCompiler, protoViewFactory, new AppRootUrl("http://www.app.com"));
var compiler = new Compiler(reader, pipeResolver, [], cache, tplResolver, cmpUrlMapper,
new UrlResolver(), renderCompiler, protoViewFactory,
new AppRootUrl("http://www.app.com"));
compiler.compileInHost(MainComponent)
.then((protoViewRef) => {
// the test should have failed if the resolver was called, so we're good
@@ -570,7 +589,7 @@ function createProtoView(elementBinders = null, type: renderApi.ViewType = null,
type = renderApi.ViewType.COMPONENT;
}
var pv = new AppProtoView(type, isEmbeddedFragment, new renderApi.RenderProtoViewRef(), null,
null, new Map(), null);
null, new Map(), null, null);
if (isBlank(elementBinders)) {
elementBinders = [];
}
@@ -653,6 +672,14 @@ class DirectiveWithProperties {
class DirectiveWithBind {
}
@Pipe({name: 'some-default-pipe'})
class SomeDefaultPipe {
}
@Pipe({name: 'some-pipe'})
class SomePipe {
}
@Directive({selector: 'directive-with-accts'})
class DirectiveWithAttributes {
constructor(@Attribute('someAttr') someAttr: String) {}
@@ -694,8 +721,8 @@ class FakeProtoViewFactory extends ProtoViewFactory {
}
createAppProtoViews(componentBinding: DirectiveBinding, renderProtoView: renderApi.ProtoViewDto,
directives: List<DirectiveBinding>): AppProtoView[] {
this.requests.push([componentBinding, renderProtoView, directives]);
directives: List<DirectiveBinding>, pipes: PipeBinding[]): AppProtoView[] {
this.requests.push([componentBinding, renderProtoView, directives, pipes]);
return collectEmbeddedPvs(ListWrapper.removeAt(this.results, 0));
}
}
@@ -192,14 +192,17 @@ class OptionallyInjectsTemplateRef {
@Injectable()
class DirectiveNeedsChangeDetectorRef {
changeDetectorRef;
constructor(cdr: ChangeDetectorRef) { this.changeDetectorRef = cdr; }
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
@Injectable()
class ComponentNeedsChangeDetectorRef {
changeDetectorRef;
constructor(cdr: ChangeDetectorRef) { this.changeDetectorRef = cdr; }
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
@Injectable()
class PipeNeedsChangeDetectorRef {
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
class A_Needs_B {
@@ -55,15 +55,14 @@ import {
SkipSelf,
SkipSelfMetadata
} from 'angular2/di';
import {
Pipes,
defaultPipes,
Pipe,
PipeTransform,
ChangeDetectorRef,
ON_PUSH
} from 'angular2/src/change_detection/change_detection';
import {Directive, Component, View, Attribute, Query} from 'angular2/annotations';
import {Directive, Component, View, Attribute, Query, Pipe} from 'angular2/annotations';
import * as viewAnn from 'angular2/src/core/annotations_impl/view';
import {QueryList} from 'angular2/src/core/compiler/query_list';
@@ -98,6 +97,7 @@ export function main() {
rootTC.detectChanges();
expect(rootTC.nativeElement).toHaveText('Hello World!');
async.done();
});
}));
@@ -242,12 +242,13 @@ export function main() {
it("should support pipes in bindings",
inject([TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async) => {
tcb.overrideView(MyCompWithPipes, new viewAnn.View({
tcb.overrideView(MyComp, new viewAnn.View({
template: '<div my-dir #dir="mydir" [elprop]="ctxProp | double"></div>',
directives: [MyDir]
directives: [MyDir],
pipes: [DoublePipe]
}))
.createAsync(MyCompWithPipes)
.createAsync(MyComp)
.then((rootTC) => {
rootTC.componentInstance.ctxProp = 'a';
rootTC.detectChanges();
@@ -1661,21 +1662,6 @@ class PushCmpWithAsyncPipe {
resolve(value) { this.completer.resolve(value); }
}
@Injectable()
class PipesWithDouble extends Pipes {
constructor(injector: Injector) { super({"double": DoublePipe}, injector); }
}
@Component({
selector: 'my-comp-with-pipes',
viewBindings: [new Binding(Pipes, {toClass: PipesWithDouble})]
})
@View({directives: []})
@Injectable()
class MyCompWithPipes {
ctxProp: string = "initial value";
}
@Component({selector: 'my-comp'})
@View({directives: []})
@Injectable()
@@ -1754,8 +1740,8 @@ class SomeViewport {
}
}
@Injectable()
class DoublePipe implements Pipe {
@Pipe({name: 'double'})
class DoublePipe implements PipeTransform {
onDestroy() {}
transform(value, args = null) { return `${value}${value}`; }
}
@@ -70,7 +70,7 @@ export function main() {
varBindings.set('a', 'b');
var renderPv = createRenderProtoView([], null, varBindings);
var appPvs =
protoViewFactory.createAppProtoViews(bindDirective(MainComponent), renderPv, []);
protoViewFactory.createAppProtoViews(bindDirective(MainComponent), renderPv, [], []);
expect(appPvs[0].variableBindings.get('a')).toEqual('b');
expect(appPvs.length).toBe(1);
});
@@ -318,7 +318,7 @@ function _createProtoView(type: ViewType, binders: ElementBinder[] = null) {
}
var protoChangeDetector = <any>new SpyProtoChangeDetector();
protoChangeDetector.spy('instantiate').andReturn(new SpyChangeDetector());
var res = new AppProtoView(type, null, null, protoChangeDetector, null, null, 0);
var res = new AppProtoView(type, null, null, protoChangeDetector, null, null, 0, null);
res.elementBinders = binders;
var mappedElementIndices = ListWrapper.createFixedSize(countNestedElementBinders(res));
for (var i = 0; i < binders.length; i++) {
@@ -25,7 +25,7 @@ export function main() {
function createViewPool({capacity}): AppViewPool { return new AppViewPool(capacity); }
function createProtoView() {
return new AppProtoView(null, null, null, null, null, null, null);
return new AppProtoView(null, null, null, null, null, null, null, null);
}
function createView(pv) {
@@ -0,0 +1,27 @@
import {
ddescribe,
xdescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach
} from 'angular2/test_lib';
import {PipeBinding} from 'angular2/src/core/pipes/pipe_binding';
import {Pipe} from 'angular2/src/core/annotations_impl/annotations';
class MyPipe {}
export function main() {
describe("PipeBinding", () => {
it('should create a binding out of a type', () => {
var binding = PipeBinding.createFromType(MyPipe, new Pipe({name: 'my-pipe'}));
expect(binding.name).toEqual('my-pipe');
expect(binding.factory()).toBeAnInstanceOf(MyPipe);
expect(binding.dependencies.length).toEqual(0);
});
});
}
@@ -0,0 +1,56 @@
import {
ddescribe,
xdescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach
} from 'angular2/test_lib';
import {PipeTransform} from 'angular2/change_detection';
import {Injector, Inject, bind} from 'angular2/di';
import {ProtoPipes, Pipes} from 'angular2/src/core/pipes/pipes';
import {PipeBinding} from 'angular2/src/core/pipes/pipe_binding';
import {Pipe} from 'angular2/src/core/annotations_impl/annotations';
class PipeA implements PipeTransform {
transform(a, b) {}
onDestroy() {}
}
class PipeB implements PipeTransform {
dep;
constructor(@Inject("dep") dep: any) { this.dep = dep; }
transform(a, b) {}
onDestroy() {}
}
export function main() {
describe("Pipes", () => {
var injector;
beforeEach(
() => { injector = Injector.resolveAndCreate([bind('dep').toValue('dependency')]); });
it('should instantiate a pipe', () => {
var proto = new ProtoPipes([PipeBinding.createFromType(PipeA, new Pipe({name: 'a'}))]);
var pipes = new Pipes(proto, injector);
expect(pipes.get("a")).toBeAnInstanceOf(PipeA);
});
it('should throw when no pipe found', () => {
var proto = new ProtoPipes([]);
var pipes = new Pipes(proto, injector);
expect(() => pipes.get("invalid")).toThrowErrorWith("Cannot find pipe 'invalid'");
});
it('should inject dependencies from the provided injector', () => {
var proto = new ProtoPipes([PipeBinding.createFromType(PipeB, new Pipe({name: 'b'}))]);
var pipes = new Pipes(proto, injector);
expect(pipes.get("b").dep).toEqual("dependency");
});
});
}
@@ -14,8 +14,8 @@ import {
} 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 {WrappedValue} from 'angular2/change_detection';
import {AsyncPipe} from 'angular2/pipes';
import {
EventEmitter,
ObservableWrapper,
@@ -1,6 +1,6 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {DatePipe} from 'angular2/src/change_detection/pipes/date_pipe';
import {DatePipe} from 'angular2/pipes';
import {DateWrapper} from 'angular2/src/facade/lang';
export function main() {
@@ -15,7 +15,7 @@ import {
} from 'angular2/test_lib';
import {Json, RegExp, NumberWrapper, StringWrapper} from 'angular2/src/facade/lang';
import {JsonPipe} from 'angular2/src/change_detection/pipes/json_pipe';
import {JsonPipe} from 'angular2/pipes';
export function main() {
describe("JsonPipe", () => {
@@ -1,6 +1,6 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {LimitToPipe} from 'angular2/src/change_detection/pipes/limit_to_pipe';
import {LimitToPipe} from 'angular2/pipes';
export function main() {
describe("LimitToPipe", () => {
@@ -1,6 +1,6 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {LowerCasePipe} from 'angular2/src/change_detection/pipes/lowercase_pipe';
import {LowerCasePipe} from 'angular2/pipes';
export function main() {
describe("LowerCasePipe", () => {
@@ -1,10 +1,6 @@
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';
import {DecimalPipe, PercentPipe, CurrencyPipe} from 'angular2/pipes';
export function main() {
describe("DecimalPipe", () => {
@@ -18,6 +14,7 @@ export function main() {
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');
});
@@ -1,6 +1,6 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {UpperCasePipe} from 'angular2/src/change_detection/pipes/uppercase_pipe';
import {UpperCasePipe} from 'angular2/pipes';
export function main() {
describe("UpperCasePipe", () => {
@@ -26,9 +26,17 @@ import {
routerDirectives
} from 'angular2/router';
import {ExceptionHandler} from 'angular2/src/core/exception_handler';
import {LocationStrategy} from 'angular2/src/router/location_strategy';
import {MockLocationStrategy} from 'angular2/src/mock/mock_location_strategy';
class _ArrayLogger {
res: any[] = [];
log(s: any): void { this.res.push(s); }
logGroup(s: any): void { this.res.push(s); }
logGroupEnd(){};
}
export function main() {
describe('RouteConfig with POJO arguments', () => {
var fakeDoc, el, testBindings;
@@ -36,10 +44,13 @@ export function main() {
fakeDoc = DOM.createHtmlDocument();
el = DOM.createElement('app-cmp', fakeDoc);
DOM.appendChild(fakeDoc.body, el);
var logger = new _ArrayLogger();
var exceptionHandler = new ExceptionHandler(logger, true);
testBindings = [
routerInjectables,
bind(LocationStrategy).toClass(MockLocationStrategy),
bind(DOCUMENT_TOKEN).toValue(fakeDoc)
bind(DOCUMENT_TOKEN).toValue(fakeDoc),
bind(ExceptionHandler).toValue(exceptionHandler)
];
});