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>${tag}>`})
+ 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);
- });
-});