diff --git a/modules/angular2/src/change_detection/pipes/pipes.ts b/modules/angular2/src/change_detection/pipes/pipes.ts index b6ac1745915890..5bab56394ea510 100644 --- a/modules/angular2/src/change_detection/pipes/pipes.ts +++ b/modules/angular2/src/change_detection/pipes/pipes.ts @@ -1,13 +1,32 @@ -import {List, ListWrapper} from 'angular2/src/facade/collection'; +import {ListWrapper, isListLikeIterable, StringMapWrapper} from 'angular2/src/facade/collection'; import {isBlank, isPresent, BaseException, CONST} from 'angular2/src/facade/lang'; import {Pipe, PipeFactory} from './pipe'; -import {Injectable} from 'angular2/src/di/decorators'; +import {Injectable, UnboundedMetadata, OptionalMetadata} from 'angular2/di'; import {ChangeDetectorRef} from '../change_detector_ref'; +import {Binding} from 'angular2/di'; @Injectable() @CONST() export class Pipes { - constructor(public config) {} + /** + * Map of {@link Pipe} names to {@link PipeFactory} lists used to configure the + * {@link Pipes} registry. + * + * #Example + * + * ``` + * var pipesConfig = { + * 'json': [jsonPipeFactory] + * } + * @Component({ + * viewInjector: [ + * bind(Pipes).toValue(new Pipes(pipesConfig)) + * ] + * }) + * ``` + */ + config: StringMap; + constructor(config: StringMap) { this.config = config; } get(type: string, obj, cdRef?: ChangeDetectorRef, existingPipe?: Pipe): Pipe { if (isPresent(existingPipe) && existingPipe.supports(obj)) return existingPipe; @@ -20,6 +39,65 @@ export class Pipes { return factory.create(cdRef); } + /** + * Takes a {@link Pipes} config object and returns a binding used to append the + * provided config to an inherited {@link Pipes} instance and return a new + * {@link Pipes} instance. + * + * If the provided config contains a key that is not yet present in the + * inherited {@link Pipes}' config, a new {@link PipeFactory} list will be created + * for that key. Otherwise, the provided config will be merged with the inherited + * {@link Pipes} instance by appending pipes to their respective keys, without mutating + * the inherited {@link Pipes}. + * + * The following example shows how to append a new {@link PipeFactory} to the + * existing list of `async` factories, which will only be applied to the injector + * for this component and its children. This step is all that's required to make a new + * pipe available to this component's template. + * + * # Example + * + * ``` + * @Component({ + * viewInjector: [ + * Pipes.append({ + * async: [newAsyncPipe] + * }) + * ] + * }) + * ``` + */ + static append(config): Binding { + return new Binding(Pipes, { + toFactory: (pipes: Pipes) => { + if (!isPresent(pipes)) { + // Typically would occur when calling Pipe.append inside of dependencies passed to + // bootstrap(), which would override default pipes instead of append. + throw new BaseException('Cannot append to Pipes without a parent injector'); + } + var mergedConfig: StringMap = >{}; + + // Manual deep copy of existing Pipes config, + // so that lists of PipeFactories don't get mutated. + StringMapWrapper.forEach(pipes.config, (v: PipeFactory[], k: string) => { + var localPipeList: PipeFactory[] = mergedConfig[k] = []; + v.forEach((p: PipeFactory) => { localPipeList.push(p); }); + }); + + StringMapWrapper.forEach(config, (v: PipeFactory[], k: string) => { + if (isListLikeIterable(mergedConfig[k])) { + mergedConfig[k] = ListWrapper.concat(mergedConfig[k], config[k]); + } else { + mergedConfig[k] = config[k]; + } + }); + return new Pipes(mergedConfig); + }, + // Dependency technically isn't optional, but we can provide a better error message this way. + deps: [[Pipes, new UnboundedMetadata(), new OptionalMetadata()]] + }) + } + private _getListOfFactories(type: string, obj: any): PipeFactory[] { var listOfFactories = this.config[type]; if (isBlank(listOfFactories)) { diff --git a/modules/angular2/test/change_detection/pipes/pipes_spec.ts b/modules/angular2/test/change_detection/pipes/pipes_spec.ts index ac1984eec2a597..e5773347e0b49b 100644 --- a/modules/angular2/test/change_detection/pipes/pipes_spec.ts +++ b/modules/angular2/test/change_detection/pipes/pipes_spec.ts @@ -11,7 +11,9 @@ import { SpyPipeFactory } from 'angular2/test_lib'; +import {Injector, bind} from 'angular2/di'; import {Pipes} from 'angular2/src/change_detection/pipes/pipes'; +import {PipeFactory} from 'angular2/src/change_detection/pipes/pipe'; export function main() { describe("pipe registry", () => { @@ -73,5 +75,49 @@ export function main() { expect(() => r.get("type", "some object")) .toThrowError(`Cannot find 'type' pipe supporting object 'some object'`); }); + + describe('.append()', () => { + it('should create a factory that appends new pipes to old', () => { + firstPipeFactory.spy("supports").andReturn(false); + secondPipeFactory.spy("supports").andReturn(true); + secondPipeFactory.spy("create").andReturn(secondPipe); + var originalPipes: Pipes = new Pipes({'async': [firstPipeFactory]}); + var binding = Pipes.append({'async':[secondPipeFactory]}); + var pipes: Pipes = binding.toFactory(originalPipes); + + expect(pipes.config['async'].length).toBe(2); + expect(originalPipes.config['async'].length).toBe(1); + expect(pipes.get('async', 'second plz')).toBe(secondPipe); + }); + + + it('should append to di-inherited pipes', () => { + firstPipeFactory.spy("supports").andReturn(false); + secondPipeFactory.spy("supports").andReturn(true); + secondPipeFactory.spy("create").andReturn(secondPipe); + + var originalPipes: Pipes = new Pipes({'async': [firstPipeFactory]}); + var injector: Injector = Injector.resolveAndCreate([bind(Pipes).toValue(originalPipes)]); + var childInjector: Injector = + injector.resolveAndCreateChild([Pipes.append({'async': [secondPipeFactory]})]); + var parentPipes: Pipes = injector.get(Pipes); + var childPipes: Pipes = childInjector.get(Pipes); + expect(childPipes.config['async'].length).toBe(2); + expect(parentPipes.config['async'].length).toBe(1); + expect(childPipes.get('async', 'second plz')).toBe(secondPipe); + }); + + + it('should throw if calling append when creating root injector', () => { + secondPipeFactory.spy("supports").andReturn(true); + secondPipeFactory.spy("create").andReturn(secondPipe); + + var injector: Injector = + Injector.resolveAndCreate([Pipes.append({'async': [secondPipeFactory]})]); + + expect(() => injector.get(Pipes)) + .toThrowError(/Cannot append to Pipes without a parent injector/g); + }); + }); }); -} \ No newline at end of file +}