diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 2c206026791e2..e46334d35fd6c 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -133,8 +133,9 @@ export function renderComponent( clean: CLEAN_PROMISE, }; const rootView = createLView( - -1, rendererFactory.createRenderer(hostNode, componentDef.rendererType), createTView(null), - null, rootContext, componentDef.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways); + -1, rendererFactory.createRenderer(hostNode, componentDef.rendererType), + createTView(null, null), null, rootContext, + componentDef.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways); const oldView = enterView(rootView, null !); let elementNode: LElementNode; diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index 354ebfd1c9f0a..fd8d3172381ac 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -16,7 +16,7 @@ import {Type} from '../type'; import {resolveRendererType2} from '../view/util'; import {diPublic} from './di'; -import {ComponentDef, ComponentDefFeature, ComponentTemplate, DirectiveDef, DirectiveDefFeature, DirectiveDefListOrFactory, PipeDef} from './interfaces/definition'; +import {ComponentDef, ComponentDefFeature, ComponentTemplate, DirectiveDef, DirectiveDefFeature, DirectiveDefListOrFactory, PipeDef, PipeDefListOrFactory} from './interfaces/definition'; import {CssSelector} from './interfaces/projection'; @@ -154,6 +154,14 @@ export function defineComponent(componentDefinition: { * `DirectiveDef`s. The function is necessary to be able to support forward declarations. */ directiveDefs?: DirectiveDefListOrFactory | null; + + /** + * Registry of pipes that may be found in this component's view. + * + * The property is either an array of `PipeDefs`s or a function which returns the array of + * `PipeDefs`s. The function is necessary to be able to support forward declarations. + */ + pipeDefs?: PipeDefListOrFactory | null; }): ComponentDef { const type = componentDefinition.type; const def = >{ @@ -176,6 +184,7 @@ export function defineComponent(componentDefinition: { onDestroy: type.prototype.ngOnDestroy || null, onPush: componentDefinition.changeDetection === ChangeDetectionStrategy.OnPush, directiveDefs: componentDefinition.directiveDefs || null, + pipeDefs: componentDefinition.pipeDefs || null, selector: componentDefinition.selector }; const feature = componentDefinition.features; @@ -380,15 +389,25 @@ export const defineDirective = defineComponent as any as(directiveDefinition: * }); * } * ``` - * @param type Pipe class reference. Needed to extract pipe lifecycle hooks. - * @param factory A factory for creating a pipe instance. - * @param pure Whether the pipe is pure. + * @param pipeDef Pipe definition generated by the compiler */ -export function definePipe( - {type, factory, pure}: {type: Type, factory: () => T, pure?: boolean}): PipeDef { +export function definePipe(pipeDef: { + /** Name of the pipe. Used for matching pipes in template to pipe defs. */ + name: string, + + /** Pipe class reference. Needed to extract pipe lifecycle hooks. */ + type: Type, + + /** A factory for creating a pipe instance. */ + factory: () => T, + + /** Whether the pipe is pure. */ + pure?: boolean +}): PipeDef { return >{ - n: factory, - pure: pure !== false, - onDestroy: type.prototype.ngOnDestroy || null + name: pipeDef.name, + n: pipeDef.factory, + pure: pipeDef.pure !== false, + onDestroy: pipeDef.type.prototype.ngOnDestroy || null }; } diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 220717ac6a830..2ce1172f10107 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -19,7 +19,7 @@ import {LContainerNode, LElementNode, LNode, LNodeType, TNodeFlags, LProjectionN import {assertNodeType} from './node_assert'; import {appendChild, insertChild, insertView, appendProjectedNode, removeView, canInsertNativeNode} from './node_manipulation'; import {isNodeMatchingSelector, matchingSelectorIndex} from './node_selector_matcher'; -import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefList, DirectiveDefListOrFactory, DirectiveType} from './interfaces/definition'; +import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefList, DirectiveDefListOrFactory, DirectiveType, PipeDef, PipeDefListOrFactory} from './interfaces/definition'; import {RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, ObjectOrientedRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; import {isDifferent, stringify} from './util'; import {executeHooks, queueLifecycleHooks, queueInitHooks, executeInitHooks} from './hooks'; @@ -400,20 +400,22 @@ function resetApplicationState() { * @param context to pass into the template. * @param providedRendererFactory renderer factory to use * @param host The host element node to use - * @param directiveRegistry Any directive defs that should be used to match nodes to directives + * @param defs Any directive or pipe defs that should be used for matching */ export function renderTemplate( hostNode: RElement, template: ComponentTemplate, context: T, providedRendererFactory: RendererFactory3, host: LElementNode | null, - directiveRegistry: DirectiveDefListOrFactory | null = null): LElementNode { + directives?: DirectiveDefListOrFactory | null, + pipes?: PipeDefListOrFactory | null): LElementNode { if (host == null) { resetApplicationState(); rendererFactory = providedRendererFactory; + const tView = getOrCreateTView(template, directives || null, pipes || null); host = createLNode( null, LNodeType.Element, hostNode, createLView( - -1, providedRendererFactory.createRenderer(null, null), - getOrCreateTView(template, directiveRegistry), null, {}, LViewFlags.CheckAlways)); + -1, providedRendererFactory.createRenderer(null, null), tView, null, {}, + LViewFlags.CheckAlways)); } const hostView = host.data !; ngDevMode && assertNotNull(hostView, 'Host node should have an LView defined in host.data.'); @@ -432,9 +434,11 @@ export function renderEmbeddedTemplate( let cm: boolean = false; if (viewNode == null) { // TODO: revisit setting currentView when re-writing view containers + const directives = currentView && currentView.tView.directiveRegistry; + const pipes = currentView && currentView.tView.pipeRegistry; + const view = createLView( - -1, renderer, createTView(currentView && currentView.tView.directiveRegistry), template, - context, LViewFlags.CheckAlways); + -1, renderer, createTView(directives, pipes), template, context, LViewFlags.CheckAlways); viewNode = createLNode(null, LNodeType.View, null, view); cm = true; } @@ -635,15 +639,20 @@ function findMatchingLocalNames( * if it doesn't already exist. * * @param template The template from which to get static data + * @param directives Directive defs that should be saved on TView + * @param pipes Pipe defs that should be saved on TView * @returns TView */ function getOrCreateTView( - template: ComponentTemplate, defs: DirectiveDefListOrFactory | null): TView { - return template.ngPrivateData || (template.ngPrivateData = createTView(defs) as never); + template: ComponentTemplate, directives: DirectiveDefListOrFactory | null, + pipes: PipeDefListOrFactory | null): TView { + return template.ngPrivateData || + (template.ngPrivateData = createTView(directives, pipes) as never); } /** Creates a TView instance */ -export function createTView(defs: DirectiveDefListOrFactory | null): TView { +export function createTView( + defs: DirectiveDefListOrFactory | null, pipes: PipeDefListOrFactory | null): TView { return { data: [], directives: null, @@ -658,7 +667,8 @@ export function createTView(defs: DirectiveDefListOrFactory | null): TView { pipeDestroyHooks: null, hostBindings: null, components: null, - directiveRegistry: typeof defs === 'function' ? defs() : defs + directiveRegistry: typeof defs === 'function' ? defs() : defs, + pipeRegistry: typeof pipes === 'function' ? pipes() : pipes }; } @@ -720,7 +730,7 @@ export function hostElement( const node = createLNode( 0, LNodeType.Element, rNode, createLView( - -1, renderer, getOrCreateTView(def.template, def.directiveDefs), null, null, + -1, renderer, getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs), null, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways)); if (firstTemplatePass) { @@ -1159,7 +1169,7 @@ export function directiveCreate( function addComponentLogic( index: number, elementIndex: number, instance: T, def: ComponentDef): void { - const tView = getOrCreateTView(def.template, def.directiveDefs); + const tView = getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs); // Only component views should be added to the view tree directly. Embedded views are // accessed through their containers because they may be removed / re-added later. @@ -1466,7 +1476,8 @@ function getOrCreateEmbeddedTView(viewIndex: number, parent: LContainerNode): TV ngDevMode && assertNodeType(parent, LNodeType.Container); const tContainer = (parent !.tNode as TContainerNode).data; if (viewIndex >= tContainer.length || tContainer[viewIndex] == null) { - tContainer[viewIndex] = createTView(currentView.tView.directiveRegistry); + const tView = currentView.tView; + tContainer[viewIndex] = createTView(tView.directiveRegistry, tView.pipeRegistry); } return tContainer[viewIndex]; } diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 3b81790aee2e9..5c864f3024a30 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -161,6 +161,14 @@ export interface ComponentDef extends DirectiveDef { * `DirectiveDef`s. The function is necessary to be able to support forward declarations. */ directiveDefs: DirectiveDefListOrFactory|null; + + /** + * Registry of pipes that may be found in this view. + * + * The property is either an array of `PipeDefs`s or a function which returns the array of + * `PipeDefs`s. The function is necessary to be able to support forward declarations. + */ + pipeDefs: PipeDefListOrFactory|null; } /** @@ -176,6 +184,13 @@ export interface ComponentDef extends DirectiveDef { * See: {@link definePipe} */ export interface PipeDef { + /** + * Pipe name. + * + * Used to resolve pipe in templates. + */ + name: string; + /** * factory function used to create a new directive instance. * @@ -208,6 +223,15 @@ export type DirectiveDefListOrFactory = (() => DirectiveDefList) | DirectiveDefL export type DirectiveDefList = (DirectiveDef| ComponentDef)[]; +/** + * Type used for PipeDefs on component definition. + * + * The function is necessary to be able to support forward declarations. + */ +export type PipeDefListOrFactory = (() => PipeDefList) | PipeDefList; + +export type PipeDefList = PipeDef[]; + // Note: This hack is necessary so we don't erroneously get a circular dependency // failure based on types. export const unusedValueExportToPlacateAjd = 1; diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 6166d53918c7b..87ccc9e080dd0 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -7,7 +7,7 @@ */ import {LContainer} from './container'; -import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDef} from './definition'; +import {ComponentTemplate, DirectiveDefList, PipeDef, PipeDefList} from './definition'; import {LElementNode, LViewNode, TNode} from './node'; import {LQueries} from './query'; import {Renderer3} from './renderer'; @@ -245,6 +245,17 @@ export interface TView { */ directiveRegistry: DirectiveDefList|null; + /** + * Full registry of pipes that may be found in this view. + * + * The property is either an array of `PipeDefs`s or a function which returns the array of + * `PipeDefs`s. The function is necessary to be able to support forward declarations. + * + * It's necessary to keep a copy of the full def list on the TView so it's possible + * to render template functions without a host component. + */ + pipeRegistry: PipeDefList|null; + /** * Array of ngOnInit and ngDoCheck hooks that should be executed for this view in * creation mode. diff --git a/packages/core/src/render3/pipe.ts b/packages/core/src/render3/pipe.ts index 8d16c3007590b..156cfb4143201 100644 --- a/packages/core/src/render3/pipe.ts +++ b/packages/core/src/render3/pipe.ts @@ -9,10 +9,9 @@ import {PipeTransform} from '../change_detection/pipe_transform'; import {getTView, load, store} from './instructions'; -import {PipeDef} from './interfaces/definition'; +import {PipeDef, PipeDefList} from './interfaces/definition'; import {pureFunction1, pureFunction2, pureFunction3, pureFunction4, pureFunctionV} from './pure_function'; - /** * Create a pipe. * @@ -21,19 +20,45 @@ import {pureFunction1, pureFunction2, pureFunction3, pureFunction4, pureFunction * @param firstInstance (optional) The first instance of the pipe that can be reused for pure pipes. * @returns T the instance of the pipe. */ -export function pipe(index: number, pipeDef: PipeDef, firstInstance?: T): T { +export function pipe(index: number, pipeName: string, firstInstance?: any): any { const tView = getTView(); + let pipeDef: PipeDef; + if (tView.firstTemplatePass) { + pipeDef = getPipeDef(pipeName, tView.pipeRegistry); tView.data[index] = pipeDef; if (pipeDef.onDestroy) { (tView.pipeDestroyHooks || (tView.pipeDestroyHooks = [])).push(index, pipeDef.onDestroy); } + } else { + pipeDef = tView.data[index] as PipeDef; } + const pipeInstance = pipeDef.pure && firstInstance ? firstInstance : pipeDef.n(); store(index, pipeInstance); return pipeInstance; } +/** + * Searches the pipe registry for a pipe with the given name. If one is found, + * returns the pipe. Otherwise, an error is thrown because the pipe cannot be resolved. + * + * @param name Name of pipe to resolve + * @param registry Full list of available pipes + * @returns Matching PipeDef + */ +function getPipeDef(name: string, registry: PipeDefList | null): PipeDef { + if (registry) { + for (let i = 0; i < registry.length; i++) { + const pipeDef = registry[i]; + if (name === pipeDef.name) { + return pipeDef; + } + } + } + throw new Error(`Pipe with name '${name}' not found!`); +} + /** * Invokes a pipe with 1 arguments. * diff --git a/packages/core/test/render3/compiler_canonical/pipes_spec.ts b/packages/core/test/render3/compiler_canonical/pipes_spec.ts index 301d551dbbe48..d4ebe21be7546 100644 --- a/packages/core/test/render3/compiler_canonical/pipes_spec.ts +++ b/packages/core/test/render3/compiler_canonical/pipes_spec.ts @@ -38,6 +38,7 @@ describe('pipes', () => { // NORMATIVE static ngPipeDef = $r3$.ɵdefinePipe({ + name: 'myPipe', type: MyPipe, factory: function MyPipe_Factory() { return new MyPipe(); }, pure: false, @@ -57,6 +58,7 @@ describe('pipes', () => { // NORMATIVE static ngPipeDef = $r3$.ɵdefinePipe({ + name: 'myPurePipe', type: MyPurePipe, factory: function MyPurePipe_Factory() { return new MyPurePipe(); }, pure: true, @@ -64,11 +66,6 @@ describe('pipes', () => { // /NORMATIVE } - // NORMATIVE - const $MyPurePipe_ngPipeDef$ = MyPurePipe.ngPipeDef; - const $MyPipe_ngPipeDef$ = MyPipe.ngPipeDef; - // /NORMATIVE - it('should render pipes', () => { type $MyApp$ = MyApp; myPipeTransformCalls = 0; @@ -87,8 +84,8 @@ describe('pipes', () => { template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) { if (cm) { $r3$.ɵT(0); - $r3$.ɵPp(1, $MyPipe_ngPipeDef$); - $r3$.ɵPp(2, $MyPurePipe_ngPipeDef$); + $r3$.ɵPp(1, 'myPipe'); + $r3$.ɵPp(2, 'myPurePipe'); } $r3$.ɵt(0, $r3$.ɵi1('', $r3$.ɵpb2(1, $r3$.ɵpb2(2, ctx.name, ctx.size), ctx.size), '')); } @@ -96,6 +93,10 @@ describe('pipes', () => { // /NORMATIVE } + // NON-NORMATIVE + MyApp.ngComponentDef.pipeDefs = () => [MyPurePipe.ngPipeDef, MyPipe.ngPipeDef]; + // /NON-NORMATIVE + let myApp: MyApp = renderComponent(MyApp); expect(toHtml(containerEl)).toEqual('World!'); expect(myPurePipeTransformCalls).toEqual(1); @@ -115,7 +116,6 @@ describe('pipes', () => { it('should render many pipes and forward the first instance (pure or impure pipe)', () => { type $MyApp$ = MyApp; - type $MyPurePipe$ = MyPurePipe; myPipeTransformCalls = 0; myPurePipeTransformCalls = 0; @@ -155,12 +155,12 @@ describe('pipes', () => { selector: [[['my-app'], null]], factory: function MyApp_Factory() { return new MyApp(); }, template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) { - let $pi$: $MyPurePipe$; + let $pi$: $any$; if (cm) { $r3$.ɵT(0); - $pi$ = $r3$.ɵPp(1, $MyPurePipe_ngPipeDef$); + $pi$ = $r3$.ɵPp(1, 'myPurePipe'); $r3$.ɵT(2); - $r3$.ɵPp(3, $MyPurePipe_ngPipeDef$, $pi$); + $r3$.ɵPp(3, 'myPurePipe', $pi$); $r3$.ɵC(4, C4, '', ['oneTimeIf', '']); } $r3$.ɵt(0, $r3$.ɵi1('', $r3$.ɵpb2(1, ctx.name, ctx.size), '')); @@ -173,7 +173,7 @@ describe('pipes', () => { if (cm) { $r3$.ɵE(0, 'div'); $r3$.ɵT(1); - $r3$.ɵPp(2, $MyPurePipe_ngPipeDef$, $pi$); + $r3$.ɵPp(2, 'myPurePipe', $pi$); $r3$.ɵe(); } $r3$.ɵt(1, $r3$.ɵi1('', $r3$.ɵpb2(2, ctx.name, ctx.size), '')); @@ -185,6 +185,7 @@ describe('pipes', () => { // NON-NORMATIVE MyApp.ngComponentDef.directiveDefs = [OneTimeIf.ngDirectiveDef]; + MyApp.ngComponentDef.pipeDefs = [MyPurePipe.ngPipeDef]; // /NON-NORMATIVE let myApp: MyApp = renderComponent(MyApp); diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 589d210611f97..6b6055ea3f168 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -654,7 +654,7 @@ describe('di', () => { describe('getOrCreateNodeInjector', () => { it('should handle initial undefined state', () => { const contentView = - createLView(-1, null !, createTView(null), null, null, LViewFlags.CheckAlways); + createLView(-1, null !, createTView(null, null), null, null, LViewFlags.CheckAlways); const oldView = enterView(contentView, null !); try { const parent = createLNode(0, LNodeType.Element, null, null); diff --git a/packages/core/test/render3/pipe_spec.ts b/packages/core/test/render3/pipe_spec.ts index aa4b51ca1463e..f2a35161fcb84 100644 --- a/packages/core/test/render3/pipe_spec.ts +++ b/packages/core/test/render3/pipe_spec.ts @@ -14,7 +14,7 @@ import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, import {pipe, pipeBind1, pipeBind3, pipeBind4, pipeBindV} from '../../src/render3/pipe'; import {RenderLog, getRendererFactory2, patchLoggingRenderer2} from './imported_renderer2'; -import {renderToHtml} from './render_util'; +import {ComponentFixture, createComponent, renderToHtml} from './render_util'; let log: string[] = []; @@ -30,16 +30,33 @@ describe('pipe', () => { person = new Person(); }); + const defs = () => [CountingPipe.ngPipeDef, MultiArgPipe.ngPipeDef, CountingImpurePipe.ngPipeDef]; + it('should support interpolation', () => { function Template(person: Person, cm: boolean) { if (cm) { text(0); - pipe(1, CountingPipe.ngPipeDef); + pipe(1, 'countingPipe'); } textBinding(0, interpolation1('', pipeBind1(1, person.name), '')); } + person.init('bob', null); - expect(renderToHtml(Template, person)).toEqual('bob state:0'); + expect(renderToHtml(Template, person, null, defs)).toEqual('bob state:0'); + }); + + it('should throw if pipe is not found', () => { + const App = createComponent('app', function(ctx: any, cm: boolean) { + if (cm) { + text(0); + pipe(1, 'randomPipeName'); + } + textBinding(0, interpolation1('', pipeBind1(1, ctx.value), '')); + }, [], defs); + + expect(() => { + const fixture = new ComponentFixture(App); + }).toThrowError(/Pipe with name 'randomPipeName' not found!/); }); it('should support bindings', () => { @@ -64,6 +81,7 @@ describe('pipe', () => { transform(value: any) { return `${value}${value}`; } static ngPipeDef = definePipe({ + name: 'double', type: DoublePipe, factory: function DoublePipe_Factory() { return new DoublePipe(); }, }); @@ -72,13 +90,13 @@ describe('pipe', () => { function Template(ctx: string, cm: boolean) { if (cm) { elementStart(0, 'div', ['myDir', '']); - pipe(1, DoublePipe.ngPipeDef); + pipe(1, 'double'); elementEnd(); } elementProperty(0, 'elprop', bind(pipeBind1(1, ctx))); directive = loadDirective(0); } - renderToHtml(Template, 'a', [MyDir.ngDirectiveDef]); + renderToHtml(Template, 'a', [MyDir.ngDirectiveDef], [DoublePipe.ngPipeDef]); expect(directive !.dirProp).toEqual('aa'); }); @@ -86,27 +104,29 @@ describe('pipe', () => { function Template(person: Person, cm: boolean) { if (cm) { text(0); - pipe(1, MultiArgPipe.ngPipeDef); + pipe(1, 'multiArgPipe'); } textBinding( 0, interpolation1('', pipeBind3(1, person.name, 'one', person.address !.city), '')); } + person.init('value', new Address('two')); - expect(renderToHtml(Template, person)).toEqual('value one two default'); + expect(renderToHtml(Template, person, null, defs)).toEqual('value one two default'); }); it('should support calling pipes with different number of arguments', () => { function Template(person: Person, cm: boolean) { if (cm) { text(0); - pipe(1, MultiArgPipe.ngPipeDef); - pipe(2, MultiArgPipe.ngPipeDef); + pipe(1, 'multiArgPipe'); + pipe(2, 'multiArgPipe'); } textBinding( 0, interpolation1('', pipeBind4(2, pipeBindV(1, [person.name, 'a', 'b']), 0, 1, 2), '')); } + person.init('value', null); - expect(renderToHtml(Template, person)).toEqual('value a b default 0 1 2'); + expect(renderToHtml(Template, person, null, defs)).toEqual('value a b default 0 1 2'); }); it('should do nothing when no change', () => { @@ -115,6 +135,7 @@ describe('pipe', () => { transform(value: any) { return value; } static ngPipeDef = definePipe({ + name: 'identityPipe', type: IdentityPipe, factory: function IdentityPipe_Factory() { return new IdentityPipe(); }, }); @@ -123,16 +144,17 @@ describe('pipe', () => { function Template(person: Person, cm: boolean) { if (cm) { elementStart(0, 'div'); - pipe(1, IdentityPipe.ngPipeDef); + pipe(1, 'identityPipe'); elementEnd(); } elementProperty(0, 'someProp', bind(pipeBind1(1, 'Megatron'))); } - renderToHtml(Template, person, [], rendererFactory2); + + renderToHtml(Template, person, null, [IdentityPipe.ngPipeDef], rendererFactory2); expect(renderLog.log).toEqual(['someProp=Megatron']); renderLog.clear(); - renderToHtml(Template, person, [], rendererFactory2); + renderToHtml(Template, person, null, defs, rendererFactory2); expect(renderLog.log).toEqual([]); }); @@ -141,36 +163,36 @@ describe('pipe', () => { function Template(person: Person, cm: boolean) { if (cm) { text(0); - pipe(1, CountingPipe.ngPipeDef); + pipe(1, 'countingPipe'); } textBinding(0, interpolation1('', pipeBind1(1, person.name), '')); } // change from undefined -> null person.name = null; - expect(renderToHtml(Template, person)).toEqual('null state:0'); - expect(renderToHtml(Template, person)).toEqual('null state:0'); + expect(renderToHtml(Template, person, null, defs)).toEqual('null state:0'); + expect(renderToHtml(Template, person, null, defs)).toEqual('null state:0'); // change from null -> some value person.name = 'bob'; - expect(renderToHtml(Template, person)).toEqual('bob state:1'); - expect(renderToHtml(Template, person)).toEqual('bob state:1'); + expect(renderToHtml(Template, person, null, defs)).toEqual('bob state:1'); + expect(renderToHtml(Template, person, null, defs)).toEqual('bob state:1'); // change from some value -> some other value person.name = 'bart'; - expect(renderToHtml(Template, person)).toEqual('bart state:2'); - expect(renderToHtml(Template, person)).toEqual('bart state:2'); + expect(renderToHtml(Template, person, null, defs)).toEqual('bart state:2'); + expect(renderToHtml(Template, person, null, defs)).toEqual('bart state:2'); }); it('should cache pure pipes', () => { function Template(ctx: any, cm: boolean) { - let pipeInstance; + let pipeInstance: any; if (cm) { elementStart(0, 'div'); - pipeInstance = pipe(1, CountingPipe.ngPipeDef); + pipeInstance = pipe(1, 'countingPipe'); elementEnd(); elementStart(2, 'div'); - pipe(3, CountingPipe.ngPipeDef, pipeInstance); + pipe(3, 'countingPipe', pipeInstance); elementEnd(); container(4); } @@ -184,7 +206,7 @@ describe('pipe', () => { { if (cm1) { elementStart(0, 'div'); - pipe(1, CountingPipe.ngPipeDef, pipeInstance); + pipe(1, 'countingPipe', pipeInstance); elementEnd(); } elementProperty(0, 'someProp', bind(pipeBind1(1, true))); @@ -195,8 +217,9 @@ describe('pipe', () => { } containerRefreshEnd(); } + const pipeInstances: CountingPipe[] = []; - renderToHtml(Template, {}, [], rendererFactory2); + renderToHtml(Template, {}, null, defs, rendererFactory2); expect(pipeInstances.length).toEqual(4); expect(pipeInstances[0]).toBeAnInstanceOf(CountingPipe); expect(pipeInstances[1]).toBe(pipeInstances[0]); @@ -210,24 +233,24 @@ describe('pipe', () => { function Template(person: Person, cm: boolean) { if (cm) { text(0); - pipe(1, CountingImpurePipe.ngPipeDef); + pipe(1, 'countingImpurePipe'); } textBinding(0, interpolation1('', pipeBind1(1, person.name), '')); } person.name = 'bob'; - expect(renderToHtml(Template, person)).toEqual('bob state:0'); - expect(renderToHtml(Template, person)).toEqual('bob state:1'); + expect(renderToHtml(Template, person, null, defs)).toEqual('bob state:0'); + expect(renderToHtml(Template, person, null, defs)).toEqual('bob state:1'); }); it('should not cache impure pipes', () => { function Template(ctx: any, cm: boolean) { if (cm) { elementStart(0, 'div'); - pipe(1, CountingImpurePipe.ngPipeDef); + pipe(1, 'countingImpurePipe'); elementEnd(); elementStart(2, 'div'); - pipe(3, CountingImpurePipe.ngPipeDef); + pipe(3, 'countingImpurePipe'); elementEnd(); container(4); } @@ -241,7 +264,7 @@ describe('pipe', () => { { if (cm1) { elementStart(0, 'div'); - pipe(1, CountingImpurePipe.ngPipeDef); + pipe(1, 'countingImpurePipe'); elementEnd(); } elementProperty(0, 'someProp', bind(pipeBind1(1, true))); @@ -252,8 +275,9 @@ describe('pipe', () => { } containerRefreshEnd(); } + const pipeInstances: CountingImpurePipe[] = []; - renderToHtml(Template, {}, [], rendererFactory2); + renderToHtml(Template, {}, null, defs, rendererFactory2); expect(pipeInstances.length).toEqual(4); expect(pipeInstances[0]).toBeAnInstanceOf(CountingImpurePipe); expect(pipeInstances[1]).toBeAnInstanceOf(CountingImpurePipe); @@ -273,6 +297,7 @@ describe('pipe', () => { transform(value: any): any { return null; } static ngPipeDef = definePipe({ + name: 'pipeWithOnDestroy', type: PipeWithOnDestroy, factory: function PipeWithOnDestroy_Factory() { return new PipeWithOnDestroy(); }, }); @@ -290,7 +315,7 @@ describe('pipe', () => { { if (cm1) { text(0); - pipe(1, PipeWithOnDestroy.ngPipeDef); + pipe(1, 'pipeWithOnDestroy'); } textBinding(0, interpolation1('', pipeBind1(1, person.age), '')); } @@ -299,21 +324,23 @@ describe('pipe', () => { } containerRefreshEnd(); } + const defs = [PipeWithOnDestroy.ngPipeDef]; + person.age = 25; - renderToHtml(Template, person); + renderToHtml(Template, person, null, defs); person.age = 15; - renderToHtml(Template, person); + renderToHtml(Template, person, null, defs); expect(log).toEqual(['pipeWithOnDestroy - ngOnDestroy']); log = []; person.age = 30; - renderToHtml(Template, person); + renderToHtml(Template, person, null, defs); expect(log).toEqual([]); log = []; person.age = 10; - renderToHtml(Template, person); + renderToHtml(Template, person, null, defs); expect(log).toEqual(['pipeWithOnDestroy - ngOnDestroy']); }); }); @@ -327,6 +354,7 @@ class CountingPipe implements PipeTransform { transform(value: any) { return `${value} state:${this.state++}`; } static ngPipeDef = definePipe({ + name: 'countingPipe', type: CountingPipe, factory: function CountingPipe_Factory() { return new CountingPipe(); }, }); @@ -339,6 +367,7 @@ class CountingImpurePipe implements PipeTransform { transform(value: any) { return `${value} state:${this.state++}`; } static ngPipeDef = definePipe({ + name: 'countingImpurePipe', type: CountingImpurePipe, factory: function CountingImpurePipe_Factory() { return new CountingImpurePipe(); }, pure: false, @@ -352,6 +381,7 @@ class MultiArgPipe implements PipeTransform { } static ngPipeDef = definePipe({ + name: 'multiArgPipe', type: MultiArgPipe, factory: function MultiArgPipe_Factory() { return new MultiArgPipe(); }, }); diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 233dac0ac6868..3de933fcef790 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -11,6 +11,7 @@ import {stringifyElement} from '@angular/platform-browser/testing/src/browser_ut import {CreateComponentOptions} from '../../src/render3/component'; import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, PublicFeature, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index'; import {NG_HOST_SYMBOL, renderTemplate} from '../../src/render3/instructions'; +import {DirectiveDefListOrFactory, PipeDefListOrFactory} from '../../src/render3/interfaces/definition'; import {LElementNode} from '../../src/render3/interfaces/node'; import {RElement, RText, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer'; @@ -151,10 +152,11 @@ export function resetDOM() { * @deprecated use `TemplateFixture` or `ComponentFixture` */ export function renderToHtml( - template: ComponentTemplate, ctx: any, defs?: any[], - providedRendererFactory?: RendererFactory3 | null) { + template: ComponentTemplate, ctx: any, directives?: DirectiveDefListOrFactory | null, + pipes?: PipeDefListOrFactory | null, providedRendererFactory?: RendererFactory3 | null) { host = renderTemplate( - containerEl, template, ctx, providedRendererFactory || testRendererFactory, host, defs); + containerEl, template, ctx, providedRendererFactory || testRendererFactory, host, + directives || null, pipes || null); return toHtml(containerEl); } @@ -189,8 +191,8 @@ export function toHtml(componentOrElement: T | RElement): string { } export function createComponent( - name: string, template: ComponentTemplate, - defs: (ComponentDef| DirectiveDef)[] = []): ComponentType { + name: string, template: ComponentTemplate, directives: DirectiveDefListOrFactory = [], + pipes: PipeDefListOrFactory = []): ComponentType { return class Component { value: any; static ngComponentDef = defineComponent({ @@ -199,7 +201,8 @@ export function createComponent( factory: () => new Component, template: template, features: [PublicFeature], - directiveDefs: () => defs + directiveDefs: directives, + pipeDefs: pipes }); }; } diff --git a/packages/core/test/render3/renderer_factory_spec.ts b/packages/core/test/render3/renderer_factory_spec.ts index 7fc21fc642c34..623c96b54e5e4 100644 --- a/packages/core/test/render3/renderer_factory_spec.ts +++ b/packages/core/test/render3/renderer_factory_spec.ts @@ -88,7 +88,7 @@ describe('renderer factory lifecycle', () => { }); it('should work with a template', () => { - renderToHtml(Template, {}, [], rendererFactory); + renderToHtml(Template, {}, null, null, rendererFactory); expect(logs).toEqual(['create', 'begin', 'function', 'end']); logs = []; @@ -97,7 +97,7 @@ describe('renderer factory lifecycle', () => { }); it('should work with a template which contains a component', () => { - renderToHtml(TemplateWithComponent, {}, defs, rendererFactory); + renderToHtml(TemplateWithComponent, {}, defs, null, rendererFactory); expect(logs).toEqual( ['create', 'begin', 'function_with_component', 'create', 'component', 'end']);