/** * @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, PublicFeature, defineComponent, template, defineDirective} from '../../src/render3/index'; import {NO_CHANGE, 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 {pureFunction1, pureFunction2} from '../../src/render3/pure_function'; import {ComponentFixture, TemplateFixture, createComponent, renderToHtml, createDirective} from './render_util'; import {NgForOf} from './common_with_def'; describe('elementProperty', () => { it('should support bindings to properties', () => { const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'span'); } if (rf & RenderFlags.Update) { elementProperty(0, 'id', bind(ctx.id)); } }, 1, 1); const fixture = new ComponentFixture(App); fixture.component.id = 'testId'; fixture.update(); expect(fixture.html).toEqual(''); fixture.component.id = 'otherId'; fixture.update(); expect(fixture.html).toEqual(''); }); it('should support creation time bindings to properties', () => { function expensive(ctx: string): any { if (ctx === 'cheapId') { return ctx; } else { throw 'Too expensive!'; } } function Template(rf: RenderFlags, ctx: string) { if (rf & RenderFlags.Create) { element(0, 'span'); } if (rf & RenderFlags.Update) { elementProperty(0, 'id', rf & RenderFlags.Create ? expensive(ctx) : NO_CHANGE); } } expect(renderToHtml(Template, 'cheapId', 1)).toEqual(''); expect(renderToHtml(Template, 'expensiveId', 1)).toEqual(''); }); it('should support interpolation for properties', () => { const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'span'); } if (rf & RenderFlags.Update) { elementProperty(0, 'id', interpolation1('_', ctx.id, '_')); } }, 1, 1); const fixture = new ComponentFixture(App); fixture.component.id = 'testId'; fixture.update(); expect(fixture.html).toEqual(''); fixture.component.id = 'otherId'; fixture.update(); expect(fixture.html).toEqual(''); }); describe('host', () => { let nameComp !: NameComp; class NameComp { names !: string[]; static ngComponentDef = defineComponent({ type: NameComp, selectors: [['name-comp']], factory: function NameComp_Factory() { return nameComp = new NameComp(); }, consts: 0, vars: 0, template: function NameComp_Template(rf: RenderFlags, ctx: NameComp) {}, inputs: {names: 'names'} }); } it('should support host bindings in directives', () => { let directiveInstance: Directive|undefined; class Directive { // @HostBinding('className') klass = 'foo'; static ngDirectiveDef = defineDirective({ type: Directive, selectors: [['', 'dir', '']], factory: () => directiveInstance = new Directive, hostVars: 1, hostBindings: (directiveIndex: number, elementIndex: number) => { elementProperty(elementIndex, 'className', bind(load(directiveIndex).klass)); } }); } function Template() { element(0, 'span', [AttributeMarker.SelectOnly, 'dir']); } const fixture = new TemplateFixture(Template, () => {}, 1, 0, [Directive]); expect(fixture.html).toEqual(''); directiveInstance !.klass = 'bar'; fixture.update(); expect(fixture.html).toEqual(''); }); it('should support host bindings on root component', () => { class HostBindingComp { // @HostBinding() id = 'my-id'; static ngComponentDef = defineComponent({ type: HostBindingComp, selectors: [['host-binding-comp']], factory: () => new HostBindingComp(), consts: 0, vars: 0, hostVars: 1, hostBindings: (dirIndex: number, elIndex: number) => { const instance = load(dirIndex) as HostBindingComp; elementProperty(elIndex, 'id', bind(instance.id)); }, template: (rf: RenderFlags, ctx: HostBindingComp) => {} }); } const fixture = new ComponentFixture(HostBindingComp); expect(fixture.hostElement.id).toBe('my-id'); fixture.component.id = 'other-id'; fixture.update(); expect(fixture.hostElement.id).toBe('other-id'); }); it('should support host bindings on multiple nodes', () => { let hostBindingDir !: HostBindingDir; class HostBindingDir { // @HostBinding() id = 'foo'; static ngDirectiveDef = defineDirective({ type: HostBindingDir, selectors: [['', 'hostBindingDir', '']], factory: () => hostBindingDir = new HostBindingDir(), hostVars: 1, hostBindings: (directiveIndex: number, elementIndex: number) => { elementProperty(elementIndex, 'id', bind(load(directiveIndex).id)); }, features: [PublicFeature] }); } 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) => {}, features: [PublicFeature] }); } /** *
*
* */ const App = createComponent('app', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { element(0, 'div', ['hostBindingDir', '']); element(1, 'div', ['someDir', '']); element(2, 'host-binding-comp'); } }, 3, 0, [HostBindingDir, SomeDir, HostBindingComp]); const fixture = new ComponentFixture(App); const hostBindingDiv = fixture.hostElement.querySelector('div') as HTMLElement; const hostBindingComp = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; expect(hostBindingDiv.id).toEqual('foo'); expect(hostBindingComp.title).toEqual('my-title'); hostBindingDir.id = 'bar'; fixture.update(); expect(hostBindingDiv.id).toEqual('bar'); }); it('should support host bindings on second template pass', () => { class HostBindingDir { // @HostBinding() id = 'foo'; static ngDirectiveDef = defineDirective({ type: HostBindingDir, selectors: [['', 'hostBindingDir', '']], factory: () => new HostBindingDir(), hostVars: 1, hostBindings: (directiveIndex: number, elementIndex: number) => { elementProperty(elementIndex, 'id', bind(load(directiveIndex).id)); }, features: [PublicFeature] }); } /**
*/ const Parent = createComponent('parent', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { element(0, 'div', ['hostBindingDir', '']); } }, 1, 0, [HostBindingDir]); /** * * */ const App = createComponent('app', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { element(0, 'parent'); element(1, 'parent'); } }, 2, 0, [Parent]); const fixture = new ComponentFixture(App); const divs = fixture.hostElement.querySelectorAll('div'); expect(divs[0].id).toEqual('foo'); expect(divs[1].id).toEqual('foo'); }); it('should support host bindings in for loop', () => { class HostBindingDir { // @HostBinding() id = 'foo'; static ngDirectiveDef = defineDirective({ type: HostBindingDir, selectors: [['', 'hostBindingDir', '']], factory: () => new HostBindingDir(), hostVars: 1, hostBindings: (directiveIndex: number, elementIndex: number) => { elementProperty(elementIndex, 'id', bind(load(directiveIndex).id)); }, features: [PublicFeature] }); } function NgForTemplate(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'div'); { element(1, 'p', ['hostBindingDir', '']); } elementEnd(); } } /** *
*

*
*/ const App = createComponent('parent', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { template(0, NgForTemplate, 2, 0, null, ['ngForOf', '']); } if (rf & RenderFlags.Update) { elementProperty(0, 'ngForOf', bind(ctx.rows)); } }, 1, 1, [HostBindingDir, NgForOf]); const fixture = new ComponentFixture(App); fixture.component.rows = [1, 2, 3]; fixture.update(); const paragraphs = fixture.hostElement.querySelectorAll('p'); expect(paragraphs[0].id).toEqual('foo'); expect(paragraphs[1].id).toEqual('foo'); expect(paragraphs[2].id).toEqual('foo'); }); it('should support component with host bindings and array literals', () => { const ff = (v: any) => ['Nancy', v, 'Ned']; class HostBindingComp { // @HostBinding() id = 'my-id'; static ngComponentDef = defineComponent({ type: HostBindingComp, selectors: [['host-binding-comp']], factory: () => new HostBindingComp(), consts: 0, vars: 0, hostVars: 1, hostBindings: (dirIndex: number, elIndex: number) => { const ctx = load(dirIndex) as HostBindingComp; elementProperty(elIndex, 'id', bind(ctx.id)); }, template: (rf: RenderFlags, ctx: HostBindingComp) => {} }); } /** * * */ const AppComponent = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'name-comp'); element(1, 'host-binding-comp'); } if (rf & RenderFlags.Update) { elementProperty(0, 'names', bind(pureFunction1(1, ff, ctx.name))); } }, 2, 3, [HostBindingComp, NameComp]); const fixture = new ComponentFixture(AppComponent); const hostBindingEl = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; fixture.component.name = 'Betty'; fixture.update(); expect(hostBindingEl.id).toBe('my-id'); expect(nameComp.names).toEqual(['Nancy', 'Betty', 'Ned']); const firstArray = nameComp.names; fixture.update(); expect(firstArray).toBe(nameComp.names); fixture.component.name = 'my-id'; fixture.update(); expect(hostBindingEl.id).toBe('my-id'); expect(nameComp.names).toEqual(['Nancy', 'my-id', 'Ned']); }); // Note: This is a contrived example. For feature parity with render2, we should make sure it // works in this way (see https://stackblitz.com/edit/angular-cbqpbe), but a more realistic // example would be an animation host binding with a literal defining the animation config. // When animation support is added, we should add another test for that case. it('should support host bindings that contain array literals', () => { const ff = (v: any) => ['red', v]; const ff2 = (v: any, v2: any) => [v, v2]; const ff3 = (v: any, v2: any) => [v, 'Nancy', v2]; let hostBindingComp !: HostBindingComp; /** * @Component({ * ... * host: { * `[id]`: `['red', id]`, * `[dir]`: `dir`, * `[title]`: `[title, otherTitle]` * } * }) * */ class HostBindingComp { id = 'blue'; dir = 'ltr'; title = 'my title'; otherTitle = 'other title'; static ngComponentDef = defineComponent({ type: HostBindingComp, selectors: [['host-binding-comp']], factory: () => hostBindingComp = new HostBindingComp(), consts: 0, vars: 0, hostVars: 8, hostBindings: (dirIndex: number, elIndex: number) => { const ctx = load(dirIndex) as HostBindingComp; // LViewData: [..., id, dir, title, ctx.id, pf1, ctx.title, ctx.otherTitle, pf2] elementProperty(elIndex, 'id', bind(pureFunction1(3, ff, ctx.id))); elementProperty(elIndex, 'dir', bind(ctx.dir)); elementProperty( elIndex, 'title', bind(pureFunction2(5, ff2, ctx.title, ctx.otherTitle))); }, template: (rf: RenderFlags, ctx: HostBindingComp) => {} }); } /** * * */ const AppComponent = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'name-comp'); element(1, 'host-binding-comp'); } if (rf & RenderFlags.Update) { elementProperty(0, 'names', bind(pureFunction2(1, ff3, ctx.name, ctx.otherName))); } }, 2, 4, [HostBindingComp, NameComp]); const fixture = new ComponentFixture(AppComponent); fixture.component.name = 'Frank'; fixture.component.otherName = 'Joe'; fixture.update(); const hostBindingEl = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; expect(hostBindingEl.id).toBe('red,blue'); expect(hostBindingEl.dir).toBe('ltr'); expect(hostBindingEl.title).toBe('my title,other title'); expect(nameComp.names).toEqual(['Frank', 'Nancy', 'Joe']); const firstArray = nameComp.names; fixture.update(); expect(firstArray).toBe(nameComp.names); hostBindingComp.id = 'green'; hostBindingComp.dir = 'rtl'; hostBindingComp.title = 'TITLE'; fixture.update(); expect(hostBindingEl.id).toBe('red,green'); expect(hostBindingEl.dir).toBe('rtl'); expect(hostBindingEl.title).toBe('TITLE,other title'); }); it('should support host bindings with literals from multiple directives', () => { let hostBindingComp !: HostBindingComp; let hostBindingDir !: HostBindingDir; const ff = (v: any) => ['red', v]; /** * @Component({ * ... * host: { * '[id]': '['red', id]' * } * }) * */ class HostBindingComp { id = 'blue'; static ngComponentDef = defineComponent({ type: HostBindingComp, selectors: [['host-binding-comp']], factory: () => hostBindingComp = new HostBindingComp(), consts: 0, vars: 0, hostVars: 3, hostBindings: (dirIndex: number, elIndex: number) => { // LViewData: [..., id, ctx.id, pf1] const ctx = load(dirIndex) as HostBindingComp; elementProperty(elIndex, 'id', bind(pureFunction1(1, ff, ctx.id))); }, template: (rf: RenderFlags, ctx: HostBindingComp) => {} }); } const ff1 = (v: any) => [v, 'other title']; /** * @Directive({ * ... * host: { * '[title]': '[title, 'other title']' * } * }) * */ class HostBindingDir { title = 'my title'; static ngDirectiveDef = defineDirective({ type: HostBindingDir, selectors: [['', 'hostDir', '']], factory: () => hostBindingDir = new HostBindingDir(), hostVars: 3, hostBindings: (dirIndex: number, elIndex: number) => { // LViewData [..., title, ctx.title, pf1] const ctx = load(dirIndex) as HostBindingDir; elementProperty(elIndex, 'title', bind(pureFunction1(1, ff1, ctx.title))); } }); } /** * * */ const AppComponent = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'host-binding-comp', ['hostDir', '']); } }, 1, 0, [HostBindingComp, HostBindingDir]); const fixture = new ComponentFixture(AppComponent); const hostElement = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; expect(hostElement.id).toBe('red,blue'); expect(hostElement.title).toBe('my title,other title'); hostBindingDir.title = 'blue'; fixture.update(); expect(hostElement.title).toBe('blue,other title'); hostBindingComp.id = 'green'; fixture.update(); expect(hostElement.id).toBe('red,green'); }); it('should support ternary expressions in host bindings', () => { let hostBindingComp !: HostBindingComp; const ff = (v: any) => ['red', v]; const ff1 = (v: any) => [v]; /** * @Component({ * ... * host: { * `[id]`: `condition ? ['red', id] : 'green'`, * `[title]`: `otherCondition ? [title] : 'other title'` * } * }) * */ class HostBindingComp { condition = true; otherCondition = true; id = 'blue'; title = 'blue'; static ngComponentDef = defineComponent({ type: HostBindingComp, selectors: [['host-binding-comp']], factory: () => hostBindingComp = new HostBindingComp(), consts: 0, vars: 0, hostVars: 6, hostBindings: (dirIndex: number, elIndex: number) => { // LViewData: [..., id, title, ctx.id, pf1, ctx.title, pf1] const ctx = load(dirIndex) as HostBindingComp; elementProperty( elIndex, 'id', bind(ctx.condition ? pureFunction1(2, ff, ctx.id) : 'green')); elementProperty( elIndex, 'title', bind(ctx.otherCondition ? pureFunction1(4, ff1, ctx.title) : 'other title')); }, template: (rf: RenderFlags, ctx: HostBindingComp) => {} }); } /** * * {{ name }} */ const AppComponent = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'host-binding-comp'); text(1); } if (rf & RenderFlags.Update) { textBinding(1, bind(ctx.name)); } }, 2, 1, [HostBindingComp]); const fixture = new ComponentFixture(AppComponent); const hostElement = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; fixture.component.name = 'Ned'; fixture.update(); expect(hostElement.id).toBe('red,blue'); expect(hostElement.title).toBe('blue'); expect(fixture.html) .toEqual(`Ned`); hostBindingComp.condition = false; hostBindingComp.title = 'TITLE'; fixture.update(); expect(hostElement.id).toBe('green'); expect(hostElement.title).toBe('TITLE'); hostBindingComp.otherCondition = false; fixture.update(); expect(hostElement.id).toBe('green'); expect(hostElement.title).toBe('other title'); }); }); describe('input properties', () => { let button: MyButton; let otherDir: OtherDir; let otherDisabledDir: OtherDisabledDir; let idDir: IdDir; class MyButton { // TODO(issue/24571): remove '!'. disabled !: boolean; static ngDirectiveDef = defineDirective({ type: MyButton, selectors: [['', 'myButton', '']], factory: () => button = new MyButton(), inputs: {disabled: 'disabled'} }); } class OtherDir { // TODO(issue/24571): remove '!'. id !: number; clickStream = new EventEmitter(); static ngDirectiveDef = defineDirective({ type: OtherDir, selectors: [['', 'otherDir', '']], factory: () => otherDir = new OtherDir(), inputs: {id: 'id'}, outputs: {clickStream: 'click'} }); } class OtherDisabledDir { // TODO(issue/24571): remove '!'. disabled !: boolean; static ngDirectiveDef = defineDirective({ type: OtherDisabledDir, selectors: [['', 'otherDisabledDir', '']], factory: () => otherDisabledDir = new OtherDisabledDir(), inputs: {disabled: 'disabled'} }); } class IdDir { // TODO(issue/24571): remove '!'. idNumber !: string; static ngDirectiveDef = defineDirective({ type: IdDir, selectors: [['', 'idDir', '']], factory: () => idDir = new IdDir(), inputs: {idNumber: 'id'} }); } const deps = [MyButton, OtherDir, OtherDisabledDir, IdDir]; it('should check input properties before setting (directives)', () => { /** */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'button', ['otherDir', '', 'myButton', '']); { text(1, 'Click me'); } elementEnd(); } if (rf & RenderFlags.Update) { elementProperty(0, 'disabled', bind(ctx.isDisabled)); elementProperty(0, 'id', bind(ctx.id)); } }, 2, 2, deps); const fixture = new ComponentFixture(App); fixture.component.isDisabled = true; fixture.component.id = 0; fixture.update(); expect(fixture.html).toEqual(``); expect(button !.disabled).toEqual(true); expect(otherDir !.id).toEqual(0); fixture.component.isDisabled = false; fixture.component.id = 1; fixture.update(); expect(fixture.html).toEqual(``); expect(button !.disabled).toEqual(false); expect(otherDir !.id).toEqual(1); }); it('should support mixed element properties and input properties', () => { /** */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'button', ['myButton', '']); { text(1, 'Click me'); } elementEnd(); } if (rf & RenderFlags.Update) { elementProperty(0, 'disabled', bind(ctx.isDisabled)); elementProperty(0, 'id', bind(ctx.id)); } }, 2, 2, deps); const fixture = new ComponentFixture(App); fixture.component.isDisabled = true; fixture.component.id = 0; fixture.update(); expect(fixture.html).toEqual(``); expect(button !.disabled).toEqual(true); fixture.component.isDisabled = false; fixture.component.id = 1; fixture.update(); expect(fixture.html).toEqual(``); expect(button !.disabled).toEqual(false); }); it('should check that property is not an input property before setting (component)', () => { let comp: Comp; class Comp { // TODO(issue/24571): remove '!'. id !: number; static ngComponentDef = defineComponent({ type: Comp, selectors: [['comp']], consts: 0, vars: 0, template: function(rf: RenderFlags, ctx: any) {}, factory: () => comp = new Comp(), inputs: {id: 'id'} }); } /** */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'comp'); } if (rf & RenderFlags.Update) { elementProperty(0, 'id', bind(ctx.id)); } }, 1, 1, [Comp]); const fixture = new ComponentFixture(App); fixture.component.id = 1; fixture.update(); expect(fixture.html).toEqual(``); expect(comp !.id).toEqual(1); fixture.component.id = 2; fixture.update(); expect(fixture.html).toEqual(``); expect(comp !.id).toEqual(2); }); it('should support two input properties with the same name', () => { /** */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'button', ['myButton', '', 'otherDisabledDir', '']); { text(1, 'Click me'); } elementEnd(); } if (rf & RenderFlags.Update) { elementProperty(0, 'disabled', bind(ctx.isDisabled)); } }, 2, 1, deps); const fixture = new ComponentFixture(App); fixture.component.isDisabled = true; fixture.update(); expect(fixture.html).toEqual(``); expect(button !.disabled).toEqual(true); expect(otherDisabledDir !.disabled).toEqual(true); fixture.component.isDisabled = false; fixture.update(); expect(fixture.html).toEqual(``); expect(button !.disabled).toEqual(false); expect(otherDisabledDir !.disabled).toEqual(false); }); it('should set input property if there is an output first', () => { /** */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'button', ['otherDir', '']); { listener('click', () => ctx.onClick()); text(1, 'Click me'); } elementEnd(); } if (rf & RenderFlags.Update) { elementProperty(0, 'id', bind(ctx.id)); } }, 2, 1, deps); const fixture = new ComponentFixture(App); let counter = 0; fixture.component.id = 1; fixture.component.onClick = () => counter++; fixture.update(); expect(fixture.html).toEqual(``); expect(otherDir !.id).toEqual(1); otherDir !.clickStream.next(); expect(counter).toEqual(1); fixture.component.id = 2; fixture.update(); fixture.html; expect(otherDir !.id).toEqual(2); }); it('should support unrelated element properties at same index in if-else block', () => { /** * // inputs: {'id': [0, 'idNumber']} * % if (condition) { * // inputs: null * % } else { * // inputs: {'id': [0, 'id']} * % } */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'button', ['idDir', '']); { text(1, 'Click me'); } elementEnd(); container(2); } if (rf & RenderFlags.Update) { elementProperty(0, 'id', bind(ctx.id1)); containerRefreshStart(2); { if (ctx.condition) { let rf0 = embeddedViewStart(0, 2, 1); if (rf0 & RenderFlags.Create) { elementStart(0, 'button'); { text(1, 'Click me too'); } elementEnd(); } if (rf0 & RenderFlags.Update) { elementProperty(0, 'id', bind(ctx.id2)); } embeddedViewEnd(); } else { let rf1 = embeddedViewStart(1, 2, 1); if (rf1 & RenderFlags.Create) { elementStart(0, 'button', ['otherDir', '']); { text(1, 'Click me too'); } elementEnd(); } if (rf1 & RenderFlags.Update) { elementProperty(0, 'id', bind(ctx.id3)); } embeddedViewEnd(); } } containerRefreshEnd(); } }, 3, 1, deps); const fixture = new ComponentFixture(App); fixture.component.condition = true; fixture.component.id1 = 'one'; fixture.component.id2 = 'two'; fixture.component.id3 = 3; fixture.update(); expect(fixture.html) .toEqual(``); expect(idDir !.idNumber).toEqual('one'); fixture.component.condition = false; fixture.component.id1 = 'four'; fixture.update(); expect(fixture.html) .toEqual(``); expect(idDir !.idNumber).toEqual('four'); expect(otherDir !.id).toEqual(3); }); }); describe('attributes and input properties', () => { let myDir: MyDir; class MyDir { // TODO(issue/24571): remove '!'. role !: string; // TODO(issue/24571): remove '!'. direction !: string; changeStream = new EventEmitter(); static ngDirectiveDef = defineDirective({ type: MyDir, selectors: [['', 'myDir', '']], factory: () => myDir = new MyDir(), inputs: {role: 'role', direction: 'dir'}, outputs: {changeStream: 'change'}, exportAs: 'myDir' }); } let dirB: MyDirB; class MyDirB { // TODO(issue/24571): remove '!'. roleB !: string; static ngDirectiveDef = defineDirective({ type: MyDirB, selectors: [['', 'myDirB', '']], factory: () => dirB = new MyDirB(), inputs: {roleB: 'role'} }); } const deps = [MyDir, MyDirB]; it('should set input property based on attribute if existing', () => { /**
*/ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'div', ['role', 'button', 'myDir', '']); } }, 1, 0, deps); const fixture = new ComponentFixture(App); expect(fixture.html).toEqual(`
`); expect(myDir !.role).toEqual('button'); }); it('should set input property and attribute if both defined', () => { /**
*/ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'div', ['role', 'button', 'myDir', '']); } if (rf & RenderFlags.Update) { elementProperty(0, 'role', bind(ctx.role)); } }, 1, 1, deps); const fixture = new ComponentFixture(App); fixture.component.role = 'listbox'; fixture.update(); expect(fixture.html).toEqual(`
`); expect(myDir !.role).toEqual('listbox'); fixture.component.role = 'button'; fixture.update(); expect(myDir !.role).toEqual('button'); }); it('should set two directive input properties based on same attribute', () => { /**
*/ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'div', ['role', 'button', 'myDir', '', 'myDirB', '']); } }, 1, 0, deps); const fixture = new ComponentFixture(App); expect(fixture.html).toEqual(`
`); expect(myDir !.role).toEqual('button'); expect(dirB !.roleB).toEqual('button'); }); it('should process two attributes on same directive', () => { /**
*/ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'div', ['role', 'button', 'dir', 'rtl', 'myDir', '']); } }, 1, 0, deps); const fixture = new ComponentFixture(App); expect(fixture.html).toEqual(`
`); expect(myDir !.role).toEqual('button'); expect(myDir !.direction).toEqual('rtl'); }); it('should process attributes and outputs properly together', () => { /**
*/ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'div', ['role', 'button', 'myDir', '']); { listener('change', () => ctx.onChange()); } elementEnd(); } }, 1, 0, deps); const fixture = new ComponentFixture(App); let counter = 0; fixture.component.onChange = () => counter++; fixture.update(); expect(fixture.html).toEqual(`
`); expect(myDir !.role).toEqual('button'); myDir !.changeStream.next(); expect(counter).toEqual(1); }); it('should process attributes properly for directives with later indices', () => { /** *
*
*/ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'div', ['role', 'button', 'dir', 'rtl', 'myDir', '']); element(1, 'div', ['role', 'listbox', 'myDirB', '']); } }, 2, 0, deps); const fixture = new ComponentFixture(App); expect(fixture.html) .toEqual( `
`); expect(myDir !.role).toEqual('button'); expect(myDir !.direction).toEqual('rtl'); expect(dirB !.roleB).toEqual('listbox'); }); it('should support attributes at same index inside an if-else block', () => { /** *
// initialInputs: [['role', 'listbox']] * * % if (condition) { *
// initialInputs: [['role', 'button']] * % } else { *
// initialInputs: [null] * % } */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'div', ['role', 'listbox', 'myDir', '']); container(1); } if (rf & RenderFlags.Update) { containerRefreshStart(1); { if (ctx.condition) { let rf1 = embeddedViewStart(0, 1, 0); if (rf1 & RenderFlags.Create) { element(0, 'div', ['role', 'button', 'myDirB', '']); } embeddedViewEnd(); } else { let rf2 = embeddedViewStart(1, 1, 0); if (rf2 & RenderFlags.Create) { element(0, 'div', ['role', 'menu']); } embeddedViewEnd(); } } containerRefreshEnd(); } }, 2, 0, deps); const fixture = new ComponentFixture(App); fixture.component.condition = true; fixture.update(); expect(fixture.html) .toEqual(`
`); expect(myDir !.role).toEqual('listbox'); expect(dirB !.roleB).toEqual('button'); expect((dirB !as any).role).toBeUndefined(); fixture.component.condition = false; fixture.update(); expect(fixture.html).toEqual(`
`); expect(myDir !.role).toEqual('listbox'); }); it('should process attributes properly inside a for loop', () => { class Comp { static ngComponentDef = defineComponent({ type: Comp, selectors: [['comp']], consts: 3, vars: 1, /**
{{ dir.role }} */ template: function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'div', ['role', 'button', 'myDir', ''], ['dir', 'myDir']); text(2); } if (rf & RenderFlags.Update) { const tmp = reference(1) as any; textBinding(2, bind(tmp.role)); } }, factory: () => new Comp(), directives: () => [MyDir] }); } /** * % for (let i = 0; i < 3; i++) { * * % } */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { container(0); } if (rf & RenderFlags.Update) { containerRefreshStart(0); { for (let i = 0; i < 2; i++) { let rf1 = embeddedViewStart(0, 1, 0); if (rf1 & RenderFlags.Create) { element(0, 'comp'); } embeddedViewEnd(); } } containerRefreshEnd(); } }, 1, 0, [Comp]); const fixture = new ComponentFixture(App); expect(fixture.html) .toEqual( `
button
button
`); }); }); });