fix(ivy): host bindings should work on nodes with providers (#26771)
PR Close #26771
This commit is contained in:
parent
f3859130f2
commit
f76ce84ae1
@ -28,9 +28,6 @@ import {enterView, leaveView, resetComponentState} from './state';
|
|||||||
import {getRootView, readElementValue, readPatchedLViewData, stringify} from './util';
|
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. */
|
/** Options that control how the component should be bootstrapped. */
|
||||||
export interface CreateComponentOptions {
|
export interface CreateComponentOptions {
|
||||||
/** Which renderer factory to use. */
|
/** Which renderer factory to use. */
|
||||||
@ -171,7 +168,6 @@ export function createRootComponentView(
|
|||||||
const tNode = createNodeAtIndex(0, TNodeType.Element, rNode, null, null);
|
const tNode = createNodeAtIndex(0, TNodeType.Element, rNode, null, null);
|
||||||
|
|
||||||
if (tView.firstTemplatePass) {
|
if (tView.firstTemplatePass) {
|
||||||
tView.expandoInstructions = ROOT_EXPANDO_INSTRUCTIONS.slice();
|
|
||||||
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), rootView, def.type);
|
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), rootView, def.type);
|
||||||
tNode.flags = TNodeFlags.isComponent;
|
tNode.flags = TNodeFlags.isComponent;
|
||||||
initNodeFlags(tNode, rootView.length, 1);
|
initNodeFlags(tNode, rootView.length, 1);
|
||||||
|
@ -23,7 +23,7 @@ import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} fro
|
|||||||
import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container';
|
import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container';
|
||||||
import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, InitialStylingFlags, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
|
import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, InitialStylingFlags, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
|
||||||
import {INJECTOR_SIZE, NodeInjectorFactory} from './interfaces/injector';
|
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 {PlayerFactory} from './interfaces/player';
|
||||||
import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
|
import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
|
||||||
import {LQueries} from './interfaces/query';
|
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
|
// Negative numbers mean that we are starting new EXPANDO block and need to update
|
||||||
// the current element and directive index.
|
// the current element and directive index.
|
||||||
currentElementIndex = -instruction;
|
currentElementIndex = -instruction;
|
||||||
// Injector block is taken into account.
|
// Injector block and providers are taken into account.
|
||||||
bindingRootIndex += INJECTOR_SIZE;
|
const providerCount = (tView.expandoInstructions[++i] as number);
|
||||||
|
bindingRootIndex += INJECTOR_SIZE + providerCount;
|
||||||
|
|
||||||
currentDirectiveIndex = bindingRootIndex;
|
currentDirectiveIndex = bindingRootIndex;
|
||||||
} else {
|
} else {
|
||||||
@ -1296,14 +1297,15 @@ export function textBinding<T>(index: number, value: T | NO_CHANGE): void {
|
|||||||
*/
|
*/
|
||||||
export function instantiateRootComponent<T>(
|
export function instantiateRootComponent<T>(
|
||||||
tView: TView, viewData: LViewData, def: ComponentDef<T>): T {
|
tView: TView, viewData: LViewData, def: ComponentDef<T>): T {
|
||||||
if (getFirstTemplatePass()) {
|
const rootTNode = getPreviousOrParentTNode();
|
||||||
|
if (tView.firstTemplatePass) {
|
||||||
if (def.providersResolver) def.providersResolver(def);
|
if (def.providersResolver) def.providersResolver(def);
|
||||||
|
generateExpandoInstructionBlock(tView, rootTNode, 1);
|
||||||
baseResolveDirective(tView, viewData, def, def.factory);
|
baseResolveDirective(tView, viewData, def, def.factory);
|
||||||
}
|
}
|
||||||
const previousOrParentTNode = getPreviousOrParentTNode();
|
const directive =
|
||||||
const directive = getNodeInjectable(
|
getNodeInjectable(tView.data, viewData, viewData.length - 1, rootTNode as TElementNode);
|
||||||
tView.data, viewData, viewData.length - 1, previousOrParentTNode as TElementNode);
|
postProcessBaseDirective(viewData, rootTNode, directive, def as DirectiveDef<T>);
|
||||||
postProcessBaseDirective(viewData, previousOrParentTNode, directive, def as DirectiveDef<T>);
|
|
||||||
return directive;
|
return directive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1316,7 +1318,6 @@ function resolveDirectives(
|
|||||||
// Please make sure to have explicit type for `exportsMap`. Inferred type triggers bug in tsickle.
|
// 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');
|
ngDevMode && assertEqual(getFirstTemplatePass(), true, 'should run on first template pass only');
|
||||||
const exportsMap: ({[key: string]: number} | null) = localRefs ? {'': -1} : null;
|
const exportsMap: ({[key: string]: number} | null) = localRefs ? {'': -1} : null;
|
||||||
generateExpandoInstructionBlock(tView, tNode, directives);
|
|
||||||
let totalHostVars = 0;
|
let totalHostVars = 0;
|
||||||
if (directives) {
|
if (directives) {
|
||||||
initNodeFlags(tNode, tView.data.length, directives.length);
|
initNodeFlags(tNode, tView.data.length, directives.length);
|
||||||
@ -1330,6 +1331,7 @@ function resolveDirectives(
|
|||||||
const def = directives[i] as DirectiveDef<any>;
|
const def = directives[i] as DirectiveDef<any>;
|
||||||
if (def.providersResolver) def.providersResolver(def);
|
if (def.providersResolver) def.providersResolver(def);
|
||||||
}
|
}
|
||||||
|
generateExpandoInstructionBlock(tView, tNode, directives.length);
|
||||||
for (let i = 0; i < directives.length; i++) {
|
for (let i = 0; i < directives.length; i++) {
|
||||||
const def = directives[i] as DirectiveDef<any>;
|
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
|
* 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.
|
* it from the hostVar count) and the directive count. See more in VIEW_DATA.md.
|
||||||
*/
|
*/
|
||||||
function generateExpandoInstructionBlock(
|
export function generateExpandoInstructionBlock(
|
||||||
tView: TView, tNode: TNode, directives: DirectiveDef<any>[] | null): void {
|
tView: TView, tNode: TNode, directiveCount: number): void {
|
||||||
const directiveCount = directives ? directives.length : 0;
|
ngDevMode && assertEqual(
|
||||||
|
tView.firstTemplatePass, true,
|
||||||
|
'Expando block should only be generated on first template pass.');
|
||||||
|
|
||||||
const elementIndex = -(tNode.index - HEADER_OFFSET);
|
const elementIndex = -(tNode.index - HEADER_OFFSET);
|
||||||
if (directiveCount > 0) {
|
const providerStartIndex = tNode.providerIndexes & TNodeProviderIndexes.ProvidersStartIndexMask;
|
||||||
|
const providerCount = tView.data.length - providerStartIndex;
|
||||||
(tView.expandoInstructions || (tView.expandoInstructions = [
|
(tView.expandoInstructions || (tView.expandoInstructions = [
|
||||||
])).push(elementIndex, directiveCount);
|
])).push(elementIndex, providerCount, directiveCount);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -170,9 +170,6 @@
|
|||||||
{
|
{
|
||||||
"name": "RENDER_PARENT"
|
"name": "RENDER_PARENT"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "ROOT_EXPANDO_INSTRUCTIONS"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "RecordViewTuple"
|
"name": "RecordViewTuple"
|
||||||
},
|
},
|
||||||
|
@ -104,9 +104,6 @@
|
|||||||
{
|
{
|
||||||
"name": "RENDER_PARENT"
|
"name": "RENDER_PARENT"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "ROOT_EXPANDO_INSTRUCTIONS"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "SANITIZER"
|
"name": "SANITIZER"
|
||||||
},
|
},
|
||||||
@ -218,6 +215,9 @@
|
|||||||
{
|
{
|
||||||
"name": "firstTemplatePass"
|
"name": "firstTemplatePass"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "generateExpandoInstructionBlock"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getBeforeNodeForView"
|
"name": "getBeforeNodeForView"
|
||||||
},
|
},
|
||||||
|
@ -164,9 +164,6 @@
|
|||||||
{
|
{
|
||||||
"name": "RENDER_PARENT"
|
"name": "RENDER_PARENT"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "ROOT_EXPANDO_INSTRUCTIONS"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "RecordViewTuple"
|
"name": "RecordViewTuple"
|
||||||
},
|
},
|
||||||
|
@ -698,9 +698,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ROOT_CONTEXT"
|
"name": "ROOT_CONTEXT"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "ROOT_EXPANDO_INSTRUCTIONS"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "RecordViewTuple"
|
"name": "RecordViewTuple"
|
||||||
},
|
},
|
||||||
|
583
packages/core/test/render3/host_binding_spec.ts
Normal file
583
packages/core/test/render3/host_binding_spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -8,14 +8,12 @@
|
|||||||
|
|
||||||
import {EventEmitter} from '@angular/core';
|
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 {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 {RenderFlags} from '../../src/render3/interfaces/definition';
|
||||||
import {NO_CHANGE} from '../../src/render3/tokens';
|
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 {ComponentFixture, createComponent, renderToHtml} from './render_util';
|
||||||
import {NgForOf} from './common_with_def';
|
|
||||||
|
|
||||||
describe('elementProperty', () => {
|
describe('elementProperty', () => {
|
||||||
|
|
||||||
@ -81,531 +79,6 @@ describe('elementProperty', () => {
|
|||||||
expect(fixture.html).toEqual('<span id="_otherId_"></span>');
|
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', () => {
|
describe('input properties', () => {
|
||||||
let button: MyButton;
|
let button: MyButton;
|
||||||
let otherDir: OtherDir;
|
let otherDir: OtherDir;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user