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';
|
||||
|
||||
|
||||
// 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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -170,9 +170,6 @@
|
|||
{
|
||||
"name": "RENDER_PARENT"
|
||||
},
|
||||
{
|
||||
"name": "ROOT_EXPANDO_INSTRUCTIONS"
|
||||
},
|
||||
{
|
||||
"name": "RecordViewTuple"
|
||||
},
|
||||
|
|
|
@ -104,9 +104,6 @@
|
|||
{
|
||||
"name": "RENDER_PARENT"
|
||||
},
|
||||
{
|
||||
"name": "ROOT_EXPANDO_INSTRUCTIONS"
|
||||
},
|
||||
{
|
||||
"name": "SANITIZER"
|
||||
},
|
||||
|
@ -218,6 +215,9 @@
|
|||
{
|
||||
"name": "firstTemplatePass"
|
||||
},
|
||||
{
|
||||
"name": "generateExpandoInstructionBlock"
|
||||
},
|
||||
{
|
||||
"name": "getBeforeNodeForView"
|
||||
},
|
||||
|
|
|
@ -164,9 +164,6 @@
|
|||
{
|
||||
"name": "RENDER_PARENT"
|
||||
},
|
||||
{
|
||||
"name": "ROOT_EXPANDO_INSTRUCTIONS"
|
||||
},
|
||||
{
|
||||
"name": "RecordViewTuple"
|
||||
},
|
||||
|
|
|
@ -698,9 +698,6 @@
|
|||
{
|
||||
"name": "ROOT_CONTEXT"
|
||||
},
|
||||
{
|
||||
"name": "ROOT_EXPANDO_INSTRUCTIONS"
|
||||
},
|
||||
{
|
||||
"name": "RecordViewTuple"
|
||||
},
|
||||
|
|
|
@ -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 {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;
|
||||
|
|
Loading…
Reference in New Issue