diff --git a/packages/core/test/acceptance/host_binding_spec.ts b/packages/core/test/acceptance/host_binding_spec.ts index 3d9137033d..cb61683447 100644 --- a/packages/core/test/acceptance/host_binding_spec.ts +++ b/packages/core/test/acceptance/host_binding_spec.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ComponentFactoryResolver, ComponentRef, Directive, HostBinding, Input, NgModule, ViewChild, ViewContainerRef} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {AfterContentInit, Component, ComponentFactoryResolver, ComponentRef, ContentChildren, Directive, DoCheck, HostBinding, HostListener, Injectable, Input, NgModule, OnChanges, OnInit, QueryList, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; +import {bypassSanitizationTrustHtml, bypassSanitizationTrustUrl} from '@angular/core/src/sanitization/bypass'; import {TestBed} from '@angular/core/testing'; import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; @@ -296,4 +298,774 @@ describe('host bindings', () => { }); }); + + @Directive({selector: '[hostBindingDir]'}) + class HostBindingDir { + @HostBinding() + id = 'foo'; + } + + it('should support host bindings in directives', () => { + @Directive({selector: '[dir]'}) + class Dir { + @HostBinding('className') + klass = 'foo'; + } + + @Component({template: ''}) + class App { + @ViewChild(Dir) directiveInstance !: Dir; + } + + TestBed.configureTestingModule({declarations: [App, Dir]}); + const fixture = TestBed.createComponent(App); + const element = fixture.nativeElement; + fixture.detectChanges(); + + expect(element.innerHTML).toContain('class="foo"'); + + fixture.componentInstance.directiveInstance.klass = 'bar'; + fixture.detectChanges(); + + expect(element.innerHTML).toContain('class="bar"'); + }); + + + it('should support host bindings on root component', () => { + @Component({template: ''}) + class HostBindingComp { + @HostBinding() + title = 'my-title'; + } + + TestBed.configureTestingModule({declarations: [HostBindingComp]}); + const fixture = TestBed.createComponent(HostBindingComp); + const element = fixture.nativeElement; + fixture.detectChanges(); + + expect(element.title).toBe('my-title'); + + fixture.componentInstance.title = 'other-title'; + fixture.detectChanges(); + + expect(element.title).toBe('other-title'); + }); + + it('should support host bindings on nodes with providers', () => { + @Injectable() + class ServiceOne { + value = 'one'; + } + + @Injectable() + class ServiceTwo { + value = 'two'; + } + + @Component({template: '', providers: [ServiceOne, ServiceTwo]}) + class App { + constructor(public serviceOne: ServiceOne, public serviceTwo: ServiceTwo) {} + + @HostBinding() + title = 'my-title'; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + const element = fixture.nativeElement; + fixture.detectChanges(); + + expect(element.title).toBe('my-title'); + expect(fixture.componentInstance.serviceOne.value).toEqual('one'); + expect(fixture.componentInstance.serviceTwo.value).toEqual('two'); + + fixture.componentInstance.title = 'other-title'; + fixture.detectChanges(); + expect(element.title).toBe('other-title'); + }); + + it('should support host bindings on multiple nodes', () => { + @Directive({selector: '[someDir]'}) + class SomeDir { + } + + @Component({selector: 'host-title-comp', template: ''}) + class HostTitleComp { + @HostBinding() + title = 'my-title'; + } + + @Component({ + template: ` +
+
+ + ` + }) + class App { + @ViewChild(HostBindingDir) hostBindingDir !: HostBindingDir; + } + + TestBed.configureTestingModule({declarations: [App, SomeDir, HostTitleComp, HostBindingDir]}); + const fixture = TestBed.createComponent(App); + const element = fixture.nativeElement; + fixture.detectChanges(); + + const hostBindingDiv = element.querySelector('div') as HTMLElement; + const hostTitleComp = element.querySelector('host-title-comp') as HTMLElement; + expect(hostBindingDiv.id).toEqual('foo'); + expect(hostTitleComp.title).toEqual('my-title'); + + fixture.componentInstance.hostBindingDir !.id = 'bar'; + fixture.detectChanges(); + expect(hostBindingDiv.id).toEqual('bar'); + }); + + it('should support consecutive components with host bindings', () => { + @Component({selector: 'host-binding-comp', template: ''}) + class HostBindingComp { + @HostBinding() + id = 'blue'; + } + + @Component({ + template: ` + + + ` + }) + class App { + @ViewChildren(HostBindingComp) hostBindingComp !: QueryList; + } + + TestBed.configureTestingModule({declarations: [App, HostBindingComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const comps = fixture.componentInstance.hostBindingComp.toArray(); + + const hostBindingEls = + fixture.nativeElement.querySelectorAll('host-binding-comp') as NodeListOf; + + expect(hostBindingEls.length).toBe(2); + + comps[0].id = 'red'; + fixture.detectChanges(); + expect(hostBindingEls[0].id).toBe('red'); + + // second element should not be affected + expect(hostBindingEls[1].id).toBe('blue'); + + comps[1].id = 'red'; + fixture.detectChanges(); + + // now second element should take updated value + expect(hostBindingEls[1].id).toBe('red'); + }); + + + it('should support dirs with host bindings on the same node as dirs without host bindings', + () => { + @Directive({selector: '[someDir]'}) + class SomeDir { + } + + @Component({template: '
'}) + class App { + @ViewChild(HostBindingDir) hostBindingDir !: HostBindingDir; + } + + TestBed.configureTestingModule({declarations: [App, SomeDir, HostBindingDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const hostBindingDiv = fixture.nativeElement.querySelector('div') as HTMLElement; + expect(hostBindingDiv.id).toEqual('foo'); + + fixture.componentInstance.hostBindingDir !.id = 'bar'; + fixture.detectChanges(); + expect(hostBindingDiv.id).toEqual('bar'); + }); + + + + it('should support host bindings that rely on values from init hooks', () => { + @Component({template: '', selector: 'init-hook-comp'}) + class InitHookComp implements OnInit, OnChanges, DoCheck { + @Input() + inputValue = ''; + + changesValue = ''; + initValue = ''; + checkValue = ''; + + ngOnChanges() { this.changesValue = 'changes'; } + + ngOnInit() { this.initValue = 'init'; } + + ngDoCheck() { this.checkValue = 'check'; } + + @HostBinding('title') + get value() { + return `${this.inputValue}-${this.changesValue}-${this.initValue}-${this.checkValue}`; + } + } + + @Component({template: ''}) + class App { + value = 'input'; + } + + TestBed.configureTestingModule({declarations: [App, InitHookComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const initHookComp = fixture.nativeElement.querySelector('init-hook-comp') as HTMLElement; + expect(initHookComp.title).toEqual('input-changes-init-check'); + + fixture.componentInstance.value = 'input2'; + fixture.detectChanges(); + expect(initHookComp.title).toEqual('input2-changes-init-check'); + }); + + it('should support host bindings with the same name as inputs', () => { + @Directive({selector: '[hostBindingDir]'}) + class HostBindingInputDir { + @Input() + disabled = false; + + @HostBinding('disabled') + hostDisabled = false; + } + + @Component({template: ''}) + class App { + @ViewChild(HostBindingInputDir) hostBindingInputDir !: HostBindingInputDir; + isDisabled = true; + } + + TestBed.configureTestingModule({declarations: [App, HostBindingInputDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const hostBindingInputDir = fixture.componentInstance.hostBindingInputDir; + + const hostBindingEl = fixture.nativeElement.querySelector('input') as HTMLInputElement; + expect(hostBindingInputDir.disabled).toBe(true); + expect(hostBindingEl.disabled).toBe(false); + + fixture.componentInstance.isDisabled = false; + fixture.detectChanges(); + expect(hostBindingInputDir.disabled).toBe(false); + expect(hostBindingEl.disabled).toBe(false); + + hostBindingInputDir.hostDisabled = true; + fixture.detectChanges(); + expect(hostBindingInputDir.disabled).toBe(false); + expect(hostBindingEl.disabled).toBe(true); + }); + + it('should support host bindings on second template pass', () => { + @Component({selector: 'parent', template: '
'}) + class Parent { + } + + @Component({ + template: ` + + + ` + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Parent, HostBindingDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const divs = fixture.nativeElement.querySelectorAll('div'); + expect(divs[0].id).toEqual('foo'); + expect(divs[1].id).toEqual('foo'); + }); + + it('should support host bindings in for loop', () => { + @Component({ + template: ` +
+

+
+ ` + }) + class App { + rows: number[] = []; + } + + TestBed.configureTestingModule({imports: [CommonModule], declarations: [App, HostBindingDir]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.rows = [1, 2, 3]; + fixture.detectChanges(); + + const paragraphs = fixture.nativeElement.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', () => { + @Component({selector: 'host-binding-comp', template: ''}) + class HostBindingComp { + @HostBinding() + id = 'my-id'; + } + + @Component({selector: 'name-comp', template: ''}) + class NameComp { + @Input() + names !: string[]; + } + + @Component({ + template: ` + + + ` + }) + class App { + @ViewChild(NameComp) nameComp !: NameComp; + name = ''; + } + + TestBed.configureTestingModule({declarations: [App, HostBindingComp, NameComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const nameComp = fixture.componentInstance.nameComp; + const hostBindingEl = fixture.nativeElement.querySelector('host-binding-comp') as HTMLElement; + fixture.componentInstance.name = 'Betty'; + fixture.detectChanges(); + expect(hostBindingEl.id).toBe('my-id'); + expect(nameComp.names).toEqual(['Nancy', 'Betty', 'Ned']); + + const firstArray = nameComp.names; + fixture.detectChanges(); + expect(firstArray).toBe(nameComp.names); + + fixture.componentInstance.name = 'my-id'; + fixture.detectChanges(); + 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', () => { + @Component({selector: 'name-comp', template: ''}) + class NameComp { + @Input() + names !: string[]; + } + + @Component({ + selector: 'host-binding-comp', + host: {'[id]': `['red', id]`, '[dir]': `dir`, '[title]': `[title, otherTitle]`}, + template: '' + }) + class HostBindingComp { + id = 'blue'; + dir = 'ltr'; + title = 'my title'; + otherTitle = 'other title'; + } + + @Component({ + template: ` + + + ` + }) + class App { + @ViewChild(HostBindingComp) hostBindingComp !: HostBindingComp; + @ViewChild(NameComp) nameComp !: NameComp; + name = ''; + otherName = ''; + } + + TestBed.configureTestingModule({declarations: [App, HostBindingComp, NameComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const {nameComp, hostBindingComp} = fixture.componentInstance; + + fixture.componentInstance.name = 'Frank'; + fixture.componentInstance.otherName = 'Joe'; + fixture.detectChanges(); + + const hostBindingEl = fixture.nativeElement.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.detectChanges(); + expect(firstArray).toBe(nameComp !.names); + + hostBindingComp.id = 'green'; + hostBindingComp.dir = 'rtl'; + hostBindingComp.title = 'TITLE'; + fixture.detectChanges(); + expect(hostBindingEl.id).toBe('red,green'); + expect(hostBindingEl.dir).toBe('rtl'); + expect(hostBindingEl.title).toBe('TITLE,other title'); + }); + + it('should support directives with and without allocHostVars on the same component', () => { + let events: string[] = []; + + @Directive({selector: '[hostDir]', host: {'[title]': `[title, 'other title']`}}) + class HostBindingDir { + title = 'my title'; + } + + @Directive({selector: '[hostListenerDir]'}) + class HostListenerDir { + @HostListener('click') + onClick() { events.push('click!'); } + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, HostBindingDir, HostListenerDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const button = fixture.nativeElement.querySelector('button') !; + button.click(); + expect(events).toEqual(['click!']); + expect(button.title).toEqual('my title,other title'); + }); + + it('should support host bindings with literals from multiple directives', () => { + @Component({selector: 'host-binding-comp', host: {'[id]': `['red', id]`}, template: ''}) + class HostBindingComp { + id = 'blue'; + } + + @Directive({selector: '[hostDir]', host: {'[title]': `[title, 'other title']`}}) + class HostBindingDir { + title = 'my title'; + } + + @Component({template: ''}) + class App { + @ViewChild(HostBindingComp) hostBindingComp !: HostBindingComp; + @ViewChild(HostBindingDir) hostBindingDir !: HostBindingDir; + } + + TestBed.configureTestingModule({declarations: [App, HostBindingComp, HostBindingDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const hostElement = fixture.nativeElement.querySelector('host-binding-comp') as HTMLElement; + expect(hostElement.id).toBe('red,blue'); + expect(hostElement.title).toBe('my title,other title'); + + fixture.componentInstance.hostBindingDir.title = 'blue'; + fixture.detectChanges(); + expect(hostElement.title).toBe('blue,other title'); + + fixture.componentInstance.hostBindingComp.id = 'green'; + fixture.detectChanges(); + expect(hostElement.id).toBe('red,green'); + }); + + it('should support ternary expressions in host bindings', () => { + @Component({ + selector: 'host-binding-comp', + template: '', + host: { + // Use `attr` since IE doesn't support the `title` property on all elements. + '[attr.id]': `condition ? ['red', id] : 'green'`, + '[attr.title]': `otherCondition ? [title] : 'other title'` + } + }) + class HostBindingComp { + condition = true; + otherCondition = true; + id = 'blue'; + title = 'blue'; + } + + @Component({template: `{{ name }}`}) + class App { + @ViewChild(HostBindingComp) hostBindingComp !: HostBindingComp; + name = ''; + } + + TestBed.configureTestingModule({declarations: [App, HostBindingComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const hostElement = fixture.nativeElement.querySelector('host-binding-comp') as HTMLElement; + fixture.componentInstance.name = 'Ned'; + fixture.detectChanges(); + + // Note that we assert for each binding individually, rather than checking against + // innerHTML, because IE10 changes the attribute order and makes it inconsistent with + // all other browsers. + expect(hostElement.id).toBe('red,blue'); + expect(hostElement.title).toBe('blue'); + expect(fixture.nativeElement.innerHTML.endsWith('Ned')).toBe(true); + + fixture.componentInstance.hostBindingComp.condition = false; + fixture.componentInstance.hostBindingComp.title = 'TITLE'; + fixture.detectChanges(); + expect(hostElement.id).toBe('green'); + expect(hostElement.title).toBe('TITLE'); + + fixture.componentInstance.hostBindingComp.otherCondition = false; + fixture.detectChanges(); + expect(hostElement.id).toBe('green'); + expect(hostElement.title).toBe('other title'); + }); + + onlyInIvy('Host bindings do not get merged in ViewEngine') + .it('should work correctly with inherited directives with hostBindings', () => { + @Directive({selector: '[superDir]', host: {'[id]': 'id'}}) + class SuperDirective { + id = 'my-id'; + } + + @Directive({selector: '[subDir]', host: {'[title]': 'title'}}) + class SubDirective extends SuperDirective { + title = 'my-title'; + } + + @Component({ + template: ` +
+
+ ` + }) + class App { + @ViewChild(SubDirective) subDir !: SubDirective; + @ViewChild(SuperDirective) superDir !: SuperDirective; + } + + TestBed.configureTestingModule({declarations: [App, SuperDirective, SubDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const els = fixture.nativeElement.querySelectorAll('div') as NodeListOf; + + const firstDivEl = els[0]; + const secondDivEl = els[1]; + + // checking first div element with inherited directive + expect(firstDivEl.id).toEqual('my-id'); + expect(firstDivEl.title).toEqual('my-title'); + + fixture.componentInstance.subDir.title = 'new-title'; + fixture.detectChanges(); + expect(firstDivEl.id).toEqual('my-id'); + expect(firstDivEl.title).toEqual('new-title'); + + fixture.componentInstance.subDir.id = 'new-id'; + fixture.detectChanges(); + expect(firstDivEl.id).toEqual('new-id'); + expect(firstDivEl.title).toEqual('new-title'); + + // checking second div element with simple directive + expect(secondDivEl.id).toEqual('my-id'); + + fixture.componentInstance.superDir.id = 'new-id'; + fixture.detectChanges(); + expect(secondDivEl.id).toEqual('new-id'); + }); + + it('should support host attributes', () => { + @Directive({selector: '[hostAttributeDir]', host: {'role': 'listbox'}}) + class HostAttributeDir { + } + + @Component({template: '
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, HostAttributeDir]}); + const fixture = TestBed.createComponent(App); + expect(fixture.nativeElement.innerHTML).toContain(`role="listbox"`); + }); + + it('should support content children in host bindings', () => { + @Component({ + selector: 'host-binding-comp', + template: '', + host: {'[id]': 'foos.length'} + }) + class HostBindingWithContentChildren { + @ContentChildren('foo') + foos !: QueryList; + } + + @Component({ + template: ` + +
+
+
+ ` + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App, HostBindingWithContentChildren]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const hostBindingEl = fixture.nativeElement.querySelector('host-binding-comp') as HTMLElement; + expect(hostBindingEl.id).toEqual('2'); + }); + + it('should support host bindings dependent on content hooks', () => { + @Component({selector: 'host-binding-comp', template: '', host: {'[id]': 'myValue'}}) + class HostBindingWithContentHooks implements AfterContentInit { + myValue = 'initial'; + + ngAfterContentInit() { this.myValue = 'after-content'; } + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, HostBindingWithContentHooks]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const hostBindingEl = fixture.nativeElement.querySelector('host-binding-comp') as HTMLElement; + expect(hostBindingEl.id).toEqual('after-content'); + }); + + describe('styles', () => { + + it('should bind to host styles', () => { + @Component( + {selector: 'host-binding-to-styles', host: {'[style.width.px]': 'width'}, template: ''}) + class HostBindingToStyles { + width = 2; + } + + @Component({template: ''}) + class App { + @ViewChild(HostBindingToStyles) hostBindingDir !: HostBindingToStyles; + } + + TestBed.configureTestingModule({declarations: [App, HostBindingToStyles]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const hostBindingEl = + fixture.nativeElement.querySelector('host-binding-to-styles') as HTMLElement; + expect(hostBindingEl.style.width).toEqual('2px'); + + fixture.componentInstance.hostBindingDir.width = 5; + fixture.detectChanges(); + expect(hostBindingEl.style.width).toEqual('5px'); + }); + + it('should bind to host styles on containers', () => { + @Directive({selector: '[hostStyles]', host: {'[style.width.px]': 'width'}}) + class HostBindingToStyles { + width = 2; + } + + @Directive({selector: '[containerDir]'}) + class ContainerDir { + constructor(public vcr: ViewContainerRef) {} + } + + @Component({template: '
'}) + class App { + @ViewChild(HostBindingToStyles) hostBindingDir !: HostBindingToStyles; + } + + TestBed.configureTestingModule({declarations: [App, HostBindingToStyles, ContainerDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const hostBindingEl = fixture.nativeElement.querySelector('div') as HTMLElement; + expect(hostBindingEl.style.width).toEqual('2px'); + + fixture.componentInstance.hostBindingDir.width = 5; + fixture.detectChanges(); + expect(hostBindingEl.style.width).toEqual('5px'); + }); + + it('should apply static host classes', () => { + @Component({selector: 'static-host-class', host: {'class': 'mat-toolbar'}, template: ''}) + class StaticHostClass { + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, StaticHostClass]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const hostBindingEl = fixture.nativeElement.querySelector('static-host-class') as HTMLElement; + expect(hostBindingEl.className).toEqual('mat-toolbar'); + }); + }); + + describe('sanitization', () => { + function verify( + tag: string, prop: string, value: any, expectedSanitizedValue: any, bypassFn: any, + isAttribute: boolean = true) { + it('should sanitize potentially unsafe properties and attributes', () => { + @Directive({ + selector: '[unsafeUrlHostBindingDir]', + host: { + [`[${isAttribute ? 'attr.' : ''}${prop}]`]: 'value', + } + }) + class UnsafeDir { + value: any = value; + } + + @Component({template: `<${tag} unsafeUrlHostBindingDir>`}) + class App { + @ViewChild(UnsafeDir) unsafeDir !: UnsafeDir; + } + + TestBed.configureTestingModule({declarations: [App, UnsafeDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const el = fixture.nativeElement.querySelector(tag) !; + const current = () => isAttribute ? el.getAttribute(prop) : (el as any)[prop]; + + fixture.componentInstance.unsafeDir.value = value; + fixture.detectChanges(); + expect(current()).toEqual(expectedSanitizedValue); + + fixture.componentInstance.unsafeDir.value = bypassFn(value); + fixture.detectChanges(); + expect(current()).toEqual(expectedSanitizedValue); + }); + } + + verify( + 'a', 'href', 'javascript:alert(1)', 'unsafe:javascript:alert(1)', + bypassSanitizationTrustUrl); + verify( + 'blockquote', 'cite', 'javascript:alert(2)', 'unsafe:javascript:alert(2)', + bypassSanitizationTrustUrl); + verify( + 'b', 'innerHTML', '', + '', bypassSanitizationTrustHtml, + /* isAttribute */ false); + }); + }); diff --git a/packages/core/test/render3/host_binding_spec.ts b/packages/core/test/render3/host_binding_spec.ts deleted file mode 100644 index 7830beb205..0000000000 --- a/packages/core/test/render3/host_binding_spec.ts +++ /dev/null @@ -1,1286 +0,0 @@ -/** - * @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 {ElementRef, QueryList, ViewContainerRef} from '@angular/core'; - -import {AttributeMarker, ɵɵInheritDefinitionFeature, ɵɵNgOnChangesFeature, ɵɵProvidersFeature, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵtemplate} from '../../src/render3/index'; -import {ɵɵallocHostVars, ɵɵbind, ɵɵdirectiveInject, ɵɵelement, ɵɵelementAttribute, ɵɵelementEnd, ɵɵelementHostAttrs, ɵɵelementHostStyleProp, ɵɵelementHostStyling, ɵɵelementHostStylingApply, ɵɵelementProperty, ɵɵelementStart, ɵɵelementStyleProp, ɵɵelementStyling, ɵɵelementStylingApply, ɵɵlistener, ɵɵload, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {ɵɵpureFunction1, ɵɵpureFunction2} from '../../src/render3/pure_function'; -import {ɵɵcontentQuery, ɵɵloadContentQuery, ɵɵqueryRefresh} from '../../src/render3/query'; -import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustUrl} from '../../src/sanitization/bypass'; -import {ɵɵsanitizeHtml, ɵɵsanitizeUrl, ɵɵsanitizeUrlOrResourceUrl} from '../../src/sanitization/sanitization'; - -import {NgForOf} from './common_with_def'; -import {ComponentFixture, TemplateFixture, createComponent, createDirective} from './render_util'; - -describe('host bindings', () => { - let nameComp: NameComp|null; - let hostBindingDir: HostBindingDir|null; - - beforeEach(() => { - nameComp = null; - nameComp = null; - hostBindingDir = null; - }); - - 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'} - }); - } - - class HostBindingDir { - // @HostBinding() - id = 'foo'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: HostBindingDir, - selectors: [['', 'hostBindingDir', '']], - factory: () => hostBindingDir = new HostBindingDir(), - hostBindings: (rf: RenderFlags, ctx: any, elementIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elementIndex, 'id', ɵɵbind(ctx.id), null, true); - } - } - }); - } - - class HostBindingComp { - // @HostBinding() - id = 'my-id'; - - static ngComponentDef = ɵɵdefineComponent({ - type: HostBindingComp, - selectors: [['host-binding-comp']], - factory: () => new HostBindingComp(), - consts: 0, - vars: 0, - hostBindings: (rf: RenderFlags, ctx: HostBindingComp, elIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'id', ɵɵbind(ctx.id), null, true); - } - }, - template: (rf: RenderFlags, ctx: HostBindingComp) => {} - }); - } - - it('should support host bindings in directives', () => { - let directiveInstance: Directive|undefined; - const elementIndices: number[] = []; - class Directive { - // @HostBinding('className') - klass = 'foo'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => directiveInstance = new Directive, - hostBindings: (rf: RenderFlags, ctx: any, elementIndex: number) => { - elementIndices.push(elementIndex); - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elementIndex, 'className', ɵɵbind(ctx.klass), null, true); - } - } - }); - } - - function Template() { ɵɵelement(0, 'span', [AttributeMarker.Bindings, 'dir']); } - - const fixture = new TemplateFixture(Template, () => {}, 1, 0, [Directive]); - expect(fixture.html).toEqual(''); - - directiveInstance !.klass = 'bar'; - fixture.update(); - expect(fixture.html).toEqual(''); - - // verify that we always call `hostBindings` function with the same element index - expect(elementIndices.every(id => id === elementIndices[0])).toBeTruthy(); - }); - - it('should support host bindings on root component', () => { - const elementIndices: number[] = []; - - class HostBindingComp { - // @HostBinding() - id = 'my-id'; - - static ngComponentDef = ɵɵdefineComponent({ - type: HostBindingComp, - selectors: [['host-binding-comp']], - factory: () => new HostBindingComp(), - consts: 0, - vars: 0, - hostBindings: (rf: RenderFlags, ctx: HostBindingComp, elIndex: number) => { - elementIndices.push(elIndex); - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'id', ɵɵbind(ctx.id), null, true); - } - }, - 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'); - - // verify that we always call `hostBindings` function with the same element index - expect(elementIndices.every(id => id === elementIndices[0])).toBeTruthy(); - }); - - 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, - hostBindings: (rf: RenderFlags, ctx: CompWithProviders, elIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'id', ɵɵbind(ctx.id), null, true); - } - }, - 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', () => { - const SomeDir = createDirective('someDir'); - - class HostTitleComp { - // @HostBinding() - title = 'my-title'; - - static ngComponentDef = ɵɵdefineComponent({ - type: HostTitleComp, - selectors: [['host-title-comp']], - factory: () => new HostTitleComp(), - consts: 0, - vars: 0, - hostBindings: (rf: RenderFlags, ctx: HostTitleComp, elIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'title', ɵɵbind(ctx.title), null, true); - } - }, - template: (rf: RenderFlags, ctx: HostTitleComp) => {} - }); - } - - /** - *
- *
- * - */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['hostBindingDir', '']); - ɵɵelement(1, 'div', ['someDir', '']); - ɵɵelement(2, 'host-title-comp'); - } - }, 3, 0, [HostBindingDir, SomeDir, HostTitleComp]); - - const fixture = new ComponentFixture(App); - const hostBindingDiv = fixture.hostElement.querySelector('div') as HTMLElement; - const hostTitleComp = fixture.hostElement.querySelector('host-title-comp') as HTMLElement; - expect(hostBindingDiv.id).toEqual('foo'); - expect(hostTitleComp.title).toEqual('my-title'); - - hostBindingDir !.id = 'bar'; - fixture.update(); - expect(hostBindingDiv.id).toEqual('bar'); - }); - - it('should support consecutive components with host bindings', () => { - let comps: HostBindingComp[] = []; - - class HostBindingComp { - // @HostBinding() - id = 'blue'; - - static ngComponentDef = ɵɵdefineComponent({ - type: HostBindingComp, - selectors: [['host-binding-comp']], - factory: () => { - const comp = new HostBindingComp(); - comps.push(comp); - return comp; - }, - consts: 0, - vars: 0, - hostBindings: (rf: RenderFlags, ctx: HostBindingComp, elIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'id', ɵɵbind(ctx.id), null, true); - } - }, - template: (rf: RenderFlags, ctx: HostBindingComp) => {} - }); - } - - /** - * - * - * */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'host-binding-comp'); - ɵɵelement(1, 'host-binding-comp'); - } - }, 2, 0, [HostBindingComp]); - - const fixture = new ComponentFixture(App); - const hostBindingEls = - fixture.hostElement.querySelectorAll('host-binding-comp') as NodeListOf; - - expect(hostBindingEls.length).toBe(2); - - comps[0].id = 'red'; - fixture.update(); - expect(hostBindingEls[0].id).toBe('red'); - - // second element should not be affected - expect(hostBindingEls[1].id).toBe('blue'); - - comps[1].id = 'red'; - fixture.update(); - - // now second element should take updated value - expect(hostBindingEls[1].id).toBe('red'); - }); - - it('should support dirs with host bindings on the same node as dirs without host bindings', - () => { - const SomeDir = createDirective('someDir'); - - /**
*/ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['someDir', '', 'hostBindingDir', '']); - } - }, 1, 0, [SomeDir, HostBindingDir]); - - const fixture = new ComponentFixture(App); - const hostBindingDiv = fixture.hostElement.querySelector('div') as HTMLElement; - expect(hostBindingDiv.id).toEqual('foo'); - - hostBindingDir !.id = 'bar'; - fixture.update(); - expect(hostBindingDiv.id).toEqual('bar'); - }); - - it('should support host bindings that rely on values from init hooks', () => { - class InitHookComp { - // @Input() - inputValue = ''; - - changesValue = ''; - initValue = ''; - checkValue = ''; - - ngOnChanges() { this.changesValue = 'changes'; } - - ngOnInit() { this.initValue = 'init'; } - - ngDoCheck() { this.checkValue = 'check'; } - - get value() { - return `${this.inputValue}-${this.changesValue}-${this.initValue}-${this.checkValue}`; - } - - static ngComponentDef = ɵɵdefineComponent({ - type: InitHookComp, - selectors: [['init-hook-comp']], - factory: () => new InitHookComp(), - template: (rf: RenderFlags, ctx: InitHookComp) => {}, - consts: 0, - vars: 0, - features: [ɵɵNgOnChangesFeature()], - hostBindings: (rf: RenderFlags, ctx: InitHookComp, elIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'title', ɵɵbind(ctx.value), null, true); - } - }, - inputs: {inputValue: 'inputValue'} - }); - } - - /** */ - class App { - value = 'input'; - - static ngComponentDef = ɵɵdefineComponent({ - type: App, - selectors: [['app']], - factory: () => new App(), - template: (rf: RenderFlags, ctx: App) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'init-hook-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'inputValue', ɵɵbind(ctx.value)); - } - }, - consts: 1, - vars: 1, - directives: [InitHookComp] - }); - } - - const fixture = new ComponentFixture(App); - const initHookComp = fixture.hostElement.querySelector('init-hook-comp') as HTMLElement; - expect(initHookComp.title).toEqual('input-changes-init-check'); - - fixture.component.value = 'input2'; - fixture.update(); - expect(initHookComp.title).toEqual('input2-changes-init-check'); - }); - - it('should support host bindings with the same name as inputs', () => { - let hostBindingInputDir !: HostBindingInputDir; - - class HostBindingInputDir { - // @Input() - disabled = false; - - // @HostBinding('disabled') - hostDisabled = false; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: HostBindingInputDir, - selectors: [['', 'hostBindingDir', '']], - factory: () => hostBindingInputDir = new HostBindingInputDir(), - hostBindings: (rf: RenderFlags, ctx: HostBindingInputDir, elIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'disabled', ɵɵbind(ctx.hostDisabled), null, true); - } - }, - inputs: {disabled: 'disabled'} - }); - } - - /** */ - class App { - isDisabled = true; - - static ngComponentDef = ɵɵdefineComponent({ - type: App, - selectors: [['app']], - factory: () => new App(), - template: (rf: RenderFlags, ctx: App) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'input', ['hostBindingDir', '']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'disabled', ɵɵbind(ctx.isDisabled)); - } - }, - consts: 1, - vars: 1, - directives: [HostBindingInputDir] - }); - } - - const fixture = new ComponentFixture(App); - const hostBindingEl = fixture.hostElement.querySelector('input') as HTMLInputElement; - expect(hostBindingInputDir.disabled).toBe(true); - expect(hostBindingEl.disabled).toBe(false); - - fixture.component.isDisabled = false; - fixture.update(); - expect(hostBindingInputDir.disabled).toBe(false); - expect(hostBindingEl.disabled).toBe(false); - - hostBindingInputDir.hostDisabled = true; - fixture.update(); - expect(hostBindingInputDir.disabled).toBe(false); - expect(hostBindingEl.disabled).toBe(true); - }); - - it('should support host bindings on second template pass', () => { - /**
*/ - 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', () => { - 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, 'div', [AttributeMarker.Template, 'ngFor', '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']; - - /** - * - * - */ - 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, - hostBindings: (rf: RenderFlags, ctx: HostBindingComp, elIndex: number) => { - // LView: [..., id, dir, title, ctx.id, pf1, ctx.title, ctx.otherTitle, pf2] - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(8); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'id', ɵɵbind(ɵɵpureFunction1(3, ff, ctx.id)), null, true); - ɵɵelementProperty(elIndex, 'dir', ɵɵbind(ctx.dir), null, true); - ɵɵelementProperty( - elIndex, 'title', ɵɵbind(ɵɵpureFunction2(5, ff2, ctx.title, ctx.otherTitle)), null, - true); - } - }, - 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, - hostBindings: (rf: RenderFlags, ctx: HostBindingComp, elIndex: number) => { - // LView: [..., id, ctx.id, pf1] - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(3); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'id', ɵɵbind(ɵɵpureFunction1(1, ff, ctx.id)), null, true); - } - }, - 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(), - hostBindings: (rf: RenderFlags, ctx: HostBindingDir, elIndex: number) => { - // LView: [..., title, ctx.title, pf1] - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(3); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty( - elIndex, 'title', ɵɵbind(ɵɵpureFunction1(1, ff1, ctx.title)), null, true); - } - } - }); - } - - /** - * - * - */ - 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 directives with and without allocHostVars on the same component', () => { - let events: string[] = []; - - 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: () => new HostBindingDir(), - hostBindings: (rf: RenderFlags, ctx: HostBindingDir, elIndex: number) => { - // LView [..., title, ctx.title, pf1] - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(3); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty( - elIndex, 'title', ɵɵbind(ɵɵpureFunction1(1, ff1, ctx.title)), null, true); - } - } - }); - } - - class HostListenerDir { - /* @HostListener('click') */ - onClick() { events.push('click!'); } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: HostListenerDir, - selectors: [['', 'hostListenerDir', '']], - factory: function HostListenerDir_Factory() { return new HostListenerDir(); }, - hostBindings: function HostListenerDir_HostBindings( - rf: RenderFlags, ctx: any, elIndex: number) { - if (rf & RenderFlags.Create) { - ɵɵlistener('click', function() { return ctx.onClick(); }); - } - } - }); - } - - // - const fixture = new TemplateFixture(() => { - ɵɵelementStart(0, 'button', ['hostListenerDir', '', 'hostDir', '']); - ɵɵtext(1, 'Click'); - ɵɵelementEnd(); - }, () => {}, 2, 0, [HostListenerDir, HostBindingDir]); - - const button = fixture.hostElement.querySelector('button') !; - button.click(); - expect(events).toEqual(['click!']); - expect(button.title).toEqual('my title,other title'); - }); - - 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, - hostBindings: (rf: RenderFlags, ctx: HostBindingComp, elIndex: number) => { - // LView: [..., id, title, ctx.id, pf1, ctx.title, pf1] - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(6); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty( - elIndex, 'id', ɵɵbind(ctx.condition ? ɵɵpureFunction1(2, ff, ctx.id) : 'green'), - null, true); - ɵɵelementProperty( - elIndex, 'title', - ɵɵbind(ctx.otherCondition ? ɵɵpureFunction1(4, ff1, ctx.title) : 'other title'), - null, true); - } - }, - 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'); - }); - - it('should work correctly with inherited directives with hostBindings', () => { - let subDir !: SubDirective; - let superDir !: SuperDirective; - - class SuperDirective { - id = 'my-id'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: SuperDirective, - selectors: [['', 'superDir', '']], - hostBindings: (rf: RenderFlags, ctx: SuperDirective, elementIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elementIndex, 'id', ɵɵbind(ctx.id), null, true); - } - }, - factory: () => superDir = new SuperDirective(), - }); - } - - class SubDirective extends SuperDirective { - title = 'my-title'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: SubDirective, - selectors: [['', 'subDir', '']], - hostBindings: (rf: RenderFlags, ctx: SubDirective, elementIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elementIndex, 'title', ɵɵbind(ctx.title), null, true); - } - }, - factory: () => subDir = new SubDirective(), - features: [ɵɵInheritDefinitionFeature] - }); - } - - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['subDir', '']); - ɵɵelement(1, 'div', ['superDir', '']); - } - }, 2, 0, [SubDirective, SuperDirective]); - - const fixture = new ComponentFixture(App); - const els = fixture.hostElement.querySelectorAll('div') as NodeListOf; - - const firstDivEl = els[0]; - const secondDivEl = els[1]; - - // checking first div element with inherited directive - expect(firstDivEl.id).toEqual('my-id'); - expect(firstDivEl.title).toEqual('my-title'); - - subDir.title = 'new-title'; - fixture.update(); - expect(firstDivEl.id).toEqual('my-id'); - expect(firstDivEl.title).toEqual('new-title'); - - subDir.id = 'new-id'; - fixture.update(); - expect(firstDivEl.id).toEqual('new-id'); - expect(firstDivEl.title).toEqual('new-title'); - - // checking second div element with simple directive - expect(secondDivEl.id).toEqual('my-id'); - - superDir.id = 'new-id'; - fixture.update(); - expect(secondDivEl.id).toEqual('new-id'); - }); - - it('should support host attributes', () => { - // host: { - // 'role': 'listbox' - // } - class HostAttributeDir { - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'hostAttributeDir', '']], - type: HostAttributeDir, - factory: () => new HostAttributeDir(), - hostBindings: function(rf, ctx, elIndex) { - if (rf & RenderFlags.Create) { - ɵɵelementHostAttrs(['role', 'listbox']); - } - } - }); - } - - //
- const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['hostAttributeDir', '']); - } - }, 1, 0, [HostAttributeDir]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(`
`); - }); - - it('should support content children in host bindings', () => { - /** - * host: { - * '[id]': 'foos.length' - * } - */ - class HostBindingWithContentChildren { - // @ContentChildren('foo') - foos !: QueryList; - - static ngComponentDef = ɵɵdefineComponent({ - type: HostBindingWithContentChildren, - selectors: [['host-binding-comp']], - factory: () => new HostBindingWithContentChildren(), - consts: 0, - vars: 0, - hostBindings: (rf: RenderFlags, ctx: HostBindingWithContentChildren, elIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'id', ɵɵbind(ctx.foos.length), null, true); - } - }, - contentQueries: (rf: RenderFlags, ctx: any, dirIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵcontentQuery(dirIndex, ['foo'], false, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadContentQuery()) && (ctx.foos = tmp); - } - }, - template: (rf: RenderFlags, cmp: HostBindingWithContentChildren) => {} - }); - } - - /** - * - *
- *
- *
- */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'host-binding-comp'); - { - ɵɵelement(1, 'div', null, ['foo', '']); - ɵɵelement(3, 'div', null, ['foo', '']); - } - ɵɵelementEnd(); - } - }, 5, 0, [HostBindingWithContentChildren]); - - const fixture = new ComponentFixture(App); - const hostBindingEl = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; - expect(hostBindingEl.id).toEqual('2'); - }); - - it('should support host bindings dependent on content hooks', () => { - /** - * host: { - * '[id]': 'myValue' - * } - */ - class HostBindingWithContentHooks { - myValue = 'initial'; - - ngAfterContentInit() { this.myValue = 'after-content'; } - - ngAfterViewInit() { this.myValue = 'after-view'; } - - static ngComponentDef = ɵɵdefineComponent({ - type: HostBindingWithContentHooks, - selectors: [['host-binding-comp']], - factory: () => new HostBindingWithContentHooks(), - consts: 0, - vars: 0, - hostBindings: (rf: RenderFlags, ctx: HostBindingWithContentHooks, elIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'id', ɵɵbind(ctx.myValue), null, true); - } - }, - template: (rf: RenderFlags, cmp: HostBindingWithContentHooks) => {} - }); - } - - /** */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'host-binding-comp'); - } - }, 1, 0, [HostBindingWithContentHooks]); - - const fixture = new ComponentFixture(App); - const hostBindingEl = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; - expect(hostBindingEl.id).toEqual('after-content'); - }); - - describe('styles', () => { - - it('should bind to host styles', () => { - let hostBindingDir !: HostBindingToStyles; - /** - * host: { - * '[style.width.px]': 'width' - * } - */ - class HostBindingToStyles { - width = 2; - - static ngComponentDef = ɵɵdefineComponent({ - type: HostBindingToStyles, - selectors: [['host-binding-to-styles']], - factory: () => hostBindingDir = new HostBindingToStyles(), - consts: 0, - vars: 0, - hostBindings: (rf: RenderFlags, ctx: HostBindingToStyles, elIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵelementHostStyling(null, ['width']); - } - if (rf & RenderFlags.Update) { - ɵɵelementHostStyleProp(0, ctx.width, 'px'); - ɵɵelementHostStylingApply(); - } - }, - template: (rf: RenderFlags, cmp: HostBindingToStyles) => {} - }); - } - - /** */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'host-binding-to-styles'); - } - }, 1, 0, [HostBindingToStyles]); - - const fixture = new ComponentFixture(App); - const hostBindingEl = - fixture.hostElement.querySelector('host-binding-to-styles') as HTMLElement; - expect(hostBindingEl.style.width).toEqual('2px'); - - hostBindingDir.width = 5; - fixture.update(); - expect(hostBindingEl.style.width).toEqual('5px'); - }); - - it('should bind to host styles on containers', () => { - let hostBindingDir !: HostBindingToStyles; - /** - * host: { - * '[style.width.px]': 'width' - * } - */ - class HostBindingToStyles { - width = 2; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: HostBindingToStyles, - selectors: [['', 'hostStyles', '']], - factory: () => hostBindingDir = new HostBindingToStyles(), - hostBindings: (rf: RenderFlags, ctx: HostBindingToStyles, elIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵelementHostStyling(null, ['width']); - } - if (rf & RenderFlags.Update) { - ɵɵelementHostStyleProp(0, ctx.width, 'px'); - ɵɵelementHostStylingApply(); - } - } - }); - } - - class ContainerDir { - constructor(public vcr: ViewContainerRef) {} - - static ngDirectiveDef = ɵɵdefineDirective({ - type: ContainerDir, - selectors: [['', 'containerDir', '']], - factory: () => new ContainerDir(ɵɵdirectiveInject(ViewContainerRef as any)), - }); - } - - /**
*/ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['containerDir', '', 'hostStyles', '']); - } - }, 1, 0, [ContainerDir, HostBindingToStyles]); - - const fixture = new ComponentFixture(App); - const hostBindingEl = fixture.hostElement.querySelector('div') as HTMLElement; - expect(hostBindingEl.style.width).toEqual('2px'); - - hostBindingDir.width = 5; - fixture.update(); - expect(hostBindingEl.style.width).toEqual('5px'); - }); - - it('should apply static host classes', () => { - /** - * host: { - * 'class': 'mat-toolbar' - * } - */ - class StaticHostClass { - static ngComponentDef = ɵɵdefineComponent({ - type: StaticHostClass, - selectors: [['static-host-class']], - factory: () => new StaticHostClass(), - consts: 0, - vars: 0, - hostBindings: (rf: RenderFlags, ctx: StaticHostClass, elIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵelementHostAttrs([AttributeMarker.Classes, 'mat-toolbar']); - ɵɵelementHostStyling(['mat-toolbar']); - } - if (rf & RenderFlags.Update) { - ɵɵelementHostStylingApply(); - } - }, - template: (rf: RenderFlags, cmp: StaticHostClass) => {} - }); - } - - /** */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'static-host-class'); - } - }, 1, 0, [StaticHostClass]); - - const fixture = new ComponentFixture(App); - const hostBindingEl = fixture.hostElement.querySelector('static-host-class') as HTMLElement; - expect(hostBindingEl.className).toEqual('mat-toolbar'); - }); - }); - - describe('sanitization', () => { - function verify( - tag: string, prop: string, value: any, expectedSanitizedValue: any, sanitizeFn: any, - bypassFn: any, isAttribute: boolean = true) { - it('should sanitize potentially unsafe properties and attributes', () => { - let hostBindingDir: UnsafeUrlHostBindingDir; - class UnsafeUrlHostBindingDir { - // val: any = value; - static ngDirectiveDef = ɵɵdefineDirective({ - type: UnsafeUrlHostBindingDir, - selectors: [['', 'unsafeUrlHostBindingDir', '']], - factory: () => hostBindingDir = new UnsafeUrlHostBindingDir(), - hostBindings: (rf: RenderFlags, ctx: any, elementIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - const fn = isAttribute ? ɵɵelementAttribute : ɵɵelementProperty; - (fn as any)(elementIndex, prop, ɵɵbind(ctx[prop]), sanitizeFn, true); - } - } - }); - } - - const fixture = new TemplateFixture(() => { - ɵɵelement(0, tag, ['unsafeUrlHostBindingDir', '']); - }, () => {}, 1, 0, [UnsafeUrlHostBindingDir]); - - const el = fixture.hostElement.querySelector(tag) !; - const current = () => isAttribute ? el.getAttribute(prop) : (el as any)[prop]; - - (hostBindingDir !as any)[prop] = value; - fixture.update(); - expect(current()).toEqual(expectedSanitizedValue); - - (hostBindingDir !as any)[prop] = bypassFn(value); - fixture.update(); - expect(current()).toEqual(value); - }); - } - - verify( - 'a', 'href', 'javascript:alert(1)', 'unsafe:javascript:alert(1)', - ɵɵsanitizeUrlOrResourceUrl, bypassSanitizationTrustUrl); - verify( - 'script', 'src', bypassSanitizationTrustResourceUrl('javascript:alert(2)'), - 'javascript:alert(2)', ɵɵsanitizeUrlOrResourceUrl, bypassSanitizationTrustResourceUrl); - verify( - 'blockquote', 'cite', 'javascript:alert(3)', 'unsafe:javascript:alert(3)', ɵɵsanitizeUrl, - bypassSanitizationTrustUrl); - verify( - 'b', 'innerHTML', '', - '', ɵɵsanitizeHtml, bypassSanitizationTrustHtml, - /* isAttribute */ false); - }); -});