diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts index 900a5a2bb2..f211d2ce18 100644 --- a/packages/core/test/acceptance/integration_spec.ts +++ b/packages/core/test/acceptance/integration_spec.ts @@ -5,12 +5,1380 @@ * 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 {Component, ContentChild, Directive, EventEmitter, HostBinding, HostListener, Input, Output, QueryList, TemplateRef, ViewChildren} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Component, ContentChild, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnInit, Output, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; +import {onlyInIvy} from '@angular/private/testing'; describe('acceptance integration tests', () => { + function stripHtmlComments(str: string) { return str.replace(//g, ''); } + + describe('render', () => { + + it('should render basic template', () => { + @Component({template: 'Greetings'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + expect(fixture.nativeElement.innerHTML).toEqual('Greetings'); + }); + + it('should render and update basic "Hello, World" template', () => { + @Component({template: '

Hello, {{name}}!

'}) + class App { + name = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.name = 'World'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

Hello, World!

'); + + fixture.componentInstance.name = 'New World'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

Hello, New World!

'); + }); + }); + + describe('ng-container', () => { + + it('should insert as a child of a regular element', () => { + @Component( + {template: '
before|Greetings|after
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + // Strip comments since VE and Ivy put them in different places. + expect(stripHtmlComments(fixture.nativeElement.innerHTML)) + .toBe('
before|Greetings|after
'); + }); + + it('should add and remove DOM nodes when ng-container is a child of a regular element', () => { + @Component({ + template: + '
content
' + }) + class App { + render = false; + } + + TestBed.configureTestingModule({declarations: [App], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + + fixture.componentInstance.render = true; + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('
content
'); + + fixture.componentInstance.render = false; + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + }); + + it('should add and remove DOM nodes when ng-container is a child of an embedded view', () => { + + @Component({template: 'content'}) + class App { + render = false; + } + + TestBed.configureTestingModule({declarations: [App], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + + fixture.componentInstance.render = true; + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('content'); + + fixture.componentInstance.render = false; + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + }); + + // https://stackblitz.com/edit/angular-tfhcz1?file=src%2Fapp%2Fapp.component.ts + it('should add and remove DOM nodes when ng-container is a child of a delayed embedded view', + () => { + + @Directive({selector: '[testDirective]'}) + class TestDirective { + constructor(private _tplRef: TemplateRef, private _vcRef: ViewContainerRef) {} + + createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); } + + clear() { this._vcRef.clear(); } + } + + @Component({ + template: 'content' + }) + class App { + @ViewChild(TestDirective) testDirective !: TestDirective; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); + + fixture.componentInstance.testDirective.createAndInsert(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe('content'); + + fixture.componentInstance.testDirective.clear(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); + }); + + it('should render at the component view root', () => { + @Component( + {selector: 'test-cmpt', template: 'component template'}) + class TestCmpt { + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, TestCmpt]}); + const fixture = TestBed.createComponent(App); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)) + .toBe('component template'); + }); + + it('should render inside another ng-container', () => { + @Component({ + selector: 'test-cmpt', + template: + 'content' + }) + class TestCmpt { + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, TestCmpt]}); + const fixture = TestBed.createComponent(App); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)) + .toBe('content'); + }); + + it('should render inside another ng-container at the root of a delayed view', () => { + @Directive({selector: '[testDirective]'}) + class TestDirective { + constructor(private _tplRef: TemplateRef, private _vcRef: ViewContainerRef) {} + + createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); } + + clear() { this._vcRef.clear(); } + } + + @Component({ + template: + 'content' + }) + class App { + @ViewChild(TestDirective) testDirective !: TestDirective; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); + + fixture.componentInstance.testDirective.createAndInsert(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe('content'); + + fixture.componentInstance.testDirective.createAndInsert(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe('contentcontent'); + + fixture.componentInstance.testDirective.clear(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); + }); + + it('should support directives and inject ElementRef', () => { + @Directive({selector: '[dir]'}) + class TestDirective { + constructor(public elRef: ElementRef) {} + } + + @Component({template: '
'}) + class App { + @ViewChild(TestDirective) testDirective !: TestDirective; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('
'); + expect(fixture.componentInstance.testDirective.elRef.nativeElement.nodeType) + .toBe(Node.COMMENT_NODE); + }); + + it('should support ViewContainerRef when ng-container is at the root of a view', () => { + @Directive({selector: '[dir]'}) + class TestDirective { + @Input() + contentTpl: TemplateRef<{}>|null = null; + + constructor(private _vcRef: ViewContainerRef) {} + + insertView() { this._vcRef.createEmbeddedView(this.contentTpl as TemplateRef<{}>); } + + clear() { this._vcRef.clear(); } + } + + @Component({ + template: + 'Content' + }) + class App { + @ViewChild(TestDirective) testDirective !: TestDirective; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + + fixture.componentInstance.testDirective.insertView(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('Content'); + + fixture.componentInstance.testDirective.clear(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + }); + + it('should support ViewContainerRef on inside ', () => { + @Directive({selector: '[dir]'}) + class TestDirective { + constructor(private _tplRef: TemplateRef<{}>, private _vcRef: ViewContainerRef) {} + + insertView() { this._vcRef.createEmbeddedView(this._tplRef); } + + clear() { this._vcRef.clear(); } + } + + @Component({template: 'Content'}) + class App { + @ViewChild(TestDirective) testDirective !: TestDirective; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + + fixture.componentInstance.testDirective.insertView(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('Content'); + + fixture.componentInstance.testDirective.clear(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + }); + + it('should not set any attributes', () => { + @Component({template: '
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('
'); + }); + + }); + + describe('text bindings', () => { + it('should render "undefined" as ""', () => { + @Component({template: '{{name}}'}) + class App { + name: string|undefined = 'benoit'; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('benoit'); + + fixture.componentInstance.name = undefined; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual(''); + }); + + it('should render "null" as ""', () => { + @Component({template: '{{name}}'}) + class App { + name: string|null = 'benoit'; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('benoit'); + + fixture.componentInstance.name = null; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual(''); + }); + + }); + + describe('ngNonBindable handling', () => { + function stripNgNonBindable(str: string) { return str.replace(/ ngnonbindable=""/i, ''); } + + it('should keep local ref for host element', () => { + @Component({ + template: ` + + Hello {{ name }}! + + {{ myRef.id }} + ` + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripNgNonBindable(fixture.nativeElement.innerHTML)) + .toEqual('Hello {{ name }}! my-id '); + }); + + it('should invoke directives for host element', () => { + let directiveInvoked: boolean = false; + + @Directive({selector: '[directive]'}) + class TestDirective implements OnInit { + ngOnInit() { directiveInvoked = true; } + } + + @Component({ + template: ` + + Hello {{ name }}! + + ` + }) + class App { + name = 'World'; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripNgNonBindable(fixture.nativeElement.innerHTML)) + .toEqual('Hello {{ name }}!'); + expect(directiveInvoked).toEqual(true); + }); + + it('should not invoke directives for nested elements', () => { + let directiveInvoked: boolean = false; + + @Directive({selector: '[directive]'}) + class TestDirective implements OnInit { + ngOnInit() { directiveInvoked = true; } + } + + @Component({ + template: ` + + Hello {{ name }}! + + ` + }) + class App { + name = 'World'; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripNgNonBindable(fixture.nativeElement.innerHTML)) + .toEqual('Hello {{ name }}!'); + expect(directiveInvoked).toEqual(false); + }); + }); + + describe('Siblings update', () => { + it('should handle a flat list of static/bound text nodes', () => { + @Component({template: 'Hello {{name}}!'}) + class App { + name = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.name = 'world'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello world!'); + + fixture.componentInstance.name = 'monde'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello monde!'); + }); + + it('should handle a list of static/bound text nodes as element children', () => { + @Component({template: 'Hello {{name}}!'}) + class App { + name = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.name = 'world'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello world!'); + + fixture.componentInstance.name = 'mundo'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello mundo!'); + }); + + it('should render/update text node as a child of a deep list of elements', () => { + @Component({template: 'Hello {{name}}!'}) + class App { + name = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.name = 'world'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello world!'); + + fixture.componentInstance.name = 'mundo'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello mundo!'); + }); + + it('should update 2 sibling elements', () => { + @Component({template: ''}) + class App { + id = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.id = 'foo'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(''); + + fixture.componentInstance.id = 'bar'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(''); + }); + + it('should handle sibling text node after element with child text node', () => { + @Component({template: '

hello

{{name}}'}) + class App { + name = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.name = 'world'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

hello

world'); + + fixture.componentInstance.name = 'mundo'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

hello

mundo'); + }); + }); + + describe('basic components', () => { + @Component({selector: 'todo', template: '

Todo{{value}}

'}) + class TodoComponent { + value = ' one'; + } + + it('should support a basic component template', () => { + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, TodoComponent]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

Todo one

'); + }); + + it('should support a component template with sibling', () => { + @Component({template: 'two'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, TodoComponent]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

Todo one

two'); + }); + + it('should support a component template with component sibling', () => { + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, TodoComponent]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML) + .toEqual('

Todo one

Todo one

'); + }); + + it('should support a component with binding on host element', () => { + @Component({selector: 'todo', template: '{{title}}'}) + class TodoComponentHostBinding { + @HostBinding() + title = 'one'; + } + + @Component({template: ''}) + class App { + @ViewChild(TodoComponentHostBinding) todoComponentHostBinding !: TodoComponentHostBinding; + } + + TestBed.configureTestingModule({declarations: [App, TodoComponentHostBinding]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('one'); + + fixture.componentInstance.todoComponentHostBinding.title = 'two'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('two'); + }); + + it('should support root component with host attribute', () => { + @Component({selector: 'host-attr-comp', template: '', host: {'role': 'button'}}) + class HostAttributeComp { + } + + TestBed.configureTestingModule({declarations: [HostAttributeComp]}); + const fixture = TestBed.createComponent(HostAttributeComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.getAttribute('role')).toEqual('button'); + }); + + it('should support component with bindings in template', () => { + @Component({selector: 'comp', template: '

{{ name }}

'}) + class MyComp { + name = 'Bess'; + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

Bess

'); + }); + + it('should support a component with sub-views', () => { + @Component({selector: 'comp', template: '
text
'}) + class MyComp { + @Input() + condition !: boolean; + } + + @Component({template: ''}) + class App { + condition = false; + } + + TestBed.configureTestingModule({declarations: [App, MyComp], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const compElement = fixture.nativeElement.querySelector('comp'); + + fixture.componentInstance.condition = true; + fixture.detectChanges(); + expect(stripHtmlComments(compElement.innerHTML)).toEqual('
text
'); + + fixture.componentInstance.condition = false; + fixture.detectChanges(); + expect(stripHtmlComments(compElement.innerHTML)).toEqual(''); + }); + + }); + + describe('element bindings', () => { + describe('elementAttribute', () => { + it('should support attribute bindings', () => { + @Component({template: ''}) + class App { + title: string|null = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.title = 'Hello'; + fixture.detectChanges(); + // initial binding + expect(fixture.nativeElement.innerHTML).toEqual(''); + + // update binding + fixture.componentInstance.title = 'Hi!'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(''); + + // remove attribute + fixture.componentInstance.title = null; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(''); + }); + + it('should stringify values used attribute bindings', () => { + @Component({template: ''}) + class App { + title: any; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.title = NaN; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(''); + + fixture.componentInstance.title = {toString: () => 'Custom toString'}; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(''); + }); + + it('should update bindings', () => { + @Component({ + template: [ + 'a:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[12]}}{{c[13]}}{{c[14]}}{{c[15]}}{{c[16]}}', + 'a0:{{c[1]}}', + 'a1:{{c[0]}}{{c[1]}}{{c[16]}}', + 'a2:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[16]}}', + 'a3:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[16]}}', + 'a4:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[16]}}', + 'a5:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[16]}}', + 'a6:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[16]}}', + 'a7:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[12]}}{{c[13]}}{{c[16]}}', + 'a8:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[12]}}{{c[13]}}{{c[14]}}{{c[15]}}{{c[16]}}', + ].join('\n') + }) + class App { + c = ['(', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e', 5, 'f', 6, 'g', 7, ')']; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent.trim()).toEqual([ + 'a:(0a1b2c3d4e5f6g7)', + 'a0:0', + 'a1:(0)', + 'a2:(0a1)', + 'a3:(0a1b2)', + 'a4:(0a1b2c3)', + 'a5:(0a1b2c3d4)', + 'a6:(0a1b2c3d4e5)', + 'a7:(0a1b2c3d4e5f6)', + 'a8:(0a1b2c3d4e5f6g7)', + ].join('\n')); + + fixture.componentInstance.c.reverse(); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent.trim()).toEqual([ + 'a:)7g6f5e4d3c2b1a0(', + 'a0:7', + 'a1:)7(', + 'a2:)7g6(', + 'a3:)7g6f5(', + 'a4:)7g6f5e4(', + 'a5:)7g6f5e4d3(', + 'a6:)7g6f5e4d3c2(', + 'a7:)7g6f5e4d3c2b1(', + 'a8:)7g6f5e4d3c2b1a0(', + ].join('\n')); + + fixture.componentInstance.c.reverse(); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent.trim()).toEqual([ + 'a:(0a1b2c3d4e5f6g7)', + 'a0:0', + 'a1:(0)', + 'a2:(0a1)', + 'a3:(0a1b2)', + 'a4:(0a1b2c3)', + 'a5:(0a1b2c3d4)', + 'a6:(0a1b2c3d4e5)', + 'a7:(0a1b2c3d4e5f6)', + 'a8:(0a1b2c3d4e5f6g7)', + ].join('\n')); + }); + + it('should not update DOM if context has not changed', () => { + @Component({ + template: ` + + + + ` + }) + class App { + title: string|null = ''; + shouldRender = true; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const span: HTMLSpanElement = fixture.nativeElement.querySelector('span'); + const bold: HTMLElement = span.querySelector('b') !; + + fixture.componentInstance.title = 'Hello'; + fixture.detectChanges(); + + // initial binding + expect(span.getAttribute('title')).toBe('Hello'); + expect(bold.getAttribute('title')).toBe('Hello'); + + // update DOM manually + bold.setAttribute('title', 'Goodbye'); + + // refresh with same binding + fixture.detectChanges(); + expect(span.getAttribute('title')).toBe('Hello'); + expect(bold.getAttribute('title')).toBe('Goodbye'); + + // refresh again with same binding + fixture.detectChanges(); + expect(span.getAttribute('title')).toBe('Hello'); + expect(bold.getAttribute('title')).toBe('Goodbye'); + }); + + it('should support host attribute bindings', () => { + @Directive({selector: '[hostBindingDir]'}) + class HostBindingDir { + @HostBinding('attr.aria-label') + label = 'some label'; + } + + @Component({template: '
'}) + class App { + @ViewChild(HostBindingDir) hostBindingDir !: HostBindingDir; + } + + TestBed.configureTestingModule({declarations: [App, HostBindingDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const hostBindingEl = fixture.nativeElement.querySelector('div'); + + // Needs `toLowerCase`, because different browsers produce + // attributes either in camel case or lower case. + expect(hostBindingEl.getAttribute('aria-label')).toBe('some label'); + + fixture.componentInstance.hostBindingDir.label = 'other label'; + fixture.detectChanges(); + + expect(hostBindingEl.getAttribute('aria-label')).toBe('other label'); + }); + }); + + describe('elementStyle', () => { + it('should support binding to styles', () => { + @Component({template: ''}) + class App { + size: string|null = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.size = '10px'; + fixture.detectChanges(); + const span: HTMLElement = fixture.nativeElement.querySelector('span'); + + expect(span.style.fontSize).toBe('10px'); + + fixture.componentInstance.size = '16px'; + fixture.detectChanges(); + expect(span.style.fontSize).toBe('16px'); + + fixture.componentInstance.size = null; + fixture.detectChanges(); + expect(span.style.fontSize).toBeFalsy(); + }); + + it('should support binding to styles with suffix', () => { + @Component({template: ''}) + class App { + size: string|number|null = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.size = '100'; + fixture.detectChanges(); + const span: HTMLElement = fixture.nativeElement.querySelector('span'); + + expect(span.style.fontSize).toEqual('100px'); + + fixture.componentInstance.size = 200; + fixture.detectChanges(); + expect(span.style.fontSize).toEqual('200px'); + + fixture.componentInstance.size = 0; + fixture.detectChanges(); + expect(span.style.fontSize).toEqual('0px'); + + fixture.componentInstance.size = null; + fixture.detectChanges(); + expect(span.style.fontSize).toBeFalsy(); + }); + }); + + describe('class-based styling', () => { + it('should support CSS class toggle', () => { + @Component({template: ''}) + class App { + value: any; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = true; + fixture.detectChanges(); + const span = fixture.nativeElement.querySelector('span'); + + expect(span.getAttribute('class')).toEqual('active'); + + fixture.componentInstance.value = false; + fixture.detectChanges(); + expect(span.getAttribute('class')).toBeFalsy(); + + // truthy values + fixture.componentInstance.value = 'a_string'; + fixture.detectChanges(); + expect(span.getAttribute('class')).toEqual('active'); + + fixture.componentInstance.value = 10; + fixture.detectChanges(); + expect(span.getAttribute('class')).toEqual('active'); + + // falsy values + fixture.componentInstance.value = ''; + fixture.detectChanges(); + expect(span.getAttribute('class')).toBeFalsy(); + + fixture.componentInstance.value = 0; + fixture.detectChanges(); + expect(span.getAttribute('class')).toBeFalsy(); + }); + + it('should work correctly with existing static classes', () => { + @Component({template: ''}) + class App { + value: any; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = true; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(''); + + fixture.componentInstance.value = false; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(''); + }); + + it('should apply classes properly when nodes are components', () => { + @Component({selector: 'my-comp', template: 'Comp Content'}) + class MyComp { + } + + @Component({template: ''}) + class App { + value: any; + } + + TestBed.configureTestingModule({declarations: [App, MyComp]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = true; + fixture.detectChanges(); + const compElement = fixture.nativeElement.querySelector('my-comp'); + + expect(fixture.nativeElement.textContent).toContain('Comp Content'); + expect(compElement.getAttribute('class')).toBe('active'); + + fixture.componentInstance.value = false; + fixture.detectChanges(); + expect(compElement.getAttribute('class')).toBeFalsy(); + }); + + it('should apply classes properly when nodes have containers', () => { + @Component({selector: 'structural-comp', template: 'Comp Content'}) + class StructuralComp { + @Input() + tmp !: TemplateRef; + + constructor(public vcr: ViewContainerRef) {} + + create() { this.vcr.createEmbeddedView(this.tmp); } + } + + @Component({ + template: ` + Temp Content + + ` + }) + class App { + @ViewChild(StructuralComp) structuralComp !: StructuralComp; + value: any; + } + + TestBed.configureTestingModule({declarations: [App, StructuralComp]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = true; + fixture.detectChanges(); + const structuralCompEl = fixture.nativeElement.querySelector('structural-comp'); + + expect(structuralCompEl.getAttribute('class')).toEqual('active'); + + fixture.componentInstance.structuralComp.create(); + fixture.detectChanges(); + expect(structuralCompEl.getAttribute('class')).toEqual('active'); + + fixture.componentInstance.value = false; + fixture.detectChanges(); + expect(structuralCompEl.getAttribute('class')).toEqual(''); + }); + + @Directive({selector: '[DirWithClass]'}) + class DirWithClassDirective { + public classesVal: string = ''; + + @Input('class') + set klass(value: string) { this.classesVal = value; } + } + + @Directive({selector: '[DirWithStyle]'}) + class DirWithStyleDirective { + public stylesVal: string = ''; + + @Input() + set style(value: string) { this.stylesVal = value; } + } + + it('should delegate initial classes to a [class] input binding if present on a directive on the same element', + () => { + @Component({template: '
'}) + class App { + @ViewChild(DirWithClassDirective) mockClassDirective !: DirWithClassDirective; + } + + TestBed.configureTestingModule({declarations: [App, DirWithClassDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.componentInstance.mockClassDirective.classesVal) + .toEqual('apple orange banana'); + }); + + it('should delegate initial styles to a [style] input binding if present on a directive on the same element', + () => { + @Component({template: '
'}) + class App { + @ViewChild(DirWithStyleDirective) mockStyleDirective !: DirWithStyleDirective; + } + + TestBed.configureTestingModule({declarations: [App, DirWithStyleDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const styles = fixture.componentInstance.mockStyleDirective.stylesVal; + + // Use `toContain` since Ivy and ViewEngine have some slight differences in formatting. + expect(styles).toContain('width:100px'); + expect(styles).toContain('height:200px'); + }); + + it('should update `[class]` and bindings in the provided directive if the input is matched', + () => { + @Component({template: '
'}) + class App { + @ViewChild(DirWithClassDirective) mockClassDirective !: DirWithClassDirective; + value = ''; + } + + TestBed.configureTestingModule({declarations: [App, DirWithClassDirective]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = 'cucumber grape'; + fixture.detectChanges(); + + expect(fixture.componentInstance.mockClassDirective.classesVal) + .toEqual('cucumber grape'); + }); + + onlyInIvy('Passing an object into [style] works differently') + .it('should update `[style]` and bindings in the provided directive if the input is matched', + () => { + @Component({template: '
'}) + class App { + @ViewChild(DirWithStyleDirective) mockStyleDirective !: DirWithStyleDirective; + value !: {[key: string]: string}; + } + + TestBed.configureTestingModule({declarations: [App, DirWithStyleDirective]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = {width: '200px', height: '500px'}; + fixture.detectChanges(); + + expect(fixture.componentInstance.mockStyleDirective.stylesVal) + .toEqual('width:200px;height:500px'); + }); + + onlyInIvy('Style binding merging works differently in Ivy') + .it('should apply initial styling to the element that contains the directive with host styling', + () => { + @Directive({ + selector: '[DirWithInitialStyling]', + host: { + 'title': 'foo', + 'class': 'heavy golden', + 'style': 'color: purple', + '[style.font-weight]': '"bold"' + } + }) + class DirWithInitialStyling { + } + + @Component({ + template: ` +
+ ` + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App, DirWithInitialStyling]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const target: HTMLDivElement = fixture.nativeElement.querySelector('div'); + const classes = target.getAttribute('class') !.split(/\s+/).sort(); + expect(classes).toEqual(['big', 'golden', 'heavy']); + + expect(target.getAttribute('title')).toEqual('foo'); + expect(target.style.getPropertyValue('color')).toEqual('black'); + expect(target.style.getPropertyValue('font-size')).toEqual('200px'); + expect(target.style.getPropertyValue('font-weight')).toEqual('bold'); + }); + + onlyInIvy('Style binding merging works differently in Ivy') + .it('should apply single styling bindings present within a directive onto the same element and defer the element\'s initial styling values when missing', + () => { + @Directive({ + selector: '[DirWithSingleStylingBindings]', + host: { + 'class': 'def', + '[class.xyz]': 'activateXYZClass', + '[style.width]': 'width', + '[style.height]': 'height' + } + }) + class DirWithSingleStylingBindings { + width: null|string = null; + height: null|string = null; + activateXYZClass: boolean = false; + } + + @Component({ + template: ` +
+ ` + }) + class App { + @ViewChild(DirWithSingleStylingBindings) + dirInstance !: DirWithSingleStylingBindings; + } + + TestBed.configureTestingModule({declarations: [App, DirWithSingleStylingBindings]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const dirInstance = fixture.componentInstance.dirInstance; + const target: HTMLDivElement = fixture.nativeElement.querySelector('div'); + expect(target.style.getPropertyValue('width')).toEqual('100px'); + expect(target.style.getPropertyValue('height')).toEqual('200px'); + expect(target.classList.contains('abc')).toBeTruthy(); + expect(target.classList.contains('def')).toBeTruthy(); + expect(target.classList.contains('xyz')).toBeFalsy(); + + dirInstance.width = '444px'; + dirInstance.height = '999px'; + dirInstance.activateXYZClass = true; + fixture.detectChanges(); + + expect(target.style.getPropertyValue('width')).toEqual('444px'); + expect(target.style.getPropertyValue('height')).toEqual('999px'); + expect(target.classList.contains('abc')).toBeTruthy(); + expect(target.classList.contains('def')).toBeTruthy(); + expect(target.classList.contains('xyz')).toBeTruthy(); + + dirInstance.width = null; + dirInstance.height = null; + fixture.detectChanges(); + + expect(target.style.getPropertyValue('width')).toEqual('100px'); + expect(target.style.getPropertyValue('height')).toEqual('200px'); + expect(target.classList.contains('abc')).toBeTruthy(); + expect(target.classList.contains('def')).toBeTruthy(); + expect(target.classList.contains('xyz')).toBeTruthy(); + }); + + onlyInIvy('Style binding merging works differently in Ivy') + .it('should properly prioritize single style binding collisions when they exist on multiple directives', + () => { + @Directive({selector: '[Dir1WithStyle]', host: {'[style.width]': 'width'}}) + class Dir1WithStyle { + width: null|string = null; + } + + @Directive({ + selector: '[Dir2WithStyle]', + host: {'style': 'width: 111px', '[style.width]': 'width'} + }) + class Dir2WithStyle { + width: null|string = null; + } + + @Component( + {template: '
'}) + class App { + @ViewChild(Dir1WithStyle) dir1Instance !: Dir1WithStyle; + @ViewChild(Dir2WithStyle) dir2Instance !: Dir2WithStyle; + width: string|null = null; + } + + TestBed.configureTestingModule({declarations: [App, Dir1WithStyle, Dir2WithStyle]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const {dir1Instance, dir2Instance} = fixture.componentInstance; + + const target: HTMLDivElement = fixture.nativeElement.querySelector('div'); + expect(target.style.getPropertyValue('width')).toEqual('111px'); + + fixture.componentInstance.width = '999px'; + dir1Instance.width = '222px'; + dir2Instance.width = '333px'; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('999px'); + + fixture.componentInstance.width = null; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('222px'); + + dir1Instance.width = null; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('333px'); + + dir2Instance.width = null; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('111px'); + + dir1Instance.width = '666px'; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('666px'); + + fixture.componentInstance.width = '777px'; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('777px'); + }); + + onlyInIvy('Style binding merging works differently in Ivy') + .it('should properly prioritize multi style binding collisions when they exist on multiple directives', + () => { + @Directive({ + selector: '[Dir1WithStyling]', + host: {'[style]': 'stylesExp', '[class]': 'classesExp'} + }) + class Dir1WithStyling { + classesExp: any = {}; + stylesExp: any = {}; + } + + @Directive({ + selector: '[Dir2WithStyling]', + host: {'style': 'width: 111px', '[style]': 'stylesExp'} + }) + class Dir2WithStyling { + stylesExp: any = {}; + } + + @Component({ + template: + '
' + }) + class App { + @ViewChild(Dir1WithStyling) dir1Instance !: Dir1WithStyling; + @ViewChild(Dir2WithStyling) dir2Instance !: Dir2WithStyling; + stylesExp: any = {}; + classesExp: any = {}; + } + + TestBed.configureTestingModule( + {declarations: [App, Dir1WithStyling, Dir2WithStyling]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const {dir1Instance, dir2Instance} = fixture.componentInstance; + + const target = fixture.nativeElement.querySelector('div') !; + expect(target.style.getPropertyValue('width')).toEqual('111px'); + + const compInstance = fixture.componentInstance; + compInstance.stylesExp = {width: '999px', height: null}; + compInstance.classesExp = {one: true, two: false}; + dir1Instance.stylesExp = {width: '222px'}; + dir1Instance.classesExp = {two: true, three: false}; + dir2Instance.stylesExp = {width: '333px', height: '100px'}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('999px'); + expect(target.style.getPropertyValue('height')).toEqual('100px'); + expect(target.classList.contains('one')).toBeTruthy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + + compInstance.stylesExp = {}; + compInstance.classesExp = {}; + dir1Instance.stylesExp = {width: '222px', height: '200px'}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('222px'); + expect(target.style.getPropertyValue('height')).toEqual('200px'); + expect(target.classList.contains('one')).toBeFalsy(); + expect(target.classList.contains('two')).toBeTruthy(); + expect(target.classList.contains('three')).toBeFalsy(); + + dir1Instance.stylesExp = {}; + dir1Instance.classesExp = {}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('333px'); + expect(target.style.getPropertyValue('height')).toEqual('100px'); + expect(target.classList.contains('one')).toBeFalsy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + + dir2Instance.stylesExp = {}; + compInstance.stylesExp = {height: '900px'}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('111px'); + expect(target.style.getPropertyValue('height')).toEqual('900px'); + + dir1Instance.stylesExp = {width: '666px', height: '600px'}; + dir1Instance.classesExp = {four: true, one: true}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('666px'); + expect(target.style.getPropertyValue('height')).toEqual('900px'); + expect(target.classList.contains('one')).toBeTruthy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + expect(target.classList.contains('four')).toBeTruthy(); + + compInstance.stylesExp = {width: '777px'}; + compInstance.classesExp = {four: false}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('777px'); + expect(target.style.getPropertyValue('height')).toEqual('600px'); + expect(target.classList.contains('one')).toBeTruthy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + expect(target.classList.contains('four')).toBeFalsy(); + }); + }); + + it('should properly handle and render interpolation for class attribute bindings', () => { + @Component({template: '
'}) + class App { + name = ''; + age = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + const target = fixture.nativeElement.querySelector('div') !; + + expect(target.classList.contains('-fred-36-')).toBeFalsy(); + + fixture.componentInstance.name = 'fred'; + fixture.componentInstance.age = '36'; + fixture.detectChanges(); + + expect(target.classList.contains('-fred-36-')).toBeTruthy(); + }); + }); + it('should only call inherited host listeners once', () => { let clicks = 0; diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 9c0118f69d..9af14ac2b4 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -6,1080 +6,42 @@ * found in the LICENSE file at https://angular.io/license */ -import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core'; - import {RendererType2} from '../../src/render/api'; import {getLContext} from '../../src/render3/context_discovery'; -import {AttributeMarker, ΔclassMap, ΔdefineComponent, ΔdefineDirective, ΔstyleMap, ΔtemplateRefExtractor} from '../../src/render3/index'; -import {ΔallocHostVars, Δbind, ΔclassProp, Δcontainer, ΔcontainerRefreshEnd, ΔcontainerRefreshStart, ΔdirectiveInject, Δelement, ΔelementAttribute, ΔelementContainerEnd, ΔelementContainerStart, ΔelementEnd, ΔelementHostAttrs, ΔelementProperty, ΔelementStart, ΔembeddedViewEnd, ΔembeddedViewStart, Δinterpolation1, Δinterpolation2, Δinterpolation3, Δinterpolation4, Δinterpolation5, Δinterpolation6, Δinterpolation7, Δinterpolation8, ΔinterpolationV, Δprojection, ΔprojectionDef, Δreference, Δselect, ΔstyleProp, Δstyling, ΔstylingApply, Δtemplate, Δtext, ΔtextBinding} from '../../src/render3/instructions/all'; +import {AttributeMarker, ΔdefineComponent, ΔdefineDirective} from '../../src/render3/index'; +import {ΔallocHostVars, Δbind, Δcontainer, ΔcontainerRefreshEnd, ΔcontainerRefreshStart, Δelement, ΔelementAttribute, ΔelementEnd, ΔelementProperty, ΔelementStart, ΔembeddedViewEnd, ΔembeddedViewStart, Δprojection, ΔprojectionDef, Δselect, Δstyling, ΔstylingApply, Δtemplate, Δtext, ΔtextBinding} from '../../src/render3/instructions/all'; import {MONKEY_PATCH_KEY_NAME} from '../../src/render3/interfaces/context'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer'; import {StylingIndex} from '../../src/render3/interfaces/styling'; import {CONTEXT, HEADER_OFFSET} from '../../src/render3/interfaces/view'; -import {ΔdisableBindings, ΔenableBindings} from '../../src/render3/state'; import {ΔsanitizeUrl} from '../../src/sanitization/sanitization'; import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; import {NgIf} from './common_with_def'; -import {ComponentFixture, MockRendererFactory, TemplateFixture, createComponent, renderToHtml} from './render_util'; +import {ComponentFixture, MockRendererFactory, renderToHtml} from './render_util'; describe('render3 integration test', () => { describe('render', () => { - - it('should render basic template', () => { - expect(renderToHtml(Template, {}, 2)).toEqual('Greetings'); - - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'span', ['title', 'Hello']); - { Δtext(1, 'Greetings'); } - ΔelementEnd(); - } - } - expect(ngDevMode).toHaveProperties({ - firstTemplatePass: 1, - tNode: 3, // 1 for div, 1 for text, 1 for host element - tView: 2, // 1 for root view, 1 for template - rendererCreateElement: 1, - }); - }); - - it('should render and update basic "Hello, World" template', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'h1'); - { Δtext(1); } - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(1, Δinterpolation1('Hello, ', ctx.name, '!')); - } - }, 2, 1); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'World'; - fixture.update(); - expect(fixture.html).toEqual('

Hello, World!

'); - - fixture.component.name = 'New World'; - fixture.update(); - expect(fixture.html).toEqual('

Hello, New World!

'); - }); - }); - - describe('text bindings', () => { - it('should render "undefined" as "" when used with `bind()`', () => { - function Template(rf: RenderFlags, name: string) { - if (rf & RenderFlags.Create) { - Δtext(0); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(0, Δbind(name)); - } - } - - expect(renderToHtml(Template, 'benoit', 1, 1)).toEqual('benoit'); - expect(renderToHtml(Template, undefined, 1, 1)).toEqual(''); - expect(ngDevMode).toHaveProperties({ - firstTemplatePass: 0, - tNode: 2, - tView: 2, // 1 for root view, 1 for template - rendererSetText: 2, - }); - }); - - it('should render "null" as "" when used with `bind()`', () => { - function Template(rf: RenderFlags, name: string) { - if (rf & RenderFlags.Create) { - Δtext(0); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(0, Δbind(name)); - } - } - - expect(renderToHtml(Template, 'benoit', 1, 1)).toEqual('benoit'); - expect(renderToHtml(Template, null, 1, 1)).toEqual(''); - expect(ngDevMode).toHaveProperties({ - firstTemplatePass: 0, - tNode: 2, - tView: 2, // 1 for root view, 1 for template - rendererSetText: 2, - }); - }); - - it('should support creation-time values in text nodes', () => { - function Template(rf: RenderFlags, value: string) { - if (rf & RenderFlags.Create) { - Δtext(0); - ΔtextBinding(0, value); - } - } - expect(renderToHtml(Template, 'once', 1, 1)).toEqual('once'); - expect(renderToHtml(Template, 'twice', 1, 1)).toEqual('once'); - expect(ngDevMode).toHaveProperties({ - firstTemplatePass: 0, - tNode: 2, - tView: 2, // 1 for root view, 1 for template - rendererSetText: 1, - }); - }); - - }); - - - describe('ngNonBindable handling', () => { - it('should keep local ref for host element', () => { - /** - * - * Hello {{ name }}! - * - * {{ myRef.id }} - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'b', ['id', 'my-id'], ['myRef', '']); - ΔdisableBindings(); - ΔelementStart(2, 'i'); - Δtext(3, 'Hello {{ name }}!'); - ΔelementEnd(); - ΔenableBindings(); - ΔelementEnd(); - Δtext(4); - } - if (rf & RenderFlags.Update) { - const ref = Δreference(1) as any; - ΔtextBinding(4, Δinterpolation1(' ', ref.id, ' ')); - } - }, 5, 1); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('Hello {{ name }}! my-id '); - }); - - it('should invoke directives for host element', () => { - let directiveInvoked: boolean = false; - - class TestDirective { - ngOnInit() { directiveInvoked = true; } - - static ngDirectiveDef = ΔdefineDirective({ - type: TestDirective, - selectors: [['', 'directive', '']], - factory: () => new TestDirective() - }); - } - - /** - * - * Hello {{ name }}! - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'b', ['directive', '']); - ΔdisableBindings(); - ΔelementStart(1, 'i'); - Δtext(2, 'Hello {{ name }}!'); - ΔelementEnd(); - ΔenableBindings(); - ΔelementEnd(); - } - }, 3, 0, [TestDirective]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('Hello {{ name }}!'); - expect(directiveInvoked).toEqual(true); - }); - - it('should not invoke directives for nested elements', () => { - let directiveInvoked: boolean = false; - - class TestDirective { - ngOnInit() { directiveInvoked = true; } - - static ngDirectiveDef = ΔdefineDirective({ - type: TestDirective, - selectors: [['', 'directive', '']], - factory: () => new TestDirective() - }); - } - - /** - * - * Hello {{ name }}! - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'b'); - ΔdisableBindings(); - ΔelementStart(1, 'i', ['directive', '']); - Δtext(2, 'Hello {{ name }}!'); - ΔelementEnd(); - ΔenableBindings(); - ΔelementEnd(); - } - }, 3, 0, [TestDirective]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('Hello {{ name }}!'); - expect(directiveInvoked).toEqual(false); - }); - }); - - describe('Siblings update', () => { - it('should handle a flat list of static/bound text nodes', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δtext(0, 'Hello '); - Δtext(1); - Δtext(2, '!'); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(1, Δbind(ctx.name)); - } - }, 3, 1); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'world'; - fixture.update(); - expect(fixture.html).toEqual('Hello world!'); - - fixture.component.name = 'monde'; - fixture.update(); - expect(fixture.html).toEqual('Hello monde!'); - }); - - it('should handle a list of static/bound text nodes as element children', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'b'); - { - Δtext(1, 'Hello '); - Δtext(2); - Δtext(3, '!'); - } - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(2, Δbind(ctx.name)); - } - }, 4, 1); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'world'; - fixture.update(); - expect(fixture.html).toEqual('Hello world!'); - - fixture.component.name = 'mundo'; - fixture.update(); - expect(fixture.html).toEqual('Hello mundo!'); - }); - - it('should render/update text node as a child of a deep list of elements', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'b'); - { - ΔelementStart(1, 'b'); - { - ΔelementStart(2, 'b'); - { - ΔelementStart(3, 'b'); - { Δtext(4); } - ΔelementEnd(); - } - ΔelementEnd(); - } - ΔelementEnd(); - } - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(4, Δinterpolation1('Hello ', ctx.name, '!')); - } - }, 5, 1); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'world'; - fixture.update(); - expect(fixture.html).toEqual('Hello world!'); - - fixture.component.name = 'mundo'; - fixture.update(); - expect(fixture.html).toEqual('Hello mundo!'); - }); - - it('should update 2 sibling elements', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'b'); - { - Δelement(1, 'span'); - ΔelementStart(2, 'span', ['class', 'foo']); - {} - ΔelementEnd(); - } - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - ΔelementAttribute(2, 'id', Δbind(ctx.id)); - } - }, 3, 1); - - const fixture = new ComponentFixture(App); - fixture.component.id = 'foo'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.id = 'bar'; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should handle sibling text node after element with child text node', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'p'); - { Δtext(1, 'hello'); } - ΔelementEnd(); - Δtext(2, 'world'); - } - }, 3); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('

hello

world'); - }); - }); - - describe('basic components', () => { - - class TodoComponent { - value = ' one'; - - static ngComponentDef = ΔdefineComponent({ - type: TodoComponent, - selectors: [['todo']], - consts: 3, - vars: 1, - template: function TodoTemplate(rf: RenderFlags, ctx: any) { + describe('text bindings', () => { + it('should support creation-time values in text nodes', () => { + function Template(rf: RenderFlags, value: string) { if (rf & RenderFlags.Create) { - ΔelementStart(0, 'p'); - { - Δtext(1, 'Todo'); - Δtext(2); - } - ΔelementEnd(); + Δtext(0); + ΔtextBinding(0, value); } - if (rf & RenderFlags.Update) { - ΔtextBinding(2, Δbind(ctx.value)); - } - }, - factory: () => new TodoComponent + } + expect(renderToHtml(Template, 'once', 1, 1)).toEqual('once'); + expect(renderToHtml(Template, 'twice', 1, 1)).toEqual('once'); + expect(ngDevMode).toHaveProperties({ + firstTemplatePass: 0, + tNode: 2, + tView: 2, // 1 for root view, 1 for template + rendererSetText: 1, + }); }); - } - - const defs = [TodoComponent]; - - it('should support a basic component template', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'todo'); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('

Todo one

'); }); - - it('should support a component template with sibling', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'todo'); - Δtext(1, 'two'); - } - }, 2, 0, defs); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('

Todo one

two'); - }); - - it('should support a component template with component sibling', () => { - /** - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'todo'); - Δelement(1, 'todo'); - } - }, 2, 0, defs); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('

Todo one

Todo one

'); - }); - - it('should support a component with binding on host element', () => { - let cmptInstance: TodoComponentHostBinding|null; - - class TodoComponentHostBinding { - title = 'one'; - static ngComponentDef = ΔdefineComponent({ - type: TodoComponentHostBinding, - selectors: [['todo']], - consts: 1, - vars: 1, - template: function TodoComponentHostBindingTemplate( - rf: RenderFlags, ctx: TodoComponentHostBinding) { - if (rf & RenderFlags.Create) { - Δtext(0); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(0, Δbind(ctx.title)); - } - }, - factory: () => cmptInstance = new TodoComponentHostBinding, - hostBindings: function(rf: RenderFlags, ctx: any, elementIndex: number): void { - if (rf & RenderFlags.Create) { - ΔallocHostVars(1); - } - if (rf & RenderFlags.Update) { - // host bindings - ΔelementProperty(elementIndex, 'title', Δbind(ctx.title)); - } - } - }); - } - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'todo'); - } - }, 1, 0, [TodoComponentHostBinding]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('one'); - - cmptInstance !.title = 'two'; - fixture.update(); - expect(fixture.html).toEqual('two'); - }); - - it('should support root component with host attribute', () => { - class HostAttributeComp { - static ngComponentDef = ΔdefineComponent({ - type: HostAttributeComp, - selectors: [['host-attr-comp']], - factory: () => new HostAttributeComp(), - consts: 0, - vars: 0, - hostBindings: function(rf, ctx, elIndex) { - if (rf & RenderFlags.Create) { - ΔelementHostAttrs(['role', 'button']); - } - }, - template: (rf: RenderFlags, ctx: HostAttributeComp) => {}, - }); - } - - const fixture = new ComponentFixture(HostAttributeComp); - expect(fixture.hostElement.getAttribute('role')).toEqual('button'); - }); - - it('should support component with bindings in template', () => { - /**

{{ name }}

*/ - class MyComp { - name = 'Bess'; - static ngComponentDef = ΔdefineComponent({ - type: MyComp, - selectors: [['comp']], - consts: 2, - vars: 1, - template: function MyCompTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'p'); - { Δtext(1); } - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(1, Δbind(ctx.name)); - } - }, - factory: () => new MyComp - }); - } - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'comp'); - } - }, 1, 0, [MyComp]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('

Bess

'); - }); - - it('should support a component with sub-views', () => { - /** - * % if (condition) { - *
text
- * % } - */ - class MyComp { - // TODO(issue/24571): remove '!'. - condition !: boolean; - static ngComponentDef = ΔdefineComponent({ - type: MyComp, - selectors: [['comp']], - consts: 1, - vars: 0, - template: function MyCompTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δcontainer(0); - } - if (rf & RenderFlags.Update) { - ΔcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ΔembeddedViewStart(0, 2, 0); - if (rf1 & RenderFlags.Create) { - ΔelementStart(0, 'div'); - { Δtext(1, 'text'); } - ΔelementEnd(); - } - ΔembeddedViewEnd(); - } - } - ΔcontainerRefreshEnd(); - } - }, - factory: () => new MyComp, - inputs: {condition: 'condition'} - }); - } - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'comp'); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'condition', Δbind(ctx.condition)); - } - }, 1, 1, [MyComp]); - - const fixture = new ComponentFixture(App); - fixture.component.condition = true; - fixture.update(); - expect(fixture.html).toEqual('
text
'); - - fixture.component.condition = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - }); - - describe('ng-container', () => { - - it('should insert as a child of a regular element', () => { - /** - *
before|Greetings|after
- */ - function Template() { - ΔelementStart(0, 'div'); - { - Δtext(1, 'before|'); - ΔelementContainerStart(2); - { - Δtext(3, 'Greetings'); - Δelement(4, 'span'); - } - ΔelementContainerEnd(); - Δtext(5, '|after'); - } - ΔelementEnd(); - } - - const fixture = new TemplateFixture(Template, () => {}, 6); - expect(fixture.html).toEqual('
before|Greetings|after
'); - }); - - it('should add and remove DOM nodes when ng-container is a child of a regular element', () => { - /** - * {% if (value) { %} - *
- * content - *
- * {% } %} - */ - const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - Δcontainer(0); - } - if (rf & RenderFlags.Update) { - ΔcontainerRefreshStart(0); - if (ctx.value) { - let rf1 = ΔembeddedViewStart(0, 3, 0); - { - if (rf1 & RenderFlags.Create) { - ΔelementStart(0, 'div'); - { - ΔelementContainerStart(1); - { Δtext(2, 'content'); } - ΔelementContainerEnd(); - } - ΔelementEnd(); - } - } - ΔembeddedViewEnd(); - } - ΔcontainerRefreshEnd(); - } - }, 1); - - const fixture = new ComponentFixture(TestCmpt); - expect(fixture.html).toEqual(''); - - fixture.component.value = true; - fixture.update(); - expect(fixture.html).toEqual('
content
'); - - fixture.component.value = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should add and remove DOM nodes when ng-container is a child of an embedded view (JS block)', - () => { - /** - * {% if (value) { %} - * content - * {% } %} - */ - const TestCmpt = - createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - Δcontainer(0); - } - if (rf & RenderFlags.Update) { - ΔcontainerRefreshStart(0); - if (ctx.value) { - let rf1 = ΔembeddedViewStart(0, 2, 0); - { - if (rf1 & RenderFlags.Create) { - ΔelementContainerStart(0); - { Δtext(1, 'content'); } - ΔelementContainerEnd(); - } - } - ΔembeddedViewEnd(); - } - ΔcontainerRefreshEnd(); - } - }, 1); - - const fixture = new ComponentFixture(TestCmpt); - expect(fixture.html).toEqual(''); - - fixture.component.value = true; - fixture.update(); - expect(fixture.html).toEqual('content'); - - fixture.component.value = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should add and remove DOM nodes when ng-container is a child of an embedded view (ViewContainerRef)', - () => { - - function ngIfTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementContainerStart(0); - { Δtext(1, 'content'); } - ΔelementContainerEnd(); - } - } - - /** - * content - */ - // equivalent to: - /** - * - * - * content - * - * - */ - const TestCmpt = - createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - Δtemplate( - 0, ngIfTemplate, 2, 0, 'ng-template', [AttributeMarker.Bindings, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'ngIf', Δbind(ctx.value)); - } - }, 1, 1, [NgIf]); - - const fixture = new ComponentFixture(TestCmpt); - expect(fixture.html).toEqual(''); - - fixture.component.value = true; - fixture.update(); - expect(fixture.html).toEqual('content'); - - fixture.component.value = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - // https://stackblitz.com/edit/angular-tfhcz1?file=src%2Fapp%2Fapp.component.ts - it('should add and remove DOM nodes when ng-container is a child of a delayed embedded view', - () => { - - class TestDirective { - constructor(private _tplRef: TemplateRef, private _vcRef: ViewContainerRef) {} - - createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); } - - clear() { this._vcRef.clear(); } - - static ngDirectiveDef = ΔdefineDirective({ - type: TestDirective, - selectors: [['', 'testDirective', '']], - factory: () => testDirective = new TestDirective( - ΔdirectiveInject(TemplateRef as any), - ΔdirectiveInject(ViewContainerRef as any)), - }); - } - - - function embeddedTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementContainerStart(0); - { Δtext(1, 'content'); } - ΔelementContainerEnd(); - } - } - - let testDirective: TestDirective; - - - ` - - content - - `; - const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - Δtemplate( - 0, embeddedTemplate, 2, 0, 'ng-template', - [AttributeMarker.Bindings, 'testDirective']); - } - }, 1, 0, [TestDirective]); - - const fixture = new ComponentFixture(TestCmpt); - expect(fixture.html).toEqual(''); - - testDirective !.createAndInsert(); - fixture.update(); - expect(fixture.html).toEqual('content'); - - testDirective !.clear(); - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should render at the component view root', () => { - /** - * component template - */ - const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - ΔelementContainerStart(0); - { Δtext(1, 'component template'); } - ΔelementContainerEnd(); - } - }, 2); - - function App() { Δelement(0, 'test-cmpt'); } - - const fixture = new TemplateFixture(App, () => {}, 1, 0, [TestCmpt]); - expect(fixture.html).toEqual('component template'); - }); - - it('should render inside another ng-container', () => { - /** - * - * - * - * content - * - * - * - */ - const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - ΔelementContainerStart(0); - { - ΔelementContainerStart(1); - { - ΔelementContainerStart(2); - { Δtext(3, 'content'); } - ΔelementContainerEnd(); - } - ΔelementContainerEnd(); - } - ΔelementContainerEnd(); - } - }, 4); - - function App() { Δelement(0, 'test-cmpt'); } - - const fixture = new TemplateFixture(App, () => {}, 1, 0, [TestCmpt]); - expect(fixture.html).toEqual('content'); - }); - - it('should render inside another ng-container at the root of a delayed view', () => { - let testDirective: TestDirective; - - class TestDirective { - constructor(private _tplRef: TemplateRef, private _vcRef: ViewContainerRef) {} - - createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); } - - clear() { this._vcRef.clear(); } - - static ngDirectiveDef = ΔdefineDirective({ - type: TestDirective, - selectors: [['', 'testDirective', '']], - factory: - () => testDirective = new TestDirective( - ΔdirectiveInject(TemplateRef as any), ΔdirectiveInject(ViewContainerRef as any)), - }); - } - - - function embeddedTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementContainerStart(0); - { - ΔelementContainerStart(1); - { - ΔelementContainerStart(2); - { Δtext(3, 'content'); } - ΔelementContainerEnd(); - } - ΔelementContainerEnd(); - } - ΔelementContainerEnd(); - } - } - - /** - * - * - * - * - * content - * - * - * - * - */ - const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - Δtemplate( - 0, embeddedTemplate, 4, 0, 'ng-template', - [AttributeMarker.Bindings, 'testDirective']); - } - }, 1, 0, [TestDirective]); - - function App() { Δelement(0, 'test-cmpt'); } - - const fixture = new ComponentFixture(TestCmpt); - expect(fixture.html).toEqual(''); - - testDirective !.createAndInsert(); - fixture.update(); - expect(fixture.html).toEqual('content'); - - testDirective !.createAndInsert(); - fixture.update(); - expect(fixture.html).toEqual('contentcontent'); - - testDirective !.clear(); - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should support directives and inject ElementRef', () => { - - class Directive { - constructor(public elRef: ElementRef) {} - - static ngDirectiveDef = ΔdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => directive = new Directive(ΔdirectiveInject(ElementRef)), - }); - } - - let directive: Directive; - - /** - *
- */ - function Template() { - ΔelementStart(0, 'div'); - { - ΔelementContainerStart(1, [AttributeMarker.Bindings, 'dir']); - ΔelementContainerEnd(); - } - ΔelementEnd(); - } - - const fixture = new TemplateFixture(Template, () => {}, 2, 0, [Directive]); - expect(fixture.html).toEqual('
'); - expect(directive !.elRef.nativeElement.nodeType).toBe(Node.COMMENT_NODE); - }); - - it('should support ViewContainerRef when ng-container is at the root of a view', () => { - - function ContentTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δtext(0, 'Content'); - } - } - - class Directive { - contentTpl: TemplateRef<{}>|null = null; - - constructor(private _vcRef: ViewContainerRef) {} - - insertView() { this._vcRef.createEmbeddedView(this.contentTpl as TemplateRef<{}>); } - - clear() { this._vcRef.clear(); } - - static ngDirectiveDef = ΔdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => directive = new Directive(ΔdirectiveInject(ViewContainerRef as any)), - inputs: {contentTpl: 'contentTpl'}, - }); - } - - let directive: Directive; - - /** - * - * Content - * - */ - const App = createComponent('app', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - ΔelementContainerStart(0, [AttributeMarker.Bindings, 'dir']); - Δtemplate( - 1, ContentTemplate, 1, 0, 'ng-template', null, ['content', ''], - ΔtemplateRefExtractor); - ΔelementContainerEnd(); - } - if (rf & RenderFlags.Update) { - const content = Δreference(2) as any; - ΔelementProperty(0, 'contentTpl', Δbind(content)); - } - }, 3, 1, [Directive]); - - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(''); - - directive !.insertView(); - fixture.update(); - expect(fixture.html).toEqual('Content'); - - directive !.clear(); - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should support ViewContainerRef on inside ', () => { - function ContentTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δtext(0, 'Content'); - } - } - - class Directive { - constructor(private _tplRef: TemplateRef<{}>, private _vcRef: ViewContainerRef) {} - - insertView() { this._vcRef.createEmbeddedView(this._tplRef); } - - clear() { this._vcRef.clear(); } - - static ngDirectiveDef = ΔdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: - () => directive = new Directive( - ΔdirectiveInject(TemplateRef as any), ΔdirectiveInject(ViewContainerRef as any)), - }); - } - - let directive: Directive; - - /** - * - * Content - * - */ - const App = createComponent('app', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - ΔelementContainerStart(0); - Δtemplate( - 1, ContentTemplate, 1, 0, 'ng-template', [AttributeMarker.Bindings, 'dir'], [], - ΔtemplateRefExtractor); - ΔelementContainerEnd(); - } - }, 2, 0, [Directive]); - - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(''); - - directive !.insertView(); - fixture.update(); - expect(fixture.html).toEqual('Content'); - - directive !.clear(); - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should not set any attributes', () => { - /** - *
- */ - function Template() { - ΔelementStart(0, 'div'); - { - ΔelementContainerStart(1, ['id', 'foo']); - ΔelementContainerEnd(); - } - ΔelementEnd(); - } - - const fixture = new TemplateFixture(Template, () => {}, 2); - expect(fixture.html).toEqual('
'); - }); - }); describe('tree', () => { @@ -1232,920 +194,6 @@ describe('render3 integration test', () => { }); - describe('element bindings', () => { - - describe('elementAttribute', () => { - it('should support attribute bindings', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'span'); - } - if (rf & RenderFlags.Update) { - ΔelementAttribute(0, 'title', Δbind(ctx.title)); - } - }, 1, 1); - - const fixture = new ComponentFixture(App); - fixture.component.title = 'Hello'; - fixture.update(); - // initial binding - expect(fixture.html).toEqual(''); - - // update binding - fixture.component.title = 'Hi!'; - fixture.update(); - expect(fixture.html).toEqual(''); - - // remove attribute - fixture.component.title = null; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should stringify values used attribute bindings', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'span'); - } - if (rf & RenderFlags.Update) { - ΔelementAttribute(0, 'title', Δbind(ctx.title)); - } - }, 1, 1); - - const fixture = new ComponentFixture(App); - fixture.component.title = NaN; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.title = {toString: () => 'Custom toString'}; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should update bindings', () => { - function Template(rf: RenderFlags, c: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'b'); - } - if (rf & RenderFlags.Update) { - ΔelementAttribute(0, 'a', ΔinterpolationV(c)); - ΔelementAttribute(0, 'a0', Δbind(c[1])); - ΔelementAttribute(0, 'a1', Δinterpolation1(c[0], c[1], c[16])); - ΔelementAttribute(0, 'a2', Δinterpolation2(c[0], c[1], c[2], c[3], c[16])); - ΔelementAttribute(0, 'a3', Δinterpolation3(c[0], c[1], c[2], c[3], c[4], c[5], c[16])); - ΔelementAttribute( - 0, 'a4', Δinterpolation4(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[16])); - ΔelementAttribute( - 0, 'a5', - Δinterpolation5(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[16])); - ΔelementAttribute( - 0, 'a6', Δinterpolation6( - c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], - c[11], c[16])); - ΔelementAttribute( - 0, 'a7', Δinterpolation7( - c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], - c[11], c[12], c[13], c[16])); - ΔelementAttribute( - 0, 'a8', Δinterpolation8( - c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], - c[11], c[12], c[13], c[14], c[15], c[16])); - } - } - let args = ['(', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e', 5, 'f', 6, 'g', 7, ')']; - expect(renderToHtml(Template, args, 1, 54)) - .toEqual( - ''); - args = args.reverse(); - expect(renderToHtml(Template, args, 1, 54)) - .toEqual( - ''); - args = args.reverse(); - expect(renderToHtml(Template, args, 1, 54)) - .toEqual( - ''); - }); - - it('should not update DOM if context has not changed', () => { - const ctx: {title: string | null} = {title: 'Hello'}; - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'span'); - Δcontainer(1); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - ΔelementAttribute(0, 'title', Δbind(ctx.title)); - ΔcontainerRefreshStart(1); - { - if (true) { - let rf1 = ΔembeddedViewStart(1, 1, 1); - { - if (rf1 & RenderFlags.Create) { - ΔelementStart(0, 'b'); - {} - ΔelementEnd(); - } - if (rf1 & RenderFlags.Update) { - ΔelementAttribute(0, 'title', Δbind(ctx.title)); - } - } - ΔembeddedViewEnd(); - } - } - ΔcontainerRefreshEnd(); - } - }, 2, 1); - - const fixture = new ComponentFixture(App); - fixture.component.title = 'Hello'; - fixture.update(); - // initial binding - expect(fixture.html).toEqual(''); - // update DOM manually - fixture.hostElement.querySelector('b') !.setAttribute('title', 'Goodbye'); - - // refresh with same binding - fixture.update(); - expect(fixture.html).toEqual(''); - - // refresh again with same binding - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should support host attribute bindings', () => { - let hostBindingDir: HostBindingDir; - - class HostBindingDir { - /* @HostBinding('attr.aria-label') */ - label = 'some label'; - - static ngDirectiveDef = ΔdefineDirective({ - type: HostBindingDir, - selectors: [['', 'hostBindingDir', '']], - factory: function HostBindingDir_Factory() { - return hostBindingDir = new HostBindingDir(); - }, - hostBindings: function HostBindingDir_HostBindings( - rf: RenderFlags, ctx: any, elIndex: number) { - if (rf & RenderFlags.Create) { - ΔallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ΔelementAttribute(elIndex, 'aria-label', Δbind(ctx.label)); - } - } - }); - } - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'div', ['hostBindingDir', '']); - } - }, 1, 0, [HostBindingDir]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(`
`); - - hostBindingDir !.label = 'other label'; - fixture.update(); - expect(fixture.html).toEqual(`
`); - }); - }); - - describe('elementStyle', () => { - it('should support binding to styles', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'span'); - Δstyling(null, ['border-color']); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔstyleProp(0, ctx.color); - ΔstylingApply(); - } - }, 1); - - const fixture = new ComponentFixture(App); - fixture.component.color = 'red'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.color = 'green'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.color = null; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should support binding to styles with suffix', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'span'); - Δstyling(null, ['font-size']); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔstyleProp(0, ctx.time, 'px'); - ΔstylingApply(); - } - }, 1); - - const fixture = new ComponentFixture(App); - fixture.component.time = '100'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.time = 200; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.time = 0; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.time = null; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - }); - - describe('class-based styling', () => { - it('should support CSS class toggle', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'span'); - Δstyling(['active']); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔclassProp(0, ctx.class); - ΔstylingApply(); - } - }, 1); - - const fixture = new ComponentFixture(App); - fixture.component.class = true; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.class = false; - fixture.update(); - expect(fixture.html).toEqual(''); - - // truthy values - fixture.component.class = 'a_string'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.class = 10; - fixture.update(); - expect(fixture.html).toEqual(''); - - // falsy values - fixture.component.class = ''; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.class = 0; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should work correctly with existing static classes', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'span', [AttributeMarker.Classes, 'existing']); - Δstyling(['existing', 'active']); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔclassProp(1, ctx.class); - ΔstylingApply(); - } - }, 1); - - const fixture = new ComponentFixture(App); - fixture.component.class = true; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.class = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should apply classes properly when nodes are components', () => { - const MyComp = createComponent('my-comp', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - Δtext(0, 'Comp Content'); - } - }, 1, 0, []); - - /** - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'my-comp'); - Δstyling(['active']); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔclassProp(0, ctx.class); - ΔstylingApply(); - } - }, 1, 0, [MyComp]); - - const fixture = new ComponentFixture(App); - fixture.component.class = true; - fixture.update(); - expect(fixture.html).toEqual('Comp Content'); - - fixture.component.class = false; - fixture.update(); - expect(fixture.html).toEqual('Comp Content'); - }); - - it('should apply classes properly when nodes have LContainers', () => { - let structuralComp !: StructuralComp; - - class StructuralComp { - tmp !: TemplateRef; - - constructor(public vcr: ViewContainerRef) {} - - create() { this.vcr.createEmbeddedView(this.tmp); } - - static ngComponentDef = ΔdefineComponent({ - type: StructuralComp, - selectors: [['structural-comp']], - factory: () => structuralComp = - new StructuralComp(ΔdirectiveInject(ViewContainerRef as any)), - inputs: {tmp: 'tmp'}, - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: StructuralComp) => { - if (rf & RenderFlags.Create) { - Δtext(0, 'Comp Content'); - } - } - }); - } - - function FooTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δtext(0, 'Temp Content'); - } - } - - /** - * - * Temp Content - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δtemplate( - 0, FooTemplate, 1, 0, 'ng-template', null, ['foo', ''], ΔtemplateRefExtractor); - ΔelementStart(2, 'structural-comp'); - Δstyling(['active']); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - const foo = Δreference(1) as any; - Δselect(2); - ΔclassProp(0, ctx.class); - ΔstylingApply(); - ΔelementProperty(2, 'tmp', Δbind(foo)); - } - }, 3, 1, [StructuralComp]); - - const fixture = new ComponentFixture(App); - fixture.component.class = true; - fixture.update(); - expect(fixture.html) - .toEqual('Comp Content'); - - structuralComp.create(); - fixture.update(); - expect(fixture.html) - .toEqual('Comp ContentTemp Content'); - - fixture.component.class = false; - fixture.update(); - expect(fixture.html) - .toEqual('Comp ContentTemp Content'); - }); - - let mockClassDirective: DirWithClassDirective; - class DirWithClassDirective { - static ngDirectiveDef = ΔdefineDirective({ - type: DirWithClassDirective, - selectors: [['', 'DirWithClass', '']], - factory: () => mockClassDirective = new DirWithClassDirective(), - inputs: {'klass': 'class'} - }); - - public classesVal: string = ''; - set klass(value: string) { this.classesVal = value; } - } - - let mockStyleDirective: DirWithStyleDirective; - class DirWithStyleDirective { - static ngDirectiveDef = ΔdefineDirective({ - type: DirWithStyleDirective, - selectors: [['', 'DirWithStyle', '']], - factory: () => mockStyleDirective = new DirWithStyleDirective(), - inputs: {'style': 'style'} - }); - - public stylesVal: string = ''; - set style(value: string) { this.stylesVal = value; } - } - - it('should delegate initial classes to a [class] input binding if present on a directive on the same element', - () => { - /** - *
- */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart( - 0, 'div', - ['DirWithClass', '', AttributeMarker.Classes, 'apple', 'orange', 'banana']); - Δstyling(); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔstylingApply(); - } - }, 1, 0, [DirWithClassDirective]); - - const fixture = new ComponentFixture(App); - expect(mockClassDirective !.classesVal).toEqual('apple orange banana'); - }); - - it('should delegate initial styles to a [style] input binding if present on a directive on the same element', - () => { - /** - *
- */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'div', [ - 'DirWithStyle', '', AttributeMarker.Styles, 'width', '100px', 'height', '200px' - ]); - Δstyling(); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔstylingApply(); - } - }, 1, 0, [DirWithStyleDirective]); - - const fixture = new ComponentFixture(App); - expect(mockStyleDirective !.stylesVal).toEqual('width:100px;height:200px'); - }); - - it('should update `[class]` and bindings in the provided directive if the input is matched', - () => { - /** - *
- */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'div', ['DirWithClass']); - Δstyling(); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔclassMap('cucumber grape'); - ΔstylingApply(); - } - }, 1, 0, [DirWithClassDirective]); - - const fixture = new ComponentFixture(App); - expect(mockClassDirective !.classesVal).toEqual('cucumber grape'); - }); - - it('should update `[style]` and bindings in the provided directive if the input is matched', - () => { - /** - *
- */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'div', ['DirWithStyle']); - Δstyling(); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔstyleMap({width: '200px', height: '500px'}); - ΔstylingApply(); - } - }, 1, 0, [DirWithStyleDirective]); - - const fixture = new ComponentFixture(App); - expect(mockStyleDirective !.stylesVal).toEqual('width:200px;height:500px'); - }); - - it('should apply initial styling to the element that contains the directive with host styling', - () => { - class DirWithInitialStyling { - static ngDirectiveDef = ΔdefineDirective({ - type: DirWithInitialStyling, - selectors: [['', 'DirWithInitialStyling', '']], - factory: () => new DirWithInitialStyling(), - hostBindings: function( - rf: RenderFlags, ctx: DirWithInitialStyling, elementIndex: number) { - if (rf & RenderFlags.Create) { - ΔelementHostAttrs([ - 'title', 'foo', AttributeMarker.Classes, 'heavy', 'golden', - AttributeMarker.Styles, 'color', 'purple', 'font-weight', 'bold' - ]); - } - } - }); - - public classesVal: string = ''; - } - - /** - *
- */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'div', [ - 'DirWithInitialStyling', '', AttributeMarker.Classes, 'big', - AttributeMarker.Styles, 'color', 'black', 'font-size', '200px' - ]); - } - }, 1, 0, [DirWithInitialStyling]); - - const fixture = new ComponentFixture(App); - const target = fixture.hostElement.querySelector('div') !; - const classes = target.getAttribute('class') !.split(/\s+/).sort(); - expect(classes).toEqual(['big', 'golden', 'heavy']); - - expect(target.getAttribute('title')).toEqual('foo'); - expect(target.style.getPropertyValue('color')).toEqual('black'); - expect(target.style.getPropertyValue('font-size')).toEqual('200px'); - expect(target.style.getPropertyValue('font-weight')).toEqual('bold'); - }); - - it('should apply single styling bindings present within a directive onto the same element and defer the element\'s initial styling values when missing', - () => { - let dirInstance: DirWithSingleStylingBindings; - /** - * - */ - class DirWithSingleStylingBindings { - static ngDirectiveDef = ΔdefineDirective({ - type: DirWithSingleStylingBindings, - selectors: [['', 'DirWithSingleStylingBindings', '']], - factory: () => dirInstance = new DirWithSingleStylingBindings(), - hostBindings: function( - rf: RenderFlags, ctx: DirWithSingleStylingBindings, elementIndex: number) { - if (rf & RenderFlags.Create) { - ΔelementHostAttrs( - [AttributeMarker.Classes, 'def', AttributeMarker.Styles, 'width', '555px']); - Δstyling(['xyz'], ['width', 'height']); - } - if (rf & RenderFlags.Update) { - ΔstyleProp(0, ctx.width); - ΔstyleProp(1, ctx.height); - ΔclassProp(0, ctx.activateXYZClass); - ΔstylingApply(); - } - } - }); - - width: null|string = null; - height: null|string = null; - activateXYZClass: boolean = false; - } - - /** - *
- */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'div', [ - 'DirWithSingleStylingBindings', '', AttributeMarker.Classes, 'abc', - AttributeMarker.Styles, 'width', '100px', 'height', '200px' - ]); - } - }, 1, 0, [DirWithSingleStylingBindings]); - - const fixture = new ComponentFixture(App); - const target = fixture.hostElement.querySelector('div') !; - expect(target.style.getPropertyValue('width')).toEqual('100px'); - expect(target.style.getPropertyValue('height')).toEqual('200px'); - expect(target.classList.contains('abc')).toBeTruthy(); - expect(target.classList.contains('def')).toBeTruthy(); - expect(target.classList.contains('xyz')).toBeFalsy(); - - dirInstance !.width = '444px'; - dirInstance !.height = '999px'; - dirInstance !.activateXYZClass = true; - fixture.update(); - - expect(target.style.getPropertyValue('width')).toEqual('444px'); - expect(target.style.getPropertyValue('height')).toEqual('999px'); - expect(target.classList.contains('abc')).toBeTruthy(); - expect(target.classList.contains('def')).toBeTruthy(); - expect(target.classList.contains('xyz')).toBeTruthy(); - - dirInstance !.width = null; - dirInstance !.height = null; - fixture.update(); - - expect(target.style.getPropertyValue('width')).toEqual('100px'); - expect(target.style.getPropertyValue('height')).toEqual('200px'); - expect(target.classList.contains('abc')).toBeTruthy(); - expect(target.classList.contains('def')).toBeTruthy(); - expect(target.classList.contains('xyz')).toBeTruthy(); - }); - - it('should properly prioritize single style binding collisions when they exist on multiple directives', - () => { - let dir1Instance: Dir1WithStyle; - /** - * Directive with host props: - * [style.width] - */ - class Dir1WithStyle { - static ngDirectiveDef = ΔdefineDirective({ - type: Dir1WithStyle, - selectors: [['', 'Dir1WithStyle', '']], - factory: () => dir1Instance = new Dir1WithStyle(), - hostBindings: function(rf: RenderFlags, ctx: Dir1WithStyle, elementIndex: number) { - if (rf & RenderFlags.Create) { - Δstyling(null, ['width']); - } - if (rf & RenderFlags.Update) { - ΔstyleProp(0, ctx.width); - ΔstylingApply(); - } - } - }); - width: null|string = null; - } - - let dir2Instance: Dir2WithStyle; - /** - * Directive with host props: - * [style.width] - * style="width:111px" - */ - class Dir2WithStyle { - static ngDirectiveDef = ΔdefineDirective({ - type: Dir2WithStyle, - selectors: [['', 'Dir2WithStyle', '']], - factory: () => dir2Instance = new Dir2WithStyle(), - hostBindings: function(rf: RenderFlags, ctx: Dir2WithStyle, elementIndex: number) { - if (rf & RenderFlags.Create) { - ΔelementHostAttrs([AttributeMarker.Styles, 'width', '111px']); - Δstyling(null, ['width']); - } - if (rf & RenderFlags.Update) { - ΔstyleProp(0, ctx.width); - ΔstylingApply(); - } - } - }); - width: null|string = null; - } - - /** - * Component with the following template: - *
- */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'div', ['Dir1WithStyle', '', 'Dir2WithStyle', '']); - Δstyling(null, ['width']); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔstyleProp(0, ctx.width); - ΔstylingApply(); - } - }, 1, 0, [Dir1WithStyle, Dir2WithStyle]); - - const fixture = new ComponentFixture(App); - const target = fixture.hostElement.querySelector('div') !; - expect(target.style.getPropertyValue('width')).toEqual('111px'); - - fixture.component.width = '999px'; - dir1Instance !.width = '222px'; - dir2Instance !.width = '333px'; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('999px'); - - fixture.component.width = null; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('222px'); - - dir1Instance !.width = null; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('333px'); - - dir2Instance !.width = null; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('111px'); - - dir1Instance !.width = '666px'; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('666px'); - - fixture.component.width = '777px'; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('777px'); - }); - - it('should properly prioritize multi style binding collisions when they exist on multiple directives', - () => { - let dir1Instance: Dir1WithStyling; - /** - * Directive with host props: - * [style] - * [class] - */ - class Dir1WithStyling { - static ngDirectiveDef = ΔdefineDirective({ - type: Dir1WithStyling, - selectors: [['', 'Dir1WithStyling', '']], - factory: () => dir1Instance = new Dir1WithStyling(), - hostBindings: function(rf: RenderFlags, ctx: Dir1WithStyling, elementIndex: number) { - if (rf & RenderFlags.Create) { - Δstyling(); - } - if (rf & RenderFlags.Update) { - ΔstyleMap(ctx.stylesExp); - ΔclassMap(ctx.classesExp); - ΔstylingApply(); - } - } - }); - - classesExp: any = {}; - stylesExp: any = {}; - } - - let dir2Instance: Dir2WithStyling; - /** - * Directive with host props: - * [style] - * style="width:111px" - */ - class Dir2WithStyling { - static ngDirectiveDef = ΔdefineDirective({ - type: Dir2WithStyling, - selectors: [['', 'Dir2WithStyling', '']], - factory: () => dir2Instance = new Dir2WithStyling(), - hostBindings: function(rf: RenderFlags, ctx: Dir2WithStyling, elementIndex: number) { - if (rf & RenderFlags.Create) { - ΔelementHostAttrs([AttributeMarker.Styles, 'width', '111px']); - Δstyling(); - } - if (rf & RenderFlags.Update) { - ΔstyleMap(ctx.stylesExp); - ΔstylingApply(); - } - } - }); - - stylesExp: any = {}; - } - - /** - * Component with the following template: - *
- */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'div', ['Dir1WithStyling', '', 'Dir2WithStyling', '']); - Δstyling(); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔstyleMap(ctx.stylesExp); - ΔclassMap(ctx.classesExp); - ΔstylingApply(); - } - }, 1, 0, [Dir1WithStyling, Dir2WithStyling]); - - const fixture = new ComponentFixture(App); - const target = fixture.hostElement.querySelector('div') !; - expect(target.style.getPropertyValue('width')).toEqual('111px'); - - const compInstance = fixture.component; - compInstance.stylesExp = {width: '999px', height: null}; - compInstance.classesExp = {one: true, two: false}; - dir1Instance !.stylesExp = {width: '222px'}; - dir1Instance !.classesExp = {two: true, three: false}; - dir2Instance !.stylesExp = {width: '333px', height: '100px'}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('999px'); - expect(target.style.getPropertyValue('height')).toEqual('100px'); - expect(target.classList.contains('one')).toBeTruthy(); - expect(target.classList.contains('two')).toBeFalsy(); - expect(target.classList.contains('three')).toBeFalsy(); - - compInstance.stylesExp = {}; - compInstance !.classesExp = {}; - dir1Instance !.stylesExp = {width: '222px', height: '200px'}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('222px'); - expect(target.style.getPropertyValue('height')).toEqual('200px'); - expect(target.classList.contains('one')).toBeFalsy(); - expect(target.classList.contains('two')).toBeTruthy(); - expect(target.classList.contains('three')).toBeFalsy(); - - dir1Instance !.stylesExp = {}; - dir1Instance !.classesExp = {}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('333px'); - expect(target.style.getPropertyValue('height')).toEqual('100px'); - expect(target.classList.contains('one')).toBeFalsy(); - expect(target.classList.contains('two')).toBeFalsy(); - expect(target.classList.contains('three')).toBeFalsy(); - - dir2Instance !.stylesExp = {}; - compInstance.stylesExp = {height: '900px'}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('111px'); - expect(target.style.getPropertyValue('height')).toEqual('900px'); - - dir1Instance !.stylesExp = {width: '666px', height: '600px'}; - dir1Instance !.classesExp = {four: true, one: true}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('666px'); - expect(target.style.getPropertyValue('height')).toEqual('900px'); - expect(target.classList.contains('one')).toBeTruthy(); - expect(target.classList.contains('two')).toBeFalsy(); - expect(target.classList.contains('three')).toBeFalsy(); - expect(target.classList.contains('four')).toBeTruthy(); - - compInstance.stylesExp = {width: '777px'}; - compInstance.classesExp = {four: false}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('777px'); - expect(target.style.getPropertyValue('height')).toEqual('600px'); - expect(target.classList.contains('one')).toBeTruthy(); - expect(target.classList.contains('two')).toBeFalsy(); - expect(target.classList.contains('three')).toBeFalsy(); - expect(target.classList.contains('four')).toBeFalsy(); - }); - }); - - it('should properly handle and render interpolation for class attribute bindings', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'div'); - Δstyling(); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - Δselect(0); - ΔclassMap(Δinterpolation2('-', ctx.name, '-', ctx.age, '-')); - ΔstylingApply(); - } - }, 1, 2); - - const fixture = new ComponentFixture(App); - const target = fixture.hostElement.querySelector('div') !; - expect(target.classList.contains('-fred-36-')).toBeFalsy(); - - fixture.component.name = 'fred'; - fixture.component.age = '36'; - fixture.update(); - - expect(target.classList.contains('-fred-36-')).toBeTruthy(); - }); - }); }); describe('template data', () => {