fix(ivy): host bindings should work on nodes with providers (#26771)

PR Close #26771
This commit is contained in:
Kara Erickson 2018-10-25 19:10:32 -07:00 committed by Matias Niemelä
parent f3859130f2
commit f76ce84ae1
8 changed files with 609 additions and 561 deletions

View File

@ -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);

View File

@ -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<T>(index: number, value: T | NO_CHANGE): void {
*/
export function instantiateRootComponent<T>(
tView: TView, viewData: LViewData, def: ComponentDef<T>): 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<T>);
const directive =
getNodeInjectable(tView.data, viewData, viewData.length - 1, rootTNode as TElementNode);
postProcessBaseDirective(viewData, rootTNode, directive, def as DirectiveDef<T>);
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<any>;
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<any>;
@ -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<any>[] | 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);
}
/**

View File

@ -170,9 +170,6 @@
{
"name": "RENDER_PARENT"
},
{
"name": "ROOT_EXPANDO_INSTRUCTIONS"
},
{
"name": "RecordViewTuple"
},

View File

@ -104,9 +104,6 @@
{
"name": "RENDER_PARENT"
},
{
"name": "ROOT_EXPANDO_INSTRUCTIONS"
},
{
"name": "SANITIZER"
},
@ -218,6 +215,9 @@
{
"name": "firstTemplatePass"
},
{
"name": "generateExpandoInstructionBlock"
},
{
"name": "getBeforeNodeForView"
},

View File

@ -164,9 +164,6 @@
{
"name": "RENDER_PARENT"
},
{
"name": "ROOT_EXPANDO_INSTRUCTIONS"
},
{
"name": "RecordViewTuple"
},

View File

@ -698,9 +698,6 @@
{
"name": "ROOT_CONTEXT"
},
{
"name": "ROOT_EXPANDO_INSTRUCTIONS"
},
{
"name": "RecordViewTuple"
},

View File

@ -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<Directive>(directiveIndex).klass));
}
});
}
function Template() { element(0, 'span', [AttributeMarker.SelectOnly, 'dir']); }
const fixture = new TemplateFixture(Template, () => {}, 1, 0, [Directive]);
expect(fixture.html).toEqual('<span class="foo"></span>');
directiveInstance !.klass = 'bar';
fixture.update();
expect(fixture.html).toEqual('<span class="bar"></span>');
});
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<HostBindingDir>(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) => {}
});
}
/**
* <div hostBindingDir></div>
* <div someDir></div>
* <host-binding-comp></host-binding-comp>
*/
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<HostBindingDir>(directiveIndex).id));
}
});
}
/** <div hostBindingDir></div> */
const Parent = createComponent('parent', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'div', ['hostBindingDir', '']);
}
}, 1, 0, [HostBindingDir]);
/**
* <parent></parent>
* <parent></parent>
*/
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<HostBindingDir>(directiveIndex).id));
}
});
}
function NgForTemplate(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
{ element(1, 'p', ['hostBindingDir', '']); }
elementEnd();
}
}
/**
* <div *ngFor="let row of rows">
* <p hostBindingDir></p>
* </div>
*/
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) => {}
});
}
/**
* <name-comp [names]="['Nancy', name, 'Ned']"></name-comp>
* <host-binding-comp></host-binding-comp>
*/
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) => {}
});
}
/**
* <name-comp [names]="[name, 'Nancy', otherName]"></name-comp>
* <host-binding-comp></host-binding-comp>
*/
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)));
}
});
}
/**
* <host-binding-comp hostDir>
* </host-binding-comp>
*/
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) => {}
});
}
/**
* <host-binding-comp></host-binding-comp>
* {{ 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(`<host-binding-comp id="red,blue" title="blue"></host-binding-comp>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');
});
});

View File

@ -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('<span id="_otherId_"></span>');
});
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<Directive>(directiveIndex).klass));
}
});
}
function Template() { element(0, 'span', [AttributeMarker.SelectOnly, 'dir']); }
const fixture = new TemplateFixture(Template, () => {}, 1, 0, [Directive]);
expect(fixture.html).toEqual('<span class="foo"></span>');
directiveInstance !.klass = 'bar';
fixture.update();
expect(fixture.html).toEqual('<span class="bar"></span>');
});
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<HostBindingDir>(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) => {}
});
}
/**
* <div hostBindingDir></div>
* <div someDir></div>
* <host-binding-comp></host-binding-comp>
*/
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<HostBindingDir>(directiveIndex).id));
}
});
}
/** <div hostBindingDir></div> */
const Parent = createComponent('parent', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'div', ['hostBindingDir', '']);
}
}, 1, 0, [HostBindingDir]);
/**
* <parent></parent>
* <parent></parent>
*/
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<HostBindingDir>(directiveIndex).id));
}
});
}
function NgForTemplate(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
{ element(1, 'p', ['hostBindingDir', '']); }
elementEnd();
}
}
/**
* <div *ngFor="let row of rows">
* <p hostBindingDir></p>
* </div>
*/
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) => {}
});
}
/**
* <name-comp [names]="['Nancy', name, 'Ned']"></name-comp>
* <host-binding-comp></host-binding-comp>
*/
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) => {}
});
}
/**
* <name-comp [names]="[name, 'Nancy', otherName]"></name-comp>
* <host-binding-comp></host-binding-comp>
*/
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)));
}
});
}
/**
* <host-binding-comp hostDir>
* </host-binding-comp>
*/
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) => {}
});
}
/**
* <host-binding-comp></host-binding-comp>
* {{ 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(`<host-binding-comp id="red,blue" title="blue"></host-binding-comp>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;