diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 3d8fcfa73a..c1d4ebdc40 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -112,7 +112,6 @@ export function renderComponent( rootView[INJECTOR] = opts.injector || null; const oldView = enterView(rootView, null !); - rootView[BINDING_INDEX] = rootView[TVIEW].bindingStartIndex; let elementNode: LElementNode; let component: T; try { diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index b344008e2c..753bd8e7d8 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -127,7 +127,6 @@ export class ComponentFactory extends viewEngine_ComponentFactory { // rootView is the parent when bootstrapping const oldView = enterView(rootView, null !); - rootView[BINDING_INDEX] = rootView[TVIEW].bindingStartIndex; let component: T; let elementNode: LElementNode; diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index dbc6c80793..706bc030e1 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -56,7 +56,7 @@ export function defineComponent(componentDefinition: { /** * The number of nodes, local refs, and pipes in this component template. * - * Used to calculate the length of the component's LViewData array, so we + * Used to calculate the length of this component's LViewData array, so we * can pre-fill the array and set the binding start index. */ // TODO(kara): remove queries from this count @@ -65,11 +65,19 @@ export function defineComponent(componentDefinition: { /** * The number of bindings in this component template (including pure fn bindings). * - * Used to calculate the length of the component's LViewData array, so we + * Used to calculate the length of this component's LViewData array, so we * can pre-fill the array and set the host binding start index. */ vars: number; + /** + * The number of host bindings (including pure fn bindings) in this component. + * + * Used to calculate the length of the LViewData array for the *parent* component + * of this component. + */ + hostVars?: number; + /** * Static attributes to set on host element. * @@ -264,6 +272,7 @@ export function defineComponent(componentDefinition: { diPublic: null, consts: componentDefinition.consts, vars: componentDefinition.vars, + hostVars: componentDefinition.hostVars || 0, factory: componentDefinition.factory, template: componentDefinition.template || null !, hostBindings: componentDefinition.hostBindings || null, @@ -577,6 +586,14 @@ export const defineDirective = defineComponent as any as(directiveDefinition: */ features?: DirectiveDefFeature[]; + /** + * The number of host bindings (including pure fn bindings) in this directive. + * + * Used to calculate the length of the LViewData array for the *parent* component + * of this directive. + */ + hostVars?: number; + /** * Function executed by the parent template to allow child directive to apply host bindings. */ diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 8c8e397a57..cf4be6bc31 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -239,6 +239,18 @@ let checkNoChangesMode = false; /** Whether or not this is the first time the current view has been processed. */ let firstTemplatePass = true; +/** + * The root index from which pure function instructions should calculate their binding + * indices. In component views, this is TView.bindingStartIndex. In a host binding + * context, this is the TView.hostBindingStartIndex + any hostVars before the given dir. + */ +let bindingRootIndex: number = -1; + +// top level variables should not be exported for performance reasons (PERF_NOTES.md) +export function getBindingRoot() { + return bindingRootIndex; +} + const enum BindingDirection { Input, Output, @@ -263,7 +275,7 @@ export function enterView(newView: LViewData, host: LElementNode | LViewNode | n creationMode = newView && (newView[FLAGS] & LViewFlags.CreationMode) === LViewFlags.CreationMode; firstTemplatePass = newView && tView.firstTemplatePass; - + bindingRootIndex = newView && tView.bindingStartIndex; renderer = newView && newView[RENDERER]; if (host != null) { @@ -295,7 +307,7 @@ export function leaveView(newView: LViewData, creationOnly?: boolean): void { viewData[FLAGS] &= ~(LViewFlags.CreationMode | LViewFlags.Dirty); } viewData[FLAGS] |= LViewFlags.RunInit; - viewData[BINDING_INDEX] = -1; + viewData[BINDING_INDEX] = tView.bindingStartIndex; enterView(newView, null); } @@ -329,11 +341,13 @@ function refreshDescendantViews() { /** Sets the host bindings for the current view. */ export function setHostBindings(bindings: number[] | null): void { if (bindings != null) { + bindingRootIndex = viewData[BINDING_INDEX] = tView.hostBindingStartIndex; const defs = tView.directives !; for (let i = 0; i < bindings.length; i += 2) { const dirIndex = bindings[i]; const def = defs[dirIndex] as DirectiveDefInternal; def.hostBindings && def.hostBindings(dirIndex, bindings[i + 1]); + bindingRootIndex = viewData[BINDING_INDEX] = bindingRootIndex + def.hostVars; } } } @@ -377,7 +391,7 @@ export function createLViewData( null, // queries flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.RunInit, // flags null !, // hostNode - -1, // bindingIndex + tView.bindingStartIndex, // bindingIndex null, // directives null, // cleanupInstances context, // context @@ -600,7 +614,6 @@ export function renderEmbeddedTemplate( oldView = enterView(viewNode.data !, viewNode); namespaceHTML(); - viewData[BINDING_INDEX] = tView.bindingStartIndex; tView.template !(rf, context); if (rf & RenderFlags.Update) { refreshDescendantViews(); @@ -644,7 +657,6 @@ export function renderComponentOrTemplate( } if (templateFn) { namespaceHTML(); - viewData[BINDING_INDEX] = tView.bindingStartIndex; templateFn(getRenderFlags(hostView), componentOrContext !); refreshDescendantViews(); } else { @@ -1042,6 +1054,7 @@ export function createTView( directives: DirectiveDefListOrFactory | null, pipes: PipeDefListOrFactory | null, viewQuery: ComponentQuery| null): TView { ngDevMode && ngDevMode.tView++; + const bindingStartIndex = HEADER_OFFSET + consts; return { id: viewIndex, template: templateFn, @@ -1049,7 +1062,8 @@ export function createTView( node: null !, data: HEADER_FILLER.slice(), // Fill in to match HEADER_OFFSET in LViewData childIndex: -1, // Children set in addToViewTree(), if any - bindingStartIndex: HEADER_OFFSET + consts, + bindingStartIndex: bindingStartIndex, + hostBindingStartIndex: bindingStartIndex + vars, directives: null, firstTemplatePass: true, initHooks: null, @@ -2062,7 +2076,6 @@ export function embeddedViewStart(viewBlockId: number, consts: number, vars: num } lContainer[ACTIVE_INDEX] !++; } - viewData[BINDING_INDEX] = tView.bindingStartIndex; return getRenderFlags(viewNode.data); } @@ -2467,7 +2480,6 @@ export function detectChangesInternal( const hostTView = hostView[TVIEW]; const templateFn = hostTView.template !; const viewQuery = hostTView.viewQuery; - viewData[BINDING_INDEX] = tView.bindingStartIndex; try { namespaceHTML(); diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index ba752e032c..43e114087c 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -141,6 +141,14 @@ export interface DirectiveDef extends BaseDef { /** Refreshes content queries associated with directives in a given view */ contentQueriesRefresh: ((directiveIndex: number, queryIndex: number) => void)|null; + /** + * The number of host bindings (including pure fn bindings) in this directive/component. + * + * Used to calculate the length of the LViewData array for the *parent* component + * of this directive/component. + */ + hostVars: number; + /** Refreshes host bindings on the associated directive. */ hostBindings: ((directiveIndex: number, elementIndex: number) => void)|null; diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index bf3f2aeb6d..ce0246a58b 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -289,6 +289,13 @@ export interface TView { */ bindingStartIndex: number; + /** + * The index at which the data array begins to store host bindings for components + * or directives in its template. Saving this value ensures that we can set the + * binding root and binding index correctly before checking host bindings. + */ + hostBindingStartIndex: number; + /** * Index of the host node of the first LView or LContainer beneath this LView in * the hierarchy. diff --git a/packages/core/src/render3/pure_function.ts b/packages/core/src/render3/pure_function.ts index 153869e894..a445da06b4 100644 --- a/packages/core/src/render3/pure_function.ts +++ b/packages/core/src/render3/pure_function.ts @@ -6,20 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import {bindingUpdated, bindingUpdated2, bindingUpdated4, updateBinding, getBinding, getCreationMode, getTView, bindingUpdated3,} from './instructions'; +import {bindingUpdated, bindingUpdated2, bindingUpdated4, updateBinding, getBinding, getCreationMode, bindingUpdated3, getBindingRoot, getTView,} from './instructions'; /** * Bindings for pure functions are stored after regular bindings. * - * ---------------------------------------------------------------------------- - * | LNodes / local refs / pipes ... | regular bindings / interpolations | pure function bindings - * ---------------------------------------------------------------------------- - * ^ - * TView.bindingStartIndex + * |--------consts--------|----------------vars----------------|------ hostVars (dir1) ------| + * --------------------------------------------------------------------------------------------- + * | nodes / refs / pipes | bindings | pure function bindings | host bindings | host slots | + * --------------------------------------------------------------------------------------------- + * ^ ^ + * TView.bindingStartIndex TView.hostBindingStartIndex * - * Pure function instructions are given an offset from TView.bindingStartIndex. - * Adding the offset to TView.bindingStartIndex gives the first index where the bindings - * are stored. + * Pure function instructions are given an offset from the binding root. Adding the offset to the + * binding root gives the first index where the bindings are stored. In component views, the binding + * root is the bindingStartIndex. In host bindings, the binding root is the hostBindingStartIndex + + * any hostVars in directives evaluated before it. */ /** @@ -33,7 +35,7 @@ import {bindingUpdated, bindingUpdated2, bindingUpdated4, updateBinding, getBind */ export function pureFunction0(slotOffset: number, pureFn: () => T, thisArg?: any): T { // TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings - const bindingIndex = getTView().bindingStartIndex + slotOffset; + const bindingIndex = getBindingRoot() + slotOffset; return getCreationMode() ? updateBinding(bindingIndex, thisArg ? pureFn.call(thisArg) : pureFn()) : getBinding(bindingIndex); @@ -52,7 +54,7 @@ export function pureFunction0(slotOffset: number, pureFn: () => T, thisArg?: export function pureFunction1( slotOffset: number, pureFn: (v: any) => any, exp: any, thisArg?: any): any { // TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings - const bindingIndex = getTView().bindingStartIndex + slotOffset; + const bindingIndex = getBindingRoot() + slotOffset; return bindingUpdated(bindingIndex, exp) ? updateBinding(bindingIndex + 1, thisArg ? pureFn.call(thisArg, exp) : pureFn(exp)) : getBinding(bindingIndex + 1); @@ -73,7 +75,7 @@ export function pureFunction2( slotOffset: number, pureFn: (v1: any, v2: any) => any, exp1: any, exp2: any, thisArg?: any): any { // TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings - const bindingIndex = getTView().bindingStartIndex + slotOffset; + const bindingIndex = getBindingRoot() + slotOffset; return bindingUpdated2(bindingIndex, exp1, exp2) ? updateBinding( bindingIndex + 2, thisArg ? pureFn.call(thisArg, exp1, exp2) : pureFn(exp1, exp2)) : @@ -96,7 +98,7 @@ export function pureFunction3( slotOffset: number, pureFn: (v1: any, v2: any, v3: any) => any, exp1: any, exp2: any, exp3: any, thisArg?: any): any { // TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings - const bindingIndex = getTView().bindingStartIndex + slotOffset; + const bindingIndex = getBindingRoot() + slotOffset; return bindingUpdated3(bindingIndex, exp1, exp2, exp3) ? updateBinding( bindingIndex + 3, @@ -121,7 +123,7 @@ export function pureFunction4( slotOffset: number, pureFn: (v1: any, v2: any, v3: any, v4: any) => any, exp1: any, exp2: any, exp3: any, exp4: any, thisArg?: any): any { // TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings - const bindingIndex = getTView().bindingStartIndex + slotOffset; + const bindingIndex = getBindingRoot() + slotOffset; return bindingUpdated4(bindingIndex, exp1, exp2, exp3, exp4) ? updateBinding( bindingIndex + 4, @@ -147,7 +149,7 @@ export function pureFunction5( slotOffset: number, pureFn: (v1: any, v2: any, v3: any, v4: any, v5: any) => any, exp1: any, exp2: any, exp3: any, exp4: any, exp5: any, thisArg?: any): any { // TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings - const bindingIndex = getTView().bindingStartIndex + slotOffset; + const bindingIndex = getBindingRoot() + slotOffset; const different = bindingUpdated4(bindingIndex, exp1, exp2, exp3, exp4); return bindingUpdated(bindingIndex + 4, exp5) || different ? updateBinding( @@ -175,7 +177,7 @@ export function pureFunction6( slotOffset: number, pureFn: (v1: any, v2: any, v3: any, v4: any, v5: any, v6: any) => any, exp1: any, exp2: any, exp3: any, exp4: any, exp5: any, exp6: any, thisArg?: any): any { // TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings - const bindingIndex = getTView().bindingStartIndex + slotOffset; + const bindingIndex = getBindingRoot() + slotOffset; const different = bindingUpdated4(bindingIndex, exp1, exp2, exp3, exp4); return bindingUpdated2(bindingIndex + 4, exp5, exp6) || different ? updateBinding( @@ -205,7 +207,7 @@ export function pureFunction7( pureFn: (v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any) => any, exp1: any, exp2: any, exp3: any, exp4: any, exp5: any, exp6: any, exp7: any, thisArg?: any): any { // TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings - const bindingIndex = getTView().bindingStartIndex + slotOffset; + const bindingIndex = getBindingRoot() + slotOffset; let different = bindingUpdated4(bindingIndex, exp1, exp2, exp3, exp4); return bindingUpdated3(bindingIndex + 4, exp5, exp6, exp7) || different ? updateBinding( @@ -238,7 +240,7 @@ export function pureFunction8( exp1: any, exp2: any, exp3: any, exp4: any, exp5: any, exp6: any, exp7: any, exp8: any, thisArg?: any): any { // TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings - const bindingIndex = getTView().bindingStartIndex + slotOffset; + const bindingIndex = getBindingRoot() + slotOffset; const different = bindingUpdated4(bindingIndex, exp1, exp2, exp3, exp4); return bindingUpdated4(bindingIndex + 4, exp5, exp6, exp7, exp8) || different ? updateBinding( @@ -264,7 +266,7 @@ export function pureFunction8( export function pureFunctionV( slotOffset: number, pureFn: (...v: any[]) => any, exps: any[], thisArg?: any): any { // TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings - let bindingIndex = getTView().bindingStartIndex + slotOffset; + let bindingIndex = getBindingRoot() + slotOffset; let different = false; for (let i = 0; i < exps.length; i++) { bindingUpdated(bindingIndex++, exps[i]) && (different = true); diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index fe2a6d0643..af4b06f2ff 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -98,6 +98,9 @@ { "name": "baseDirectiveCreate" }, + { + "name": "bindingRootIndex" + }, { "name": "bloomAdd" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 230ba05d08..5c5da9df29 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -326,6 +326,9 @@ { "name": "bind" }, + { + "name": "bindingRootIndex" + }, { "name": "bindingUpdated" }, diff --git a/packages/core/test/render3/directive_spec.ts b/packages/core/test/render3/directive_spec.ts index 32b8675c5a..3d7ee5deab 100644 --- a/packages/core/test/render3/directive_spec.ts +++ b/packages/core/test/render3/directive_spec.ts @@ -15,36 +15,6 @@ import {TemplateFixture} from './render_util'; describe('directive', () => { - describe('host', () => { - - it('should support host bindings in directives', () => { - let directiveInstance: Directive|undefined; - - class Directive { - klass = 'foo'; - static ngDirectiveDef = defineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => directiveInstance = new Directive, - hostBindings: (directiveIndex: number, elementIndex: number) => { - elementProperty( - elementIndex, 'className', bind(loadDirective(directiveIndex).klass)); - } - }); - } - - function Template() { element(0, 'span', [AttributeMarker.SelectOnly, 'dir']); } - - const fixture = new TemplateFixture(Template, () => {}, 1, 0, [Directive]); - expect(fixture.html).toEqual(''); - - directiveInstance !.klass = 'bar'; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - }); - describe('selectors', () => { it('should match directives with attribute selectors on bindings', () => { diff --git a/packages/core/test/render3/properties_spec.ts b/packages/core/test/render3/properties_spec.ts index 9a93ee483f..3f004c1ffd 100644 --- a/packages/core/test/render3/properties_spec.ts +++ b/packages/core/test/render3/properties_spec.ts @@ -79,30 +79,379 @@ describe('elementProperty', () => { expect(fixture.html).toEqual(''); }); - it('should support host bindings on root component', () => { - class HostBindingComp { - id = 'my-id'; + describe('host', () => { + let nameComp !: NameComp; + + class NameComp { + names !: string[]; static ngComponentDef = defineComponent({ - type: HostBindingComp, - selectors: [['host-binding-comp']], - factory: () => new HostBindingComp(), + type: NameComp, + selectors: [['name-comp']], + factory: function NameComp_Factory() { return nameComp = new NameComp(); }, consts: 0, vars: 0, - hostBindings: (dirIndex: number, elIndex: number) => { - const instance = loadDirective(dirIndex) as HostBindingComp; - elementProperty(elIndex, 'id', bind(instance.id)); - }, - template: (rf: RenderFlags, ctx: HostBindingComp) => {} + template: function NameComp_Template(rf: RenderFlags, ctx: NameComp) {}, + inputs: {names: 'names'} }); } - const fixture = new ComponentFixture(HostBindingComp); - expect(fixture.hostElement.id).toBe('my-id'); + it('should support host bindings in directives', () => { + let directiveInstance: Directive|undefined; + + class Directive { + // @HostBinding('className') + klass = 'foo'; + + static ngDirectiveDef = defineDirective({ + type: Directive, + selectors: [['', 'dir', '']], + factory: () => directiveInstance = new Directive, + hostVars: 1, + hostBindings: (directiveIndex: number, elementIndex: number) => { + elementProperty( + elementIndex, 'className', bind(loadDirective(directiveIndex).klass)); + } + }); + } + + function Template() { element(0, 'span', [AttributeMarker.SelectOnly, 'dir']); } + + const fixture = new TemplateFixture(Template, () => {}, 1, 0, [Directive]); + expect(fixture.html).toEqual(''); + + directiveInstance !.klass = 'bar'; + fixture.update(); + expect(fixture.html).toEqual(''); + }); + + it('should support host bindings on root component', () => { + class HostBindingComp { + // @HostBinding() + id = 'my-id'; + + static ngComponentDef = defineComponent({ + type: HostBindingComp, + selectors: [['host-binding-comp']], + factory: () => new HostBindingComp(), + consts: 0, + vars: 0, + hostVars: 1, + hostBindings: (dirIndex: number, elIndex: number) => { + const instance = loadDirective(dirIndex) as HostBindingComp; + elementProperty(elIndex, 'id', bind(instance.id)); + }, + template: (rf: RenderFlags, ctx: HostBindingComp) => {} + }); + } + + const fixture = new ComponentFixture(HostBindingComp); + expect(fixture.hostElement.id).toBe('my-id'); + + fixture.component.id = 'other-id'; + fixture.update(); + expect(fixture.hostElement.id).toBe('other-id'); + }); + + it('should support component with host bindings and array literals', () => { + const ff = (v: any) => ['Nancy', v, 'Ned']; + + class HostBindingComp { + // @HostBinding() + id = 'my-id'; + + static ngComponentDef = defineComponent({ + type: HostBindingComp, + selectors: [['host-binding-comp']], + factory: () => new HostBindingComp(), + consts: 0, + vars: 0, + hostVars: 1, + hostBindings: (dirIndex: number, elIndex: number) => { + const ctx = loadDirective(dirIndex) as HostBindingComp; + elementProperty(elIndex, 'id', bind(ctx.id)); + }, + template: (rf: RenderFlags, ctx: HostBindingComp) => {} + }); + } + + /** + * + * + */ + const AppComponent = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'name-comp'); + element(1, 'host-binding-comp'); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'names', bind(pureFunction1(1, ff, ctx.name))); + } + }, 2, 3, [HostBindingComp, NameComp]); + + const fixture = new ComponentFixture(AppComponent); + const hostBindingEl = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; + fixture.component.name = 'Betty'; + fixture.update(); + expect(hostBindingEl.id).toBe('my-id'); + expect(nameComp.names).toEqual(['Nancy', 'Betty', 'Ned']); + + const firstArray = nameComp.names; + fixture.update(); + expect(firstArray).toBe(nameComp.names); + + fixture.component.name = 'my-id'; + fixture.update(); + expect(hostBindingEl.id).toBe('my-id'); + expect(nameComp.names).toEqual(['Nancy', 'my-id', 'Ned']); + }); + + // Note: This is a contrived example. For feature parity with render2, we should make sure it + // works in this way (see https://stackblitz.com/edit/angular-cbqpbe), but a more realistic + // example would be an animation host binding with a literal defining the animation config. + // When animation support is added, we should add another test for that case. + it('should support host bindings that contain array literals', () => { + const ff = (v: any) => ['red', v]; + const ff2 = (v: any, v2: any) => [v, v2]; + const ff3 = (v: any, v2: any) => [v, 'Nancy', v2]; + let hostBindingComp !: HostBindingComp; + + /** + * @Component({ + * ... + * host: { + * `[id]`: `['red', id]`, + * `[dir]`: `dir`, + * `[title]`: `[title, otherTitle]` + * } + * }) + * + */ + class HostBindingComp { + id = 'blue'; + dir = 'ltr'; + title = 'my title'; + otherTitle = 'other title'; + + static ngComponentDef = defineComponent({ + type: HostBindingComp, + selectors: [['host-binding-comp']], + factory: () => hostBindingComp = new HostBindingComp(), + consts: 0, + vars: 0, + hostVars: 8, + hostBindings: (dirIndex: number, elIndex: number) => { + const ctx = loadDirective(dirIndex) as HostBindingComp; + // LViewData: [..., id, dir, title, ctx.id, pf1, ctx.title, ctx.otherTitle, pf2] + elementProperty(elIndex, 'id', bind(pureFunction1(3, ff, ctx.id))); + elementProperty(elIndex, 'dir', bind(ctx.dir)); + elementProperty( + elIndex, 'title', bind(pureFunction2(5, ff2, ctx.title, ctx.otherTitle))); + }, + template: (rf: RenderFlags, ctx: HostBindingComp) => {} + }); + } + + /** + * + * + */ + const AppComponent = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'name-comp'); + element(1, 'host-binding-comp'); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'names', bind(pureFunction2(1, ff3, ctx.name, ctx.otherName))); + } + }, 2, 4, [HostBindingComp, NameComp]); + + const fixture = new ComponentFixture(AppComponent); + fixture.component.name = 'Frank'; + fixture.component.otherName = 'Joe'; + fixture.update(); + + const hostBindingEl = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; + expect(hostBindingEl.id).toBe('red,blue'); + expect(hostBindingEl.dir).toBe('ltr'); + expect(hostBindingEl.title).toBe('my title,other title'); + expect(nameComp.names).toEqual(['Frank', 'Nancy', 'Joe']); + + const firstArray = nameComp.names; + fixture.update(); + expect(firstArray).toBe(nameComp.names); + + hostBindingComp.id = 'green'; + hostBindingComp.dir = 'rtl'; + hostBindingComp.title = 'TITLE'; + fixture.update(); + expect(hostBindingEl.id).toBe('red,green'); + expect(hostBindingEl.dir).toBe('rtl'); + expect(hostBindingEl.title).toBe('TITLE,other title'); + }); + + it('should support host bindings with literals from multiple directives', () => { + let hostBindingComp !: HostBindingComp; + let hostBindingDir !: HostBindingDir; + + const ff = (v: any) => ['red', v]; + + /** + * @Component({ + * ... + * host: { + * '[id]': '['red', id]' + * } + * }) + * + */ + class HostBindingComp { + id = 'blue'; + + static ngComponentDef = defineComponent({ + type: HostBindingComp, + selectors: [['host-binding-comp']], + factory: () => hostBindingComp = new HostBindingComp(), + consts: 0, + vars: 0, + hostVars: 3, + hostBindings: (dirIndex: number, elIndex: number) => { + // LViewData: [..., id, ctx.id, pf1] + const ctx = loadDirective(dirIndex) as HostBindingComp; + elementProperty(elIndex, 'id', bind(pureFunction1(1, ff, ctx.id))); + }, + template: (rf: RenderFlags, ctx: HostBindingComp) => {} + }); + } + + const ff1 = (v: any) => [v, 'other title']; + + /** + * @Directive({ + * ... + * host: { + * '[title]': '[title, 'other title']' + * } + * }) + * + */ + class HostBindingDir { + title = 'my title'; + + static ngDirectiveDef = defineDirective({ + type: HostBindingDir, + selectors: [['', 'hostDir', '']], + factory: () => hostBindingDir = new HostBindingDir(), + hostVars: 3, + hostBindings: (dirIndex: number, elIndex: number) => { + // LViewData [..., title, ctx.title, pf1] + const ctx = loadDirective(dirIndex) as HostBindingDir; + elementProperty(elIndex, 'title', bind(pureFunction1(1, ff1, ctx.title))); + } + }); + } + + /** + * + * + */ + const AppComponent = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'host-binding-comp', ['hostDir', '']); + } + }, 1, 0, [HostBindingComp, HostBindingDir]); + + const fixture = new ComponentFixture(AppComponent); + const hostElement = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; + expect(hostElement.id).toBe('red,blue'); + expect(hostElement.title).toBe('my title,other title'); + + hostBindingDir.title = 'blue'; + fixture.update(); + expect(hostElement.title).toBe('blue,other title'); + + hostBindingComp.id = 'green'; + fixture.update(); + expect(hostElement.id).toBe('red,green'); + }); + + it('should support ternary expressions in host bindings', () => { + let hostBindingComp !: HostBindingComp; + + const ff = (v: any) => ['red', v]; + const ff1 = (v: any) => [v]; + + /** + * @Component({ + * ... + * host: { + * `[id]`: `condition ? ['red', id] : 'green'`, + * `[title]`: `otherCondition ? [title] : 'other title'` + * } + * }) + * + */ + class HostBindingComp { + condition = true; + otherCondition = true; + id = 'blue'; + title = 'blue'; + + static ngComponentDef = defineComponent({ + type: HostBindingComp, + selectors: [['host-binding-comp']], + factory: () => hostBindingComp = new HostBindingComp(), + consts: 0, + vars: 0, + hostVars: 6, + hostBindings: (dirIndex: number, elIndex: number) => { + // LViewData: [..., id, title, ctx.id, pf1, ctx.title, pf1] + const ctx = loadDirective(dirIndex) as HostBindingComp; + elementProperty( + elIndex, 'id', bind(ctx.condition ? pureFunction1(2, ff, ctx.id) : 'green')); + elementProperty( + elIndex, 'title', + bind(ctx.otherCondition ? pureFunction1(4, ff1, ctx.title) : 'other title')); + }, + template: (rf: RenderFlags, ctx: HostBindingComp) => {} + }); + } + + /** + * + * {{ name }} + */ + const AppComponent = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'host-binding-comp'); + text(1); + } + if (rf & RenderFlags.Update) { + textBinding(1, bind(ctx.name)); + } + }, 2, 1, [HostBindingComp]); + + const fixture = new ComponentFixture(AppComponent); + const hostElement = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; + fixture.component.name = 'Ned'; + fixture.update(); + expect(hostElement.id).toBe('red,blue'); + expect(hostElement.title).toBe('blue'); + expect(fixture.html) + .toEqual(`Ned`); + + hostBindingComp.condition = false; + hostBindingComp.title = 'TITLE'; + fixture.update(); + expect(hostElement.id).toBe('green'); + expect(hostElement.title).toBe('TITLE'); + + hostBindingComp.otherCondition = false; + fixture.update(); + expect(hostElement.id).toBe('green'); + expect(hostElement.title).toBe('other title'); + }); - fixture.component.id = 'other-id'; - tick(fixture.component); - expect(fixture.hostElement.id).toBe('other-id'); }); describe('input properties', () => {