diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 0c2d671d87a..aa7f8bae49c 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -28,9 +28,6 @@ import {enterView, leaveView, resetComponentState} from './state'; import {getRootView, readElementValue, readPatchedLViewData, stringify} from './util'; -// Root component will always have an element index of 0 and an injector size of 1 -const ROOT_EXPANDO_INSTRUCTIONS = [0, 1]; - /** Options that control how the component should be bootstrapped. */ export interface CreateComponentOptions { /** Which renderer factory to use. */ @@ -171,7 +168,6 @@ export function createRootComponentView( const tNode = createNodeAtIndex(0, TNodeType.Element, rNode, null, null); if (tView.firstTemplatePass) { - tView.expandoInstructions = ROOT_EXPANDO_INSTRUCTIONS.slice(); diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), rootView, def.type); tNode.flags = TNodeFlags.isComponent; initNodeFlags(tNode, rootView.length, 1); diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 09aa7b0f35a..590f2ed6a21 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -23,7 +23,7 @@ import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} fro import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container'; import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, InitialStylingFlags, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {INJECTOR_SIZE, NodeInjectorFactory} from './interfaces/injector'; -import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TProjectionNode, TViewNode} from './interfaces/node'; +import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from './interfaces/node'; import {PlayerFactory} from './interfaces/player'; import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {LQueries} from './interfaces/query'; @@ -105,8 +105,9 @@ export function setHostBindings(tView: TView, viewData: LViewData): void { // Negative numbers mean that we are starting new EXPANDO block and need to update // the current element and directive index. currentElementIndex = -instruction; - // Injector block is taken into account. - bindingRootIndex += INJECTOR_SIZE; + // Injector block and providers are taken into account. + const providerCount = (tView.expandoInstructions[++i] as number); + bindingRootIndex += INJECTOR_SIZE + providerCount; currentDirectiveIndex = bindingRootIndex; } else { @@ -1296,14 +1297,15 @@ export function textBinding(index: number, value: T | NO_CHANGE): void { */ export function instantiateRootComponent( tView: TView, viewData: LViewData, def: ComponentDef): T { - if (getFirstTemplatePass()) { + const rootTNode = getPreviousOrParentTNode(); + if (tView.firstTemplatePass) { if (def.providersResolver) def.providersResolver(def); + generateExpandoInstructionBlock(tView, rootTNode, 1); baseResolveDirective(tView, viewData, def, def.factory); } - const previousOrParentTNode = getPreviousOrParentTNode(); - const directive = getNodeInjectable( - tView.data, viewData, viewData.length - 1, previousOrParentTNode as TElementNode); - postProcessBaseDirective(viewData, previousOrParentTNode, directive, def as DirectiveDef); + const directive = + getNodeInjectable(tView.data, viewData, viewData.length - 1, rootTNode as TElementNode); + postProcessBaseDirective(viewData, rootTNode, directive, def as DirectiveDef); return directive; } @@ -1316,7 +1318,6 @@ function resolveDirectives( // Please make sure to have explicit type for `exportsMap`. Inferred type triggers bug in tsickle. ngDevMode && assertEqual(getFirstTemplatePass(), true, 'should run on first template pass only'); const exportsMap: ({[key: string]: number} | null) = localRefs ? {'': -1} : null; - generateExpandoInstructionBlock(tView, tNode, directives); let totalHostVars = 0; if (directives) { initNodeFlags(tNode, tView.data.length, directives.length); @@ -1330,6 +1331,7 @@ function resolveDirectives( const def = directives[i] as DirectiveDef; if (def.providersResolver) def.providersResolver(def); } + generateExpandoInstructionBlock(tView, tNode, directives.length); for (let i = 0; i < directives.length; i++) { const def = directives[i] as DirectiveDef; @@ -1375,14 +1377,17 @@ function instantiateAllDirectives(tView: TView, viewData: LViewData, previousOrP * Each expando block starts with the element index (turned negative so we can distinguish * it from the hostVar count) and the directive count. See more in VIEW_DATA.md. */ -function generateExpandoInstructionBlock( - tView: TView, tNode: TNode, directives: DirectiveDef[] | null): void { - const directiveCount = directives ? directives.length : 0; +export function generateExpandoInstructionBlock( + tView: TView, tNode: TNode, directiveCount: number): void { + ngDevMode && assertEqual( + tView.firstTemplatePass, true, + 'Expando block should only be generated on first template pass.'); + const elementIndex = -(tNode.index - HEADER_OFFSET); - if (directiveCount > 0) { - (tView.expandoInstructions || (tView.expandoInstructions = [ - ])).push(elementIndex, directiveCount); - } + const providerStartIndex = tNode.providerIndexes & TNodeProviderIndexes.ProvidersStartIndexMask; + const providerCount = tView.data.length - providerStartIndex; + (tView.expandoInstructions || (tView.expandoInstructions = [ + ])).push(elementIndex, providerCount, directiveCount); } /** diff --git a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json index 1ab1de1467f..34a28a5ca39 100644 --- a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json @@ -170,9 +170,6 @@ { "name": "RENDER_PARENT" }, - { - "name": "ROOT_EXPANDO_INSTRUCTIONS" - }, { "name": "RecordViewTuple" }, 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 6abb011ba3c..1a90289269e 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -104,9 +104,6 @@ { "name": "RENDER_PARENT" }, - { - "name": "ROOT_EXPANDO_INSTRUCTIONS" - }, { "name": "SANITIZER" }, @@ -218,6 +215,9 @@ { "name": "firstTemplatePass" }, + { + "name": "generateExpandoInstructionBlock" + }, { "name": "getBeforeNodeForView" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 83bae1a82e6..0363026971c 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -164,9 +164,6 @@ { "name": "RENDER_PARENT" }, - { - "name": "ROOT_EXPANDO_INSTRUCTIONS" - }, { "name": "RecordViewTuple" }, diff --git a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json index a0b6afd2979..e1e3954ece2 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -698,9 +698,6 @@ { "name": "ROOT_CONTEXT" }, - { - "name": "ROOT_EXPANDO_INSTRUCTIONS" - }, { "name": "RecordViewTuple" }, diff --git a/packages/core/test/render3/host_binding_spec.ts b/packages/core/test/render3/host_binding_spec.ts new file mode 100644 index 00000000000..edc63322d47 --- /dev/null +++ b/packages/core/test/render3/host_binding_spec.ts @@ -0,0 +1,583 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {EventEmitter} from '@angular/core'; + +import {AttributeMarker, defineComponent, template, defineDirective, ProvidersFeature} from '../../src/render3/index'; +import {bind, directiveInject, element, elementEnd, elementProperty, elementStart, load, text, textBinding} from '../../src/render3/instructions'; +import {RenderFlags} from '../../src/render3/interfaces/definition'; +import {pureFunction1, pureFunction2} from '../../src/render3/pure_function'; + +import {ComponentFixture, TemplateFixture, createComponent, createDirective} from './render_util'; +import {NgForOf} from './common_with_def'; + +describe('host', () => { + let nameComp !: NameComp; + + class NameComp { + names !: string[]; + + static ngComponentDef = defineComponent({ + type: NameComp, + selectors: [['name-comp']], + factory: function NameComp_Factory() { return nameComp = new NameComp(); }, + consts: 0, + vars: 0, + template: function NameComp_Template(rf: RenderFlags, ctx: NameComp) {}, + inputs: {names: 'names'} + }); + } + + 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(load(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 = load(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 host bindings on nodes with providers', () => { + + class ServiceOne { + value = 'one' + } + class ServiceTwo { + value = 'two' + } + + class CompWithProviders { + // @HostBinding() + id = 'my-id'; + + constructor(public serviceOne: ServiceOne, public serviceTwo: ServiceTwo) {} + + static ngComponentDef = defineComponent({ + type: CompWithProviders, + selectors: [['comp-with-providers']], + factory: + () => new CompWithProviders(directiveInject(ServiceOne), directiveInject(ServiceTwo)), + consts: 0, + vars: 0, + hostVars: 1, + hostBindings: (dirIndex: number, elIndex: number) => { + const instance = load(dirIndex) as CompWithProviders; + elementProperty(elIndex, 'id', bind(instance.id)); + }, + template: (rf: RenderFlags, ctx: CompWithProviders) => {}, + features: [ProvidersFeature([[ServiceOne], [ServiceTwo]])] + }); + } + + const fixture = new ComponentFixture(CompWithProviders); + expect(fixture.hostElement.id).toBe('my-id'); + expect(fixture.component.serviceOne.value).toEqual('one'); + expect(fixture.component.serviceTwo.value).toEqual('two'); + + fixture.component.id = 'other-id'; + fixture.update(); + expect(fixture.hostElement.id).toBe('other-id'); + }); + + it('should support host bindings on multiple nodes', () => { + let hostBindingDir !: HostBindingDir; + + class HostBindingDir { + // @HostBinding() + id = 'foo'; + + static ngDirectiveDef = defineDirective({ + type: HostBindingDir, + selectors: [['', 'hostBindingDir', '']], + factory: () => hostBindingDir = new HostBindingDir(), + hostVars: 1, + hostBindings: (directiveIndex: number, elementIndex: number) => { + elementProperty(elementIndex, 'id', bind(load(directiveIndex).id)); + } + }); + } + + const SomeDir = createDirective('someDir'); + + class HostBindingComp { + // @HostBinding() + title = 'my-title'; + + 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 = load(dirIndex) as HostBindingComp; + elementProperty(elIndex, 'title', bind(ctx.title)); + }, + template: (rf: RenderFlags, ctx: HostBindingComp) => {} + }); + } + + /** + *
+ *
+ * + */ + const App = createComponent('app', (rf: RenderFlags, ctx: any) => { + if (rf & RenderFlags.Create) { + element(0, 'div', ['hostBindingDir', '']); + element(1, 'div', ['someDir', '']); + element(2, 'host-binding-comp'); + } + }, 3, 0, [HostBindingDir, SomeDir, HostBindingComp]); + + const fixture = new ComponentFixture(App); + const hostBindingDiv = fixture.hostElement.querySelector('div') as HTMLElement; + const hostBindingComp = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; + expect(hostBindingDiv.id).toEqual('foo'); + expect(hostBindingComp.title).toEqual('my-title'); + + hostBindingDir.id = 'bar'; + fixture.update(); + expect(hostBindingDiv.id).toEqual('bar'); + }); + + it('should support host bindings on second template pass', () => { + class HostBindingDir { + // @HostBinding() + id = 'foo'; + + static ngDirectiveDef = defineDirective({ + type: HostBindingDir, + selectors: [['', 'hostBindingDir', '']], + factory: () => new HostBindingDir(), + hostVars: 1, + hostBindings: (directiveIndex: number, elementIndex: number) => { + elementProperty(elementIndex, 'id', bind(load(directiveIndex).id)); + } + }); + } + + /**
*/ + const Parent = createComponent('parent', (rf: RenderFlags, ctx: any) => { + if (rf & RenderFlags.Create) { + element(0, 'div', ['hostBindingDir', '']); + } + }, 1, 0, [HostBindingDir]); + + /** + * + * + */ + const App = createComponent('app', (rf: RenderFlags, ctx: any) => { + if (rf & RenderFlags.Create) { + element(0, 'parent'); + element(1, 'parent'); + } + }, 2, 0, [Parent]); + + const fixture = new ComponentFixture(App); + const divs = fixture.hostElement.querySelectorAll('div'); + expect(divs[0].id).toEqual('foo'); + expect(divs[1].id).toEqual('foo'); + }); + + it('should support host bindings in for loop', () => { + class HostBindingDir { + // @HostBinding() + id = 'foo'; + + static ngDirectiveDef = defineDirective({ + type: HostBindingDir, + selectors: [['', 'hostBindingDir', '']], + factory: () => new HostBindingDir(), + hostVars: 1, + hostBindings: (directiveIndex: number, elementIndex: number) => { + elementProperty(elementIndex, 'id', bind(load(directiveIndex).id)); + } + }); + } + + function NgForTemplate(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + { element(1, 'p', ['hostBindingDir', '']); } + elementEnd(); + } + } + + /** + *
+ *

+ *
+ */ + const App = createComponent('parent', (rf: RenderFlags, ctx: any) => { + if (rf & RenderFlags.Create) { + template(0, NgForTemplate, 2, 0, null, ['ngForOf', '']); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'ngForOf', bind(ctx.rows)); + } + }, 1, 1, [HostBindingDir, NgForOf]); + + const fixture = new ComponentFixture(App); + fixture.component.rows = [1, 2, 3]; + fixture.update(); + + const paragraphs = fixture.hostElement.querySelectorAll('p'); + expect(paragraphs[0].id).toEqual('foo'); + expect(paragraphs[1].id).toEqual('foo'); + expect(paragraphs[2].id).toEqual('foo'); + }); + + 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 = load(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 = load(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 = load(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 = load(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 = load(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'); + }); + +}); diff --git a/packages/core/test/render3/properties_spec.ts b/packages/core/test/render3/properties_spec.ts index 1f92743c867..40851f00468 100644 --- a/packages/core/test/render3/properties_spec.ts +++ b/packages/core/test/render3/properties_spec.ts @@ -8,14 +8,12 @@ import {EventEmitter} from '@angular/core'; -import {AttributeMarker, defineComponent, template, defineDirective} from '../../src/render3/index'; +import {defineComponent, defineDirective} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, listener, load, reference, text, textBinding} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {NO_CHANGE} from '../../src/render3/tokens'; -import {pureFunction1, pureFunction2} from '../../src/render3/pure_function'; -import {ComponentFixture, TemplateFixture, createComponent, renderToHtml, createDirective} from './render_util'; -import {NgForOf} from './common_with_def'; +import {ComponentFixture, createComponent, renderToHtml} from './render_util'; describe('elementProperty', () => { @@ -81,531 +79,6 @@ describe('elementProperty', () => { expect(fixture.html).toEqual(''); }); - describe('host', () => { - let nameComp !: NameComp; - - class NameComp { - names !: string[]; - - static ngComponentDef = defineComponent({ - type: NameComp, - selectors: [['name-comp']], - factory: function NameComp_Factory() { return nameComp = new NameComp(); }, - consts: 0, - vars: 0, - template: function NameComp_Template(rf: RenderFlags, ctx: NameComp) {}, - inputs: {names: 'names'} - }); - } - - 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(load(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 = load(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 host bindings on multiple nodes', () => { - let hostBindingDir !: HostBindingDir; - - class HostBindingDir { - // @HostBinding() - id = 'foo'; - - static ngDirectiveDef = defineDirective({ - type: HostBindingDir, - selectors: [['', 'hostBindingDir', '']], - factory: () => hostBindingDir = new HostBindingDir(), - hostVars: 1, - hostBindings: (directiveIndex: number, elementIndex: number) => { - elementProperty(elementIndex, 'id', bind(load(directiveIndex).id)); - } - }); - } - - const SomeDir = createDirective('someDir'); - - class HostBindingComp { - // @HostBinding() - title = 'my-title'; - - 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 = load(dirIndex) as HostBindingComp; - elementProperty(elIndex, 'title', bind(ctx.title)); - }, - template: (rf: RenderFlags, ctx: HostBindingComp) => {} - }); - } - - /** - *
- *
- * - */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - element(0, 'div', ['hostBindingDir', '']); - element(1, 'div', ['someDir', '']); - element(2, 'host-binding-comp'); - } - }, 3, 0, [HostBindingDir, SomeDir, HostBindingComp]); - - const fixture = new ComponentFixture(App); - const hostBindingDiv = fixture.hostElement.querySelector('div') as HTMLElement; - const hostBindingComp = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; - expect(hostBindingDiv.id).toEqual('foo'); - expect(hostBindingComp.title).toEqual('my-title'); - - hostBindingDir.id = 'bar'; - fixture.update(); - expect(hostBindingDiv.id).toEqual('bar'); - }); - - it('should support host bindings on second template pass', () => { - class HostBindingDir { - // @HostBinding() - id = 'foo'; - - static ngDirectiveDef = defineDirective({ - type: HostBindingDir, - selectors: [['', 'hostBindingDir', '']], - factory: () => new HostBindingDir(), - hostVars: 1, - hostBindings: (directiveIndex: number, elementIndex: number) => { - elementProperty(elementIndex, 'id', bind(load(directiveIndex).id)); - } - }); - } - - /**
*/ - const Parent = createComponent('parent', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - element(0, 'div', ['hostBindingDir', '']); - } - }, 1, 0, [HostBindingDir]); - - /** - * - * - */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - element(0, 'parent'); - element(1, 'parent'); - } - }, 2, 0, [Parent]); - - const fixture = new ComponentFixture(App); - const divs = fixture.hostElement.querySelectorAll('div'); - expect(divs[0].id).toEqual('foo'); - expect(divs[1].id).toEqual('foo'); - }); - - it('should support host bindings in for loop', () => { - class HostBindingDir { - // @HostBinding() - id = 'foo'; - - static ngDirectiveDef = defineDirective({ - type: HostBindingDir, - selectors: [['', 'hostBindingDir', '']], - factory: () => new HostBindingDir(), - hostVars: 1, - hostBindings: (directiveIndex: number, elementIndex: number) => { - elementProperty(elementIndex, 'id', bind(load(directiveIndex).id)); - } - }); - } - - function NgForTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - elementStart(0, 'div'); - { element(1, 'p', ['hostBindingDir', '']); } - elementEnd(); - } - } - - /** - *
- *

- *
- */ - const App = createComponent('parent', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - template(0, NgForTemplate, 2, 0, null, ['ngForOf', '']); - } - if (rf & RenderFlags.Update) { - elementProperty(0, 'ngForOf', bind(ctx.rows)); - } - }, 1, 1, [HostBindingDir, NgForOf]); - - const fixture = new ComponentFixture(App); - fixture.component.rows = [1, 2, 3]; - fixture.update(); - - const paragraphs = fixture.hostElement.querySelectorAll('p'); - expect(paragraphs[0].id).toEqual('foo'); - expect(paragraphs[1].id).toEqual('foo'); - expect(paragraphs[2].id).toEqual('foo'); - }); - - 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 = load(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 = load(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 = load(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 = load(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 = load(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'); - }); - - }); - describe('input properties', () => { let button: MyButton; let otherDir: OtherDir;