diff --git a/packages/core/test/acceptance/styling_next_spec.ts b/packages/core/test/acceptance/styling_next_spec.ts deleted file mode 100644 index fbc5f01d1c..0000000000 --- a/packages/core/test/acceptance/styling_next_spec.ts +++ /dev/null @@ -1,1362 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import {Component, ComponentFactoryResolver, ComponentRef, Directive, HostBinding, Input, NgModule, ViewChild, ViewContainerRef} from '@angular/core'; -import {DebugNode, LViewDebug, toDebug} from '@angular/core/src/render3/instructions/lview_debug'; -import {loadLContextFromNode} from '@angular/core/src/render3/util/discovery_utils'; -import {ngDevModeResetPerfCounters as resetStylingCounters} from '@angular/core/src/util/ng_dev_mode'; -import {TestBed} from '@angular/core/testing'; -import {DomSanitizer, SafeStyle} from '@angular/platform-browser'; -import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {onlyInIvy} from '@angular/private/testing'; - -describe('new styling integration', () => { - beforeEach(() => resetStylingCounters()); - - onlyInIvy('ivy resolves styling across directives, components and templates in unison') - .it('should apply single property styles/classes to the element and default to any static styling values', - () => { - @Component({ - template: ` -
- ` - }) - class Cmp { - w: string|null = '100px'; - h: string|null = '100px'; - o: string|null = '0.5'; - abc = true; - xyz = false; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - expect(element.style.width).toEqual('100px'); - expect(element.style.height).toEqual('100px'); - expect(element.style.opacity).toEqual('0.5'); - expect(element.classList.contains('abc')).toBeTruthy(); - expect(element.classList.contains('xyz')).toBeFalsy(); - - fixture.componentInstance.w = null; - fixture.componentInstance.h = null; - fixture.componentInstance.o = null; - fixture.componentInstance.abc = false; - fixture.componentInstance.xyz = true; - fixture.detectChanges(); - - expect(element.style.width).toEqual('200px'); - expect(element.style.height).toEqual('200px'); - expect(element.style.opacity).toBeFalsy(); - expect(element.classList.contains('abc')).toBeFalsy(); - expect(element.classList.contains('xyz')).toBeTruthy(); - }); - - onlyInIvy('ivy resolves styling across directives, components and templates in unison') - .it('should apply single style/class across the template and directive host bindings', () => { - @Directive({selector: '[dir-that-sets-width]'}) - class DirThatSetsWidthDirective { - @Input('dir-that-sets-width') @HostBinding('style.width') public width: string = ''; - } - - @Directive({selector: '[another-dir-that-sets-width]', host: {'[style.width]': 'width'}}) - class AnotherDirThatSetsWidthDirective { - @Input('another-dir-that-sets-width') public width: string = ''; - } - - @Component({ - template: ` -
- ` - }) - class Cmp { - w0: string|null = null; - w1: string|null = null; - w2: string|null = null; - } - - TestBed.configureTestingModule( - {declarations: [Cmp, DirThatSetsWidthDirective, AnotherDirThatSetsWidthDirective]}); - const fixture = TestBed.createComponent(Cmp); - fixture.componentInstance.w0 = '100px'; - fixture.componentInstance.w1 = '200px'; - fixture.componentInstance.w2 = '300px'; - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - expect(element.style.width).toEqual('100px'); - - fixture.componentInstance.w0 = null; - fixture.detectChanges(); - - expect(element.style.width).toEqual('200px'); - - fixture.componentInstance.w1 = null; - fixture.detectChanges(); - - expect(element.style.width).toEqual('300px'); - - fixture.componentInstance.w2 = null; - fixture.detectChanges(); - - expect(element.style.width).toBeFalsy(); - - fixture.componentInstance.w2 = '400px'; - fixture.detectChanges(); - - expect(element.style.width).toEqual('400px'); - - fixture.componentInstance.w1 = '500px'; - fixture.componentInstance.w0 = '600px'; - fixture.detectChanges(); - - expect(element.style.width).toEqual('600px'); - }); - - onlyInIvy('ivy resolves styling across directives, components and templates in unison') - .it('should only run stylingFlush once when there are no collisions between styling properties', - () => { - @Directive({selector: '[dir-with-styling]'}) - class DirWithStyling { - @HostBinding('style.font-size') public fontSize = '100px'; - } - - @Component({selector: 'comp-with-styling'}) - class CompWithStyling { - @HostBinding('style.width') public width = '900px'; - - @HostBinding('style.height') public height = '900px'; - } - - @Component({ - template: ` - ... - ` - }) - class Cmp { - opacity: string|null = '0.5'; - @ViewChild(CompWithStyling, {static: true}) - compWithStyling: CompWithStyling|null = null; - @ViewChild(DirWithStyling, {static: true}) dirWithStyling: DirWithStyling|null = null; - } - - TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]}); - const fixture = TestBed.createComponent(Cmp); - fixture.detectChanges(); - - const component = fixture.componentInstance; - const element = fixture.nativeElement.querySelector('comp-with-styling'); - const node = getDebugNode(element) !; - - const styles = node.styles !; - const config = styles.config; - expect(config.hasCollisions).toBeFalsy(); - expect(config.hasMapBindings).toBeFalsy(); - expect(config.hasPropBindings).toBeTruthy(); - expect(config.allowDirectStyling).toBeTruthy(); - - expect(element.style.opacity).toEqual('0.5'); - expect(element.style.width).toEqual('900px'); - expect(element.style.height).toEqual('900px'); - expect(element.style.fontSize).toEqual('100px'); - - // once for the template flush and again for the host bindings - expect(ngDevMode !.flushStyling).toEqual(2); - resetStylingCounters(); - - component.opacity = '0.6'; - component.compWithStyling !.height = '100px'; - component.compWithStyling !.width = '100px'; - component.dirWithStyling !.fontSize = '50px'; - fixture.detectChanges(); - - expect(element.style.opacity).toEqual('0.6'); - expect(element.style.width).toEqual('100px'); - expect(element.style.height).toEqual('100px'); - expect(element.style.fontSize).toEqual('50px'); - - // there is no need to flush styling since the styles are applied directly - expect(ngDevMode !.flushStyling).toEqual(0); - }); - - onlyInIvy('ivy resolves styling across directives, components and templates in unison') - .it('should combine all styling across the template, directive and component host bindings', - () => { - @Directive({selector: '[dir-with-styling]'}) - class DirWithStyling { - @HostBinding('style.color') public color = 'red'; - - @HostBinding('style.font-size') public fontSize = '100px'; - - @HostBinding('class.dir') public dirClass = true; - } - - @Component({selector: 'comp-with-styling'}) - class CompWithStyling { - @HostBinding('style.width') public width = '900px'; - - @HostBinding('style.height') public height = '900px'; - - @HostBinding('class.comp') public compClass = true; - } - - @Component({ - template: ` - ... - ` - }) - class Cmp { - opacity: string|null = '0.5'; - width: string|null = 'auto'; - tplClass = true; - } - - TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]}); - const fixture = TestBed.createComponent(Cmp); - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('comp-with-styling'); - - const node = getDebugNode(element) !; - const styles = node.styles !; - const classes = node.classes !; - - expect(styles.values).toEqual({ - 'color': 'red', - 'width': 'auto', - 'opacity': '0.5', - 'height': '900px', - 'font-size': '100px' - }); - expect(classes.values).toEqual({ - 'dir': true, - 'comp': true, - 'tpl': true, - }); - - fixture.componentInstance.width = null; - fixture.componentInstance.opacity = null; - fixture.componentInstance.tplClass = false; - fixture.detectChanges(); - - expect(styles.values).toEqual({ - 'color': 'red', - 'width': '900px', - 'opacity': null, - 'height': '900px', - 'font-size': '100px' - }); - expect(classes.values).toEqual({ - 'dir': true, - 'comp': true, - 'tpl': false, - }); - }); - - onlyInIvy('ivy resolves styling across directives, components and templates in unison') - .it('should properly apply styling across sub and super class directive host bindings', - () => { - @Directive({selector: '[super-class-dir]'}) - class SuperClassDirective { - @HostBinding('style.width') public w1 = '100px'; - } - - @Component({selector: '[sub-class-dir]'}) - class SubClassDirective extends SuperClassDirective { - @HostBinding('style.width') public w2 = '200px'; - } - - @Component({ - template: ` -
- ` - }) - class Cmp { - w3: string|null = '300px'; - } - - TestBed.configureTestingModule( - {declarations: [Cmp, SuperClassDirective, SubClassDirective]}); - const fixture = TestBed.createComponent(Cmp); - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - - const node = getDebugNode(element) !; - const styles = node.styles !; - - expect(styles.values).toEqual({ - 'width': '300px', - }); - - fixture.componentInstance.w3 = null; - fixture.detectChanges(); - - expect(styles.values).toEqual({ - 'width': '200px', - }); - }); - - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should support situations where there are more than 32 bindings', () => { - const TOTAL_BINDINGS = 34; - - let bindingsHTML = ''; - let bindingsArr: any[] = []; - for (let i = 0; i < TOTAL_BINDINGS; i++) { - bindingsHTML += `[style.prop${i}]="bindings[${i}]" `; - bindingsArr.push(null); - } - - @Component({template: `
`}) - class Cmp { - bindings = bindingsArr; - - updateBindings(value: string) { - for (let i = 0; i < TOTAL_BINDINGS; i++) { - this.bindings[i] = value + i; - } - } - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - - let testValue = 'initial'; - fixture.componentInstance.updateBindings('initial'); - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - - const node = getDebugNode(element) !; - const styles = node.styles !; - - let values = styles.values; - let props = Object.keys(values); - expect(props.length).toEqual(TOTAL_BINDINGS); - - for (let i = 0; i < props.length; i++) { - const prop = props[i]; - const value = values[prop] as string; - const num = value.substr(testValue.length); - expect(value).toEqual(`initial${num}`); - } - - testValue = 'final'; - fixture.componentInstance.updateBindings('final'); - fixture.detectChanges(); - - values = styles.values; - props = Object.keys(values); - expect(props.length).toEqual(TOTAL_BINDINGS); - for (let i = 0; i < props.length; i++) { - const prop = props[i]; - const value = values[prop] as string; - const num = value.substr(testValue.length); - expect(value).toEqual(`final${num}`); - } - }); - - onlyInIvy('only ivy has style debugging support') - .it('should apply map-based style and class entries', () => { - @Component({template: '
'}) - class Cmp { - public c: {[key: string]: any}|null = null; - updateClasses(classes: string) { - const c = this.c || (this.c = {}); - Object.keys(this.c).forEach(className => { c[className] = false; }); - classes.split(/\s+/).forEach(className => { c[className] = true; }); - } - - public s: {[key: string]: any}|null = null; - updateStyles(prop: string, value: string|number|null) { - const s = this.s || (this.s = {}); - Object.assign(s, {[prop]: value}); - } - - reset() { - this.s = null; - this.c = null; - } - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - comp.updateStyles('width', '100px'); - comp.updateStyles('height', '200px'); - comp.updateClasses('abc'); - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - let styles = node.styles !; - let classes = node.classes !; - - let stylesSummary = styles.summary; - let widthSummary = stylesSummary['width']; - expect(widthSummary.prop).toEqual('width'); - expect(widthSummary.value).toEqual('100px'); - - let heightSummary = stylesSummary['height']; - expect(heightSummary.prop).toEqual('height'); - expect(heightSummary.value).toEqual('200px'); - - let classesSummary = classes.summary; - let abcSummary = classesSummary['abc']; - expect(abcSummary.prop).toEqual('abc'); - expect(abcSummary.value).toBeTruthy(); - - comp.reset(); - comp.updateStyles('width', '500px'); - comp.updateStyles('height', null); - comp.updateClasses('def'); - fixture.detectChanges(); - - styles = node.styles !; - classes = node.classes !; - - stylesSummary = styles.summary; - widthSummary = stylesSummary['width']; - expect(widthSummary.value).toEqual('500px'); - - heightSummary = stylesSummary['height']; - expect(heightSummary.value).toEqual(null); - - classesSummary = classes.summary; - abcSummary = classesSummary['abc']; - expect(abcSummary.prop).toEqual('abc'); - expect(abcSummary.value).toBeFalsy(); - - let defSummary = classesSummary['def']; - expect(defSummary.prop).toEqual('def'); - expect(defSummary.value).toBeTruthy(); - }); - - onlyInIvy('ivy resolves styling across directives, components and templates in unison') - .it('should resolve styling collisions across templates, directives and components for prop and map-based entries', - () => { - @Directive({selector: '[dir-that-sets-styling]'}) - class DirThatSetsStyling { - @HostBinding('style') public map: any = {color: 'red', width: '777px'}; - } - - @Component({ - template: ` -
- ` - }) - class Cmp { - map: any = {width: '111px', opacity: '0.5'}; - width: string|null = '555px'; - - @ViewChild('dir', {read: DirThatSetsStyling, static: true}) - dir !: DirThatSetsStyling; - } - - TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - - const styles = node.styles !; - - expect(styles.values).toEqual({ - 'width': '555px', - 'color': 'red', - 'font-size': '99px', - 'opacity': '0.5', - }); - - comp.width = null; - fixture.detectChanges(); - - expect(styles.values).toEqual({ - 'width': '111px', - 'color': 'red', - 'font-size': '99px', - 'opacity': '0.5', - }); - - comp.map = null; - fixture.detectChanges(); - - expect(styles.values).toEqual({ - 'width': '777px', - 'color': 'red', - 'font-size': '99px', - 'opacity': null, - }); - - comp.dir.map = null; - fixture.detectChanges(); - - expect(styles.values).toEqual({ - 'width': '200px', - 'color': null, - 'font-size': '99px', - 'opacity': null, - }); - }); - - onlyInIvy('ivy resolves styling across directives, components and templates in unison') - .it('should only apply each styling property once per CD across templates, components, directives', - () => { - @Directive({selector: '[dir-that-sets-styling]'}) - class DirThatSetsStyling { - @HostBinding('style') public map: any = {width: '999px', height: '999px'}; - } - - @Component({ - template: ` -
- ` - }) - class Cmp { - width: string|null = '111px'; - height: string|null = '111px'; - - map: any = {width: '555px', height: '555px'}; - - @ViewChild('dir', {read: DirThatSetsStyling, static: true}) - dir !: DirThatSetsStyling; - } - - TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - - resetStylingCounters(); - fixture.detectChanges(); - const element = fixture.nativeElement.querySelector('div'); - - // both are applied because this is the first pass - assertStyleCounters(2, 0); - assertStyle(element, 'width', '111px'); - assertStyle(element, 'height', '111px'); - - comp.width = '222px'; - resetStylingCounters(); - fixture.detectChanges(); - - assertStyleCounters(1, 0); - assertStyle(element, 'width', '222px'); - assertStyle(element, 'height', '111px'); - - comp.height = '222px'; - resetStylingCounters(); - fixture.detectChanges(); - - assertStyleCounters(1, 0); - assertStyle(element, 'width', '222px'); - assertStyle(element, 'height', '222px'); - - comp.width = null; - resetStylingCounters(); - fixture.detectChanges(); - - assertStyleCounters(1, 0); - assertStyle(element, 'width', '555px'); - assertStyle(element, 'height', '222px'); - - comp.width = '123px'; - comp.height = '123px'; - resetStylingCounters(); - fixture.detectChanges(); - - assertStyle(element, 'width', '123px'); - assertStyle(element, 'height', '123px'); - - comp.map = {}; - resetStylingCounters(); - fixture.detectChanges(); - - // both are applied because the map was altered - assertStyleCounters(2, 0); - assertStyle(element, 'width', '123px'); - assertStyle(element, 'height', '123px'); - - comp.width = null; - resetStylingCounters(); - fixture.detectChanges(); - - // the width is applied both in TEMPLATE and in HOST_BINDINGS mode - assertStyleCounters(2, 0); - assertStyle(element, 'width', '999px'); - assertStyle(element, 'height', '123px'); - - comp.dir.map = null; - resetStylingCounters(); - fixture.detectChanges(); - - // the width is only applied once - assertStyleCounters(1, 0); - assertStyle(element, 'width', '0px'); - assertStyle(element, 'height', '123px'); - - comp.dir.map = {width: '1000px', height: '1000px', color: 'red'}; - resetStylingCounters(); - fixture.detectChanges(); - - // only the width and color have changed - assertStyleCounters(2, 0); - assertStyle(element, 'width', '1000px'); - assertStyle(element, 'height', '123px'); - assertStyle(element, 'color', 'red'); - - comp.height = null; - resetStylingCounters(); - fixture.detectChanges(); - - // height gets applied twice and all other - // values get applied - assertStyleCounters(4, 0); - assertStyle(element, 'width', '1000px'); - assertStyle(element, 'height', '1000px'); - assertStyle(element, 'color', 'red'); - - comp.map = {color: 'blue', width: '2000px', opacity: '0.5'}; - resetStylingCounters(); - fixture.detectChanges(); - - assertStyleCounters(5, 0); - assertStyle(element, 'width', '2000px'); - assertStyle(element, 'height', '1000px'); - assertStyle(element, 'color', 'blue'); - assertStyle(element, 'opacity', '0.5'); - - comp.map = {color: 'blue', width: '2000px'}; - resetStylingCounters(); - fixture.detectChanges(); - - // all four are applied because the map was altered - assertStyleCounters(4, 1); - assertStyle(element, 'width', '2000px'); - assertStyle(element, 'height', '1000px'); - assertStyle(element, 'color', 'blue'); - assertStyle(element, 'opacity', ''); - }); - - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should sanitize style values before writing them', () => { - @Component({ - template: ` -
- ` - }) - class Cmp { - widthExp = ''; - bgImageExp = ''; - styleMapExp: any = {}; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - const styles = node.styles !; - - const lastSanitizedProps: any[] = []; - styles.overrideSanitizer((prop, value) => { - lastSanitizedProps.push(prop); - return value; - }); - - comp.bgImageExp = '123'; - fixture.detectChanges(); - - expect(styles.values).toEqual({ - 'background-image': '123', - 'width': null, - }); - - expect(lastSanitizedProps).toEqual(['background-image']); - lastSanitizedProps.length = 0; - - comp.styleMapExp = {'clip-path': '456'}; - fixture.detectChanges(); - - expect(styles.values).toEqual({ - 'background-image': '123', - 'clip-path': '456', - 'width': null, - }); - - expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']); - lastSanitizedProps.length = 0; - - comp.widthExp = '789px'; - fixture.detectChanges(); - - expect(styles.values).toEqual({ - 'background-image': '123', - 'clip-path': '456', - 'width': '789px', - }); - - expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']); - lastSanitizedProps.length = 0; - }); - - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should apply a unit to a style before writing it', () => { - @Component({ - template: ` -
- ` - }) - class Cmp { - widthExp: string|number|null = ''; - heightExp: string|number|null = ''; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - const styles = node.styles !; - - comp.widthExp = '200'; - comp.heightExp = 10; - fixture.detectChanges(); - - expect(styles.values).toEqual({ - 'width': '200px', - 'height': '10em', - }); - - comp.widthExp = 0; - comp.heightExp = null; - fixture.detectChanges(); - - expect(styles.values).toEqual({ - 'width': '0px', - 'height': null, - }); - }); - - it('should be able to bind a SafeValue to clip-path', () => { - @Component({template: '
'}) - class Cmp { - path !: SafeStyle; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const sanitizer: DomSanitizer = TestBed.inject(DomSanitizer); - - fixture.componentInstance.path = sanitizer.bypassSecurityTrustStyle('url("#test")'); - fixture.detectChanges(); - - const html = fixture.nativeElement.innerHTML; - - // Note that check the raw HTML, because (at the time of writing) the Node-based renderer - // that we use to run tests doesn't support `clip-path` in `CSSStyleDeclaration`. - expect(html).toMatch(/style=["|']clip-path:\s*url\(.*#test.*\)/); - }); - - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should evaluate follow-up [style] maps even if a former map is null', () => { - @Directive({selector: '[dir-with-styling]'}) - class DirWithStyleMap { - @HostBinding('style') public styleMap: any = {color: 'red'}; - } - - @Directive({selector: '[dir-with-styling-part2]'}) - class DirWithStyleMapPart2 { - @HostBinding('style') public styleMap: any = {width: '200px'}; - } - - @Component({ - template: ` -
- ` - }) - class Cmp { - map: any = null; - - @ViewChild('div', {read: DirWithStyleMap, static: true}) - dir1 !: DirWithStyleMap; - - @ViewChild('div', {read: DirWithStyleMapPart2, static: true}) - dir2 !: DirWithStyleMapPart2; - } - - TestBed.configureTestingModule( - {declarations: [Cmp, DirWithStyleMap, DirWithStyleMapPart2]}); - const fixture = TestBed.createComponent(Cmp); - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - const styles = node.styles !; - - const values = styles.values; - const props = Object.keys(values).sort(); - expect(props).toEqual(['color', 'width']); - - expect(values['width']).toEqual('200px'); - expect(values['color']).toEqual('red'); - }); - - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should evaluate initial style/class values on a list of elements that changes', () => { - @Component({ - template: ` -
- {{ item }} -
- ` - }) - class Cmp { - items = [1, 2, 3]; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - fixture.detectChanges(); - - function getItemElements(): HTMLElement[] { - return [].slice.call(fixture.nativeElement.querySelectorAll('div')); - } - - function getItemClasses(): string[] { - return getItemElements().map(e => e.className).sort().join(' ').split(' '); - } - - expect(getItemElements().length).toEqual(3); - expect(getItemClasses()).toEqual([ - 'initial-class', - 'item-1', - 'initial-class', - 'item-2', - 'initial-class', - 'item-3', - ]); - - comp.items = [2, 4, 6, 8]; - fixture.detectChanges(); - - expect(getItemElements().length).toEqual(4); - expect(getItemClasses()).toEqual([ - 'initial-class', - 'item-2', - 'initial-class', - 'item-4', - 'initial-class', - 'item-6', - 'initial-class', - 'item-8', - ]); - }); - - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should create and update multiple class bindings across multiple elements in a template', - () => { - @Component({ - template: ` -
header
-
- {{ item }} -
- - ` - }) - class Cmp { - items = [1, 2, 3]; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - fixture.detectChanges(); - - function getItemElements(): HTMLElement[] { - return [].slice.call(fixture.nativeElement.querySelectorAll('div')); - } - - function getItemClasses(): string[] { - return getItemElements().map(e => e.className).sort().join(' ').split(' '); - } - - const header = fixture.nativeElement.querySelector('header'); - expect(header.classList.contains('header')); - - const footer = fixture.nativeElement.querySelector('footer'); - expect(footer.classList.contains('footer')); - - expect(getItemElements().length).toEqual(3); - expect(getItemClasses()).toEqual([ - 'item', - 'item-1', - 'item', - 'item-2', - 'item', - 'item-3', - ]); - }); - - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should understand multiple directives which contain initial classes', () => { - @Directive({selector: 'dir-one'}) - class DirOne { - @HostBinding('class') public className = 'dir-one'; - } - - @Directive({selector: 'dir-two'}) - class DirTwo { - @HostBinding('class') public className = 'dir-two'; - } - - @Component({ - template: ` - -
- - ` - }) - class Cmp { - } - - TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]}); - const fixture = TestBed.createComponent(Cmp); - fixture.detectChanges(); - - const dirOne = fixture.nativeElement.querySelector('dir-one'); - const div = fixture.nativeElement.querySelector('div'); - const dirTwo = fixture.nativeElement.querySelector('dir-two'); - - expect(dirOne.classList.contains('dir-one')).toBeTruthy(); - expect(dirTwo.classList.contains('dir-two')).toBeTruthy(); - expect(div.classList.contains('initial')).toBeTruthy(); - }); - - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should evaluate styling across the template directives when there are multiple elements/sources of styling', - () => { - @Directive({selector: '[one]'}) - class DirOne { - @HostBinding('class') public className = 'dir-one'; - } - - @Directive({selector: '[two]'}) - class DirTwo { - @HostBinding('class') public className = 'dir-two'; - } - - @Component({ - template: ` -
-
-
- ` - }) - class Cmp { - w = 100; - h = 200; - c = 'red'; - } - - TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]}); - const fixture = TestBed.createComponent(Cmp); - fixture.detectChanges(); - - const divA = fixture.nativeElement.querySelector('.a'); - const divB = fixture.nativeElement.querySelector('.b'); - const divC = fixture.nativeElement.querySelector('.c'); - - expect(divA.style.width).toEqual('100px'); - expect(divB.style.height).toEqual('200px'); - expect(divC.style.color).toEqual('red'); - }); - - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should evaluate styling across the template and directives within embedded views', - () => { - @Directive({selector: '[some-dir-with-styling]'}) - class SomeDirWithStyling { - @HostBinding('style') - public styles = { - width: '200px', - height: '500px', - }; - } - - @Component({ - template: ` -
- {{ item }} -
-
-

- ` - }) - class Cmp { - items: any[] = []; - c = 'red'; - w = 100; - h = 100; - } - - TestBed.configureTestingModule({declarations: [Cmp, SomeDirWithStyling]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - comp.items = [1, 2, 3, 4]; - fixture.detectChanges(); - - const items = fixture.nativeElement.querySelectorAll('.item'); - expect(items.length).toEqual(4); - const [a, b, c, d] = items; - expect(a.style.height).toEqual('0px'); - expect(b.style.height).toEqual('100px'); - expect(c.style.height).toEqual('200px'); - expect(d.style.height).toEqual('300px'); - - const section = fixture.nativeElement.querySelector('section'); - const p = fixture.nativeElement.querySelector('p'); - - expect(section.style['width']).toEqual('100px'); - expect(p.style['height']).toEqual('100px'); - }); - - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should flush bindings even if any styling hasn\'t changed in a previous directive', - () => { - @Directive({selector: '[one]'}) - class DirOne { - @HostBinding('style.width') w = '100px'; - @HostBinding('style.opacity') o = '0.5'; - } - - @Directive({selector: '[two]'}) - class DirTwo { - @HostBinding('style.height') h = '200px'; - @HostBinding('style.color') c = 'red'; - } - - @Component({template: '
'}) - class Cmp { - @ViewChild('target', {read: DirOne, static: true}) one !: DirOne; - @ViewChild('target', {read: DirTwo, static: true}) two !: DirTwo; - } - - TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - fixture.detectChanges(); - - const div = fixture.nativeElement.querySelector('div'); - expect(div.style.opacity).toEqual('0.5'); - expect(div.style.color).toEqual('red'); - expect(div.style.width).toEqual('100px'); - expect(div.style.height).toEqual('200px'); - - comp.two.h = '300px'; - fixture.detectChanges(); - expect(div.style.opacity).toEqual('0.5'); - expect(div.style.color).toEqual('red'); - expect(div.style.width).toEqual('100px'); - expect(div.style.height).toEqual('300px'); - }); - - it('should work with NO_CHANGE values if they are applied to bindings ', () => { - @Component({ - template: ` -
- ` - }) - class Cmp { - w: any = null; - h: any = null; - o: any = null; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - - comp.w = '100px'; - comp.h = '200px'; - comp.o = '0.5'; - fixture.detectChanges(); - - const div = fixture.nativeElement.querySelector('div'); - expect(div.style.width).toEqual('100px'); - expect(div.style.height).toEqual('200px'); - expect(div.style.opacity).toEqual('0.5'); - - comp.w = '500px'; - comp.o = '1'; - fixture.detectChanges(); - - expect(div.style.width).toEqual('500px'); - expect(div.style.height).toEqual('200px'); - expect(div.style.opacity).toEqual('1'); - }); - - it('should allow [ngStyle] and [ngClass] to be used together', () => { - @Component({ - template: ` -
- ` - }) - class Cmp { - c: any = 'foo bar'; - s: any = {width: '200px'}; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - fixture.detectChanges(); - - const div = fixture.nativeElement.querySelector('div'); - expect(div.style.width).toEqual('200px'); - expect(div.classList.contains('foo')).toBeTruthy(); - expect(div.classList.contains('bar')).toBeTruthy(); - }); - - it('should allow detectChanges to be run in a property change that causes additional styling to be rendered', - () => { - @Component({ - selector: 'child', - template: ` -
- `, - }) - class ChildCmp { - readyTpl = false; - - @HostBinding('class.ready-host') - readyHost = false; - } - - @Component({ - selector: 'parent', - template: ` -
-
-

{{prop}}

-
- `, - host: { - '[style.color]': 'color', - }, - }) - class ParentCmp { - private _prop = ''; - - @ViewChild('template', {read: ViewContainerRef, static: false}) - vcr: ViewContainerRef = null !; - - private child: ComponentRef = null !; - - @Input() - set prop(value: string) { - this._prop = value; - if (this.child && value === 'go') { - this.child.instance.readyHost = true; - this.child.instance.readyTpl = true; - this.child.changeDetectorRef.detectChanges(); - } - } - - get prop() { return this._prop; } - - ngAfterViewInit() { - const factory = this.componentFactoryResolver.resolveComponentFactory(ChildCmp); - this.child = this.vcr.createComponent(factory); - } - - constructor(private componentFactoryResolver: ComponentFactoryResolver) {} - } - - @Component({ - template: ``, - }) - class App { - prop = 'a'; - } - - @NgModule({ - entryComponents: [ChildCmp], - declarations: [ChildCmp], - }) - class ChildCmpModule { - } - - TestBed.configureTestingModule({declarations: [App, ParentCmp], imports: [ChildCmpModule]}); - const fixture = TestBed.createComponent(App); - fixture.detectChanges(false); - - let readyHost = fixture.nativeElement.querySelector('.ready-host'); - let readyChild = fixture.nativeElement.querySelector('.ready-child'); - - expect(readyHost).toBeFalsy(); - expect(readyChild).toBeFalsy(); - - fixture.componentInstance.prop = 'go'; - fixture.detectChanges(false); - - readyHost = fixture.nativeElement.querySelector('.ready-host'); - readyChild = fixture.nativeElement.querySelector('.ready-child'); - expect(readyHost).toBeTruthy(); - expect(readyChild).toBeTruthy(); - }); - - it('should allow detectChanges to be run in a hook that causes additional styling to be rendered', - () => { - @Component({ - selector: 'child', - template: ` -
- `, - }) - class ChildCmp { - readyTpl = false; - - @HostBinding('class.ready-host') - readyHost = false; - } - - @Component({ - selector: 'parent', - template: ` -
-
-

{{prop}}

-
- `, - }) - class ParentCmp { - updateChild = false; - - @ViewChild('template', {read: ViewContainerRef, static: false}) - vcr: ViewContainerRef = null !; - - private child: ComponentRef = null !; - - ngDoCheck() { - if (this.updateChild) { - this.child.instance.readyHost = true; - this.child.instance.readyTpl = true; - this.child.changeDetectorRef.detectChanges(); - } - } - - ngAfterViewInit() { - const factory = this.componentFactoryResolver.resolveComponentFactory(ChildCmp); - this.child = this.vcr.createComponent(factory); - } - - constructor(private componentFactoryResolver: ComponentFactoryResolver) {} - } - - @Component({ - template: ``, - }) - class App { - @ViewChild('parent', {static: true}) public parent: ParentCmp|null = null; - } - - @NgModule({ - entryComponents: [ChildCmp], - declarations: [ChildCmp], - }) - class ChildCmpModule { - } - - TestBed.configureTestingModule({declarations: [App, ParentCmp], imports: [ChildCmpModule]}); - const fixture = TestBed.createComponent(App); - fixture.detectChanges(false); - - let readyHost = fixture.nativeElement.querySelector('.ready-host'); - let readyChild = fixture.nativeElement.querySelector('.ready-child'); - expect(readyHost).toBeFalsy(); - expect(readyChild).toBeFalsy(); - - const parent = fixture.componentInstance.parent !; - parent.updateChild = true; - fixture.detectChanges(false); - - readyHost = fixture.nativeElement.querySelector('.ready-host'); - readyChild = fixture.nativeElement.querySelector('.ready-child'); - expect(readyHost).toBeTruthy(); - expect(readyChild).toBeTruthy(); - }); -}); - -function assertStyleCounters(countForSet: number, countForRemove: number) { - expect(ngDevMode !.rendererSetStyle).toEqual(countForSet); - expect(ngDevMode !.rendererRemoveStyle).toEqual(countForRemove); -} - -function assertStyle(element: HTMLElement, prop: string, value: any) { - expect((element.style as any)[prop]).toEqual(value); -} - -function getDebugNode(element: Node): DebugNode|null { - const lContext = loadLContextFromNode(element); - const lViewDebug = toDebug(lContext.lView) as LViewDebug; - const debugNodes = lViewDebug.nodes || []; - for (let i = 0; i < debugNodes.length; i++) { - const n = debugNodes[i]; - if (n.native === element) { - return n; - } - } - return null; -} diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index acfb5eab31..5977917ca2 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.ts @@ -5,7 +5,9 @@ * 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, Directive, ElementRef, HostBinding, Input, ViewChild} from '@angular/core'; +import {Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, HostBinding, Input, NgModule, ViewChild, ViewContainerRef} from '@angular/core'; +import {DebugNode, LViewDebug, toDebug} from '@angular/core/src/render3/instructions/lview_debug'; +import {loadLContextFromNode} from '@angular/core/src/render3/util/discovery_utils'; import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {TestBed} from '@angular/core/testing'; import {By, DomSanitizer, SafeStyle} from '@angular/platform-browser'; @@ -672,4 +674,1347 @@ describe('styling', () => { expect(classList.contains('one')).toBe(true); expect(classList.contains('two')).toBe(true); }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should apply single property styles/classes to the element and default to any static styling values', + () => { + @Component({ + template: ` +
+ ` + }) + class Cmp { + w: string|null = '100px'; + h: string|null = '100px'; + o: string|null = '0.5'; + abc = true; + xyz = false; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + expect(element.style.width).toEqual('100px'); + expect(element.style.height).toEqual('100px'); + expect(element.style.opacity).toEqual('0.5'); + expect(element.classList.contains('abc')).toBeTruthy(); + expect(element.classList.contains('xyz')).toBeFalsy(); + + fixture.componentInstance.w = null; + fixture.componentInstance.h = null; + fixture.componentInstance.o = null; + fixture.componentInstance.abc = false; + fixture.componentInstance.xyz = true; + fixture.detectChanges(); + + expect(element.style.width).toEqual('200px'); + expect(element.style.height).toEqual('200px'); + expect(element.style.opacity).toBeFalsy(); + expect(element.classList.contains('abc')).toBeFalsy(); + expect(element.classList.contains('xyz')).toBeTruthy(); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should apply single style/class across the template and directive host bindings', () => { + @Directive({selector: '[dir-that-sets-width]'}) + class DirThatSetsWidthDirective { + @Input('dir-that-sets-width') @HostBinding('style.width') public width: string = ''; + } + + @Directive({selector: '[another-dir-that-sets-width]', host: {'[style.width]': 'width'}}) + class AnotherDirThatSetsWidthDirective { + @Input('another-dir-that-sets-width') public width: string = ''; + } + + @Component({ + template: ` +
+ ` + }) + class Cmp { + w0: string|null = null; + w1: string|null = null; + w2: string|null = null; + } + + TestBed.configureTestingModule( + {declarations: [Cmp, DirThatSetsWidthDirective, AnotherDirThatSetsWidthDirective]}); + const fixture = TestBed.createComponent(Cmp); + fixture.componentInstance.w0 = '100px'; + fixture.componentInstance.w1 = '200px'; + fixture.componentInstance.w2 = '300px'; + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + expect(element.style.width).toEqual('100px'); + + fixture.componentInstance.w0 = null; + fixture.detectChanges(); + + expect(element.style.width).toEqual('200px'); + + fixture.componentInstance.w1 = null; + fixture.detectChanges(); + + expect(element.style.width).toEqual('300px'); + + fixture.componentInstance.w2 = null; + fixture.detectChanges(); + + expect(element.style.width).toBeFalsy(); + + fixture.componentInstance.w2 = '400px'; + fixture.detectChanges(); + + expect(element.style.width).toEqual('400px'); + + fixture.componentInstance.w1 = '500px'; + fixture.componentInstance.w0 = '600px'; + fixture.detectChanges(); + + expect(element.style.width).toEqual('600px'); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should only run stylingFlush once when there are no collisions between styling properties', + () => { + @Directive({selector: '[dir-with-styling]'}) + class DirWithStyling { + @HostBinding('style.font-size') public fontSize = '100px'; + } + + @Component({selector: 'comp-with-styling'}) + class CompWithStyling { + @HostBinding('style.width') public width = '900px'; + + @HostBinding('style.height') public height = '900px'; + } + + @Component({ + template: ` + ... + ` + }) + class Cmp { + opacity: string|null = '0.5'; + @ViewChild(CompWithStyling, {static: true}) + compWithStyling: CompWithStyling|null = null; + @ViewChild(DirWithStyling, {static: true}) dirWithStyling: DirWithStyling|null = null; + } + + TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const component = fixture.componentInstance; + const element = fixture.nativeElement.querySelector('comp-with-styling'); + const node = getDebugNode(element) !; + + const styles = node.styles !; + const config = styles.config; + expect(config.hasCollisions).toBeFalsy(); + expect(config.hasMapBindings).toBeFalsy(); + expect(config.hasPropBindings).toBeTruthy(); + expect(config.allowDirectStyling).toBeTruthy(); + + expect(element.style.opacity).toEqual('0.5'); + expect(element.style.width).toEqual('900px'); + expect(element.style.height).toEqual('900px'); + expect(element.style.fontSize).toEqual('100px'); + + // once for the template flush and again for the host bindings + expect(ngDevMode !.flushStyling).toEqual(2); + ngDevModeResetPerfCounters(); + + component.opacity = '0.6'; + component.compWithStyling !.height = '100px'; + component.compWithStyling !.width = '100px'; + component.dirWithStyling !.fontSize = '50px'; + fixture.detectChanges(); + + expect(element.style.opacity).toEqual('0.6'); + expect(element.style.width).toEqual('100px'); + expect(element.style.height).toEqual('100px'); + expect(element.style.fontSize).toEqual('50px'); + + // there is no need to flush styling since the styles are applied directly + expect(ngDevMode !.flushStyling).toEqual(0); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should combine all styling across the template, directive and component host bindings', + () => { + @Directive({selector: '[dir-with-styling]'}) + class DirWithStyling { + @HostBinding('style.color') public color = 'red'; + + @HostBinding('style.font-size') public fontSize = '100px'; + + @HostBinding('class.dir') public dirClass = true; + } + + @Component({selector: 'comp-with-styling'}) + class CompWithStyling { + @HostBinding('style.width') public width = '900px'; + + @HostBinding('style.height') public height = '900px'; + + @HostBinding('class.comp') public compClass = true; + } + + @Component({ + template: ` + ... + ` + }) + class Cmp { + opacity: string|null = '0.5'; + width: string|null = 'auto'; + tplClass = true; + } + + TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('comp-with-styling'); + + const node = getDebugNode(element) !; + const styles = node.styles !; + const classes = node.classes !; + + expect(styles.values).toEqual({ + 'color': 'red', + 'width': 'auto', + 'opacity': '0.5', + 'height': '900px', + 'font-size': '100px' + }); + expect(classes.values).toEqual({ + 'dir': true, + 'comp': true, + 'tpl': true, + }); + + fixture.componentInstance.width = null; + fixture.componentInstance.opacity = null; + fixture.componentInstance.tplClass = false; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'color': 'red', + 'width': '900px', + 'opacity': null, + 'height': '900px', + 'font-size': '100px' + }); + expect(classes.values).toEqual({ + 'dir': true, + 'comp': true, + 'tpl': false, + }); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should properly apply styling across sub and super class directive host bindings', + () => { + @Directive({selector: '[super-class-dir]'}) + class SuperClassDirective { + @HostBinding('style.width') public w1 = '100px'; + } + + @Component({selector: '[sub-class-dir]'}) + class SubClassDirective extends SuperClassDirective { + @HostBinding('style.width') public w2 = '200px'; + } + + @Component({ + template: ` +
+ ` + }) + class Cmp { + w3: string|null = '300px'; + } + + TestBed.configureTestingModule( + {declarations: [Cmp, SuperClassDirective, SubClassDirective]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + + const node = getDebugNode(element) !; + const styles = node.styles !; + + expect(styles.values).toEqual({ + 'width': '300px', + }); + + fixture.componentInstance.w3 = null; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'width': '200px', + }); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should support situations where there are more than 32 bindings', () => { + const TOTAL_BINDINGS = 34; + + let bindingsHTML = ''; + let bindingsArr: any[] = []; + for (let i = 0; i < TOTAL_BINDINGS; i++) { + bindingsHTML += `[style.prop${i}]="bindings[${i}]" `; + bindingsArr.push(null); + } + + @Component({template: `
`}) + class Cmp { + bindings = bindingsArr; + + updateBindings(value: string) { + for (let i = 0; i < TOTAL_BINDINGS; i++) { + this.bindings[i] = value + i; + } + } + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + + let testValue = 'initial'; + fixture.componentInstance.updateBindings('initial'); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + + const node = getDebugNode(element) !; + const styles = node.styles !; + + let values = styles.values; + let props = Object.keys(values); + expect(props.length).toEqual(TOTAL_BINDINGS); + + for (let i = 0; i < props.length; i++) { + const prop = props[i]; + const value = values[prop] as string; + const num = value.substr(testValue.length); + expect(value).toEqual(`initial${num}`); + } + + testValue = 'final'; + fixture.componentInstance.updateBindings('final'); + fixture.detectChanges(); + + values = styles.values; + props = Object.keys(values); + expect(props.length).toEqual(TOTAL_BINDINGS); + for (let i = 0; i < props.length; i++) { + const prop = props[i]; + const value = values[prop] as string; + const num = value.substr(testValue.length); + expect(value).toEqual(`final${num}`); + } + }); + + onlyInIvy('only ivy has style debugging support') + .it('should apply map-based style and class entries', () => { + @Component({template: '
'}) + class Cmp { + public c: {[key: string]: any}|null = null; + updateClasses(classes: string) { + const c = this.c || (this.c = {}); + Object.keys(this.c).forEach(className => { c[className] = false; }); + classes.split(/\s+/).forEach(className => { c[className] = true; }); + } + + public s: {[key: string]: any}|null = null; + updateStyles(prop: string, value: string|number|null) { + const s = this.s || (this.s = {}); + Object.assign(s, {[prop]: value}); + } + + reset() { + this.s = null; + this.c = null; + } + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + comp.updateStyles('width', '100px'); + comp.updateStyles('height', '200px'); + comp.updateClasses('abc'); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + const node = getDebugNode(element) !; + let styles = node.styles !; + let classes = node.classes !; + + let stylesSummary = styles.summary; + let widthSummary = stylesSummary['width']; + expect(widthSummary.prop).toEqual('width'); + expect(widthSummary.value).toEqual('100px'); + + let heightSummary = stylesSummary['height']; + expect(heightSummary.prop).toEqual('height'); + expect(heightSummary.value).toEqual('200px'); + + let classesSummary = classes.summary; + let abcSummary = classesSummary['abc']; + expect(abcSummary.prop).toEqual('abc'); + expect(abcSummary.value).toBeTruthy(); + + comp.reset(); + comp.updateStyles('width', '500px'); + comp.updateStyles('height', null); + comp.updateClasses('def'); + fixture.detectChanges(); + + styles = node.styles !; + classes = node.classes !; + + stylesSummary = styles.summary; + widthSummary = stylesSummary['width']; + expect(widthSummary.value).toEqual('500px'); + + heightSummary = stylesSummary['height']; + expect(heightSummary.value).toEqual(null); + + classesSummary = classes.summary; + abcSummary = classesSummary['abc']; + expect(abcSummary.prop).toEqual('abc'); + expect(abcSummary.value).toBeFalsy(); + + let defSummary = classesSummary['def']; + expect(defSummary.prop).toEqual('def'); + expect(defSummary.value).toBeTruthy(); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should resolve styling collisions across templates, directives and components for prop and map-based entries', + () => { + @Directive({selector: '[dir-that-sets-styling]'}) + class DirThatSetsStyling { + @HostBinding('style') public map: any = {color: 'red', width: '777px'}; + } + + @Component({ + template: ` +
+ ` + }) + class Cmp { + map: any = {width: '111px', opacity: '0.5'}; + width: string|null = '555px'; + + @ViewChild('dir', {read: DirThatSetsStyling, static: true}) + dir !: DirThatSetsStyling; + } + + TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + const node = getDebugNode(element) !; + + const styles = node.styles !; + + expect(styles.values).toEqual({ + 'width': '555px', + 'color': 'red', + 'font-size': '99px', + 'opacity': '0.5', + }); + + comp.width = null; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'width': '111px', + 'color': 'red', + 'font-size': '99px', + 'opacity': '0.5', + }); + + comp.map = null; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'width': '777px', + 'color': 'red', + 'font-size': '99px', + 'opacity': null, + }); + + comp.dir.map = null; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'width': '200px', + 'color': null, + 'font-size': '99px', + 'opacity': null, + }); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should only apply each styling property once per CD across templates, components, directives', + () => { + @Directive({selector: '[dir-that-sets-styling]'}) + class DirThatSetsStyling { + @HostBinding('style') public map: any = {width: '999px', height: '999px'}; + } + + @Component({ + template: ` +
+ ` + }) + class Cmp { + width: string|null = '111px'; + height: string|null = '111px'; + + map: any = {width: '555px', height: '555px'}; + + @ViewChild('dir', {read: DirThatSetsStyling, static: true}) + dir !: DirThatSetsStyling; + } + + TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + const element = fixture.nativeElement.querySelector('div'); + + // both are applied because this is the first pass + assertStyleCounters(2, 0); + assertStyle(element, 'width', '111px'); + assertStyle(element, 'height', '111px'); + + comp.width = '222px'; + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + + assertStyleCounters(1, 0); + assertStyle(element, 'width', '222px'); + assertStyle(element, 'height', '111px'); + + comp.height = '222px'; + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + + assertStyleCounters(1, 0); + assertStyle(element, 'width', '222px'); + assertStyle(element, 'height', '222px'); + + comp.width = null; + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + + assertStyleCounters(1, 0); + assertStyle(element, 'width', '555px'); + assertStyle(element, 'height', '222px'); + + comp.width = '123px'; + comp.height = '123px'; + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + + assertStyle(element, 'width', '123px'); + assertStyle(element, 'height', '123px'); + + comp.map = {}; + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + + // both are applied because the map was altered + assertStyleCounters(2, 0); + assertStyle(element, 'width', '123px'); + assertStyle(element, 'height', '123px'); + + comp.width = null; + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + + // the width is applied both in TEMPLATE and in HOST_BINDINGS mode + assertStyleCounters(2, 0); + assertStyle(element, 'width', '999px'); + assertStyle(element, 'height', '123px'); + + comp.dir.map = null; + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + + // the width is only applied once + assertStyleCounters(1, 0); + assertStyle(element, 'width', '0px'); + assertStyle(element, 'height', '123px'); + + comp.dir.map = {width: '1000px', height: '1000px', color: 'red'}; + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + + // only the width and color have changed + assertStyleCounters(2, 0); + assertStyle(element, 'width', '1000px'); + assertStyle(element, 'height', '123px'); + assertStyle(element, 'color', 'red'); + + comp.height = null; + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + + // height gets applied twice and all other + // values get applied + assertStyleCounters(4, 0); + assertStyle(element, 'width', '1000px'); + assertStyle(element, 'height', '1000px'); + assertStyle(element, 'color', 'red'); + + comp.map = {color: 'blue', width: '2000px', opacity: '0.5'}; + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + + assertStyleCounters(5, 0); + assertStyle(element, 'width', '2000px'); + assertStyle(element, 'height', '1000px'); + assertStyle(element, 'color', 'blue'); + assertStyle(element, 'opacity', '0.5'); + + comp.map = {color: 'blue', width: '2000px'}; + ngDevModeResetPerfCounters(); + fixture.detectChanges(); + + // all four are applied because the map was altered + assertStyleCounters(4, 1); + assertStyle(element, 'width', '2000px'); + assertStyle(element, 'height', '1000px'); + assertStyle(element, 'color', 'blue'); + assertStyle(element, 'opacity', ''); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should sanitize style values before writing them', () => { + @Component({ + template: ` +
+ ` + }) + class Cmp { + widthExp = ''; + bgImageExp = ''; + styleMapExp: any = {}; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + const node = getDebugNode(element) !; + const styles = node.styles !; + + const lastSanitizedProps: any[] = []; + styles.overrideSanitizer((prop, value) => { + lastSanitizedProps.push(prop); + return value; + }); + + comp.bgImageExp = '123'; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'background-image': '123', + 'width': null, + }); + + expect(lastSanitizedProps).toEqual(['background-image']); + lastSanitizedProps.length = 0; + + comp.styleMapExp = {'clip-path': '456'}; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'background-image': '123', + 'clip-path': '456', + 'width': null, + }); + + expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']); + lastSanitizedProps.length = 0; + + comp.widthExp = '789px'; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'background-image': '123', + 'clip-path': '456', + 'width': '789px', + }); + + expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']); + lastSanitizedProps.length = 0; + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should apply a unit to a style before writing it', () => { + @Component({ + template: ` +
+ ` + }) + class Cmp { + widthExp: string|number|null = ''; + heightExp: string|number|null = ''; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + const node = getDebugNode(element) !; + const styles = node.styles !; + + comp.widthExp = '200'; + comp.heightExp = 10; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'width': '200px', + 'height': '10em', + }); + + comp.widthExp = 0; + comp.heightExp = null; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'width': '0px', + 'height': null, + }); + }); + + it('should be able to bind a SafeValue to clip-path', () => { + @Component({template: '
'}) + class Cmp { + path !: SafeStyle; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const sanitizer: DomSanitizer = TestBed.inject(DomSanitizer); + + fixture.componentInstance.path = sanitizer.bypassSecurityTrustStyle('url("#test")'); + fixture.detectChanges(); + + const html = fixture.nativeElement.innerHTML; + + // Note that check the raw HTML, because (at the time of writing) the Node-based renderer + // that we use to run tests doesn't support `clip-path` in `CSSStyleDeclaration`. + expect(html).toMatch(/style=["|']clip-path:\s*url\(.*#test.*\)/); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should evaluate follow-up [style] maps even if a former map is null', () => { + @Directive({selector: '[dir-with-styling]'}) + class DirWithStyleMap { + @HostBinding('style') public styleMap: any = {color: 'red'}; + } + + @Directive({selector: '[dir-with-styling-part2]'}) + class DirWithStyleMapPart2 { + @HostBinding('style') public styleMap: any = {width: '200px'}; + } + + @Component({ + template: ` +
+ ` + }) + class Cmp { + map: any = null; + + @ViewChild('div', {read: DirWithStyleMap, static: true}) + dir1 !: DirWithStyleMap; + + @ViewChild('div', {read: DirWithStyleMapPart2, static: true}) + dir2 !: DirWithStyleMapPart2; + } + + TestBed.configureTestingModule( + {declarations: [Cmp, DirWithStyleMap, DirWithStyleMapPart2]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + const node = getDebugNode(element) !; + const styles = node.styles !; + + const values = styles.values; + const props = Object.keys(values).sort(); + expect(props).toEqual(['color', 'width']); + + expect(values['width']).toEqual('200px'); + expect(values['color']).toEqual('red'); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should evaluate initial style/class values on a list of elements that changes', () => { + @Component({ + template: ` +
+ {{ item }} +
+ ` + }) + class Cmp { + items = [1, 2, 3]; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + function getItemElements(): HTMLElement[] { + return [].slice.call(fixture.nativeElement.querySelectorAll('div')); + } + + function getItemClasses(): string[] { + return getItemElements().map(e => e.className).sort().join(' ').split(' '); + } + + expect(getItemElements().length).toEqual(3); + expect(getItemClasses()).toEqual([ + 'initial-class', + 'item-1', + 'initial-class', + 'item-2', + 'initial-class', + 'item-3', + ]); + + comp.items = [2, 4, 6, 8]; + fixture.detectChanges(); + + expect(getItemElements().length).toEqual(4); + expect(getItemClasses()).toEqual([ + 'initial-class', + 'item-2', + 'initial-class', + 'item-4', + 'initial-class', + 'item-6', + 'initial-class', + 'item-8', + ]); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should create and update multiple class bindings across multiple elements in a template', + () => { + @Component({ + template: ` +
header
+
+ {{ item }} +
+
footer
+ ` + }) + class Cmp { + items = [1, 2, 3]; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + function getItemElements(): HTMLElement[] { + return [].slice.call(fixture.nativeElement.querySelectorAll('div')); + } + + function getItemClasses(): string[] { + return getItemElements().map(e => e.className).sort().join(' ').split(' '); + } + + const header = fixture.nativeElement.querySelector('header'); + expect(header.classList.contains('header')); + + const footer = fixture.nativeElement.querySelector('footer'); + expect(footer.classList.contains('footer')); + + expect(getItemElements().length).toEqual(3); + expect(getItemClasses()).toEqual([ + 'item', + 'item-1', + 'item', + 'item-2', + 'item', + 'item-3', + ]); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should understand multiple directives which contain initial classes', () => { + @Directive({selector: 'dir-one'}) + class DirOne { + @HostBinding('class') public className = 'dir-one'; + } + + @Directive({selector: 'dir-two'}) + class DirTwo { + @HostBinding('class') public className = 'dir-two'; + } + + @Component({ + template: ` + +
+ + ` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const dirOne = fixture.nativeElement.querySelector('dir-one'); + const div = fixture.nativeElement.querySelector('div'); + const dirTwo = fixture.nativeElement.querySelector('dir-two'); + + expect(dirOne.classList.contains('dir-one')).toBeTruthy(); + expect(dirTwo.classList.contains('dir-two')).toBeTruthy(); + expect(div.classList.contains('initial')).toBeTruthy(); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should evaluate styling across the template directives when there are multiple elements/sources of styling', + () => { + @Directive({selector: '[one]'}) + class DirOne { + @HostBinding('class') public className = 'dir-one'; + } + + @Directive({selector: '[two]'}) + class DirTwo { + @HostBinding('class') public className = 'dir-two'; + } + + @Component({ + template: ` +
+
+
+ ` + }) + class Cmp { + w = 100; + h = 200; + c = 'red'; + } + + TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const divA = fixture.nativeElement.querySelector('.a'); + const divB = fixture.nativeElement.querySelector('.b'); + const divC = fixture.nativeElement.querySelector('.c'); + + expect(divA.style.width).toEqual('100px'); + expect(divB.style.height).toEqual('200px'); + expect(divC.style.color).toEqual('red'); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should evaluate styling across the template and directives within embedded views', + () => { + @Directive({selector: '[some-dir-with-styling]'}) + class SomeDirWithStyling { + @HostBinding('style') + public styles = { + width: '200px', + height: '500px', + }; + } + + @Component({ + template: ` +
+ {{ item }} +
+
+

+ ` + }) + class Cmp { + items: any[] = []; + c = 'red'; + w = 100; + h = 100; + } + + TestBed.configureTestingModule({declarations: [Cmp, SomeDirWithStyling]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + comp.items = [1, 2, 3, 4]; + fixture.detectChanges(); + + const items = fixture.nativeElement.querySelectorAll('.item'); + expect(items.length).toEqual(4); + const [a, b, c, d] = items; + expect(a.style.height).toEqual('0px'); + expect(b.style.height).toEqual('100px'); + expect(c.style.height).toEqual('200px'); + expect(d.style.height).toEqual('300px'); + + const section = fixture.nativeElement.querySelector('section'); + const p = fixture.nativeElement.querySelector('p'); + + expect(section.style['width']).toEqual('100px'); + expect(p.style['height']).toEqual('100px'); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should flush bindings even if any styling hasn\'t changed in a previous directive', + () => { + @Directive({selector: '[one]'}) + class DirOne { + @HostBinding('style.width') w = '100px'; + @HostBinding('style.opacity') o = '0.5'; + } + + @Directive({selector: '[two]'}) + class DirTwo { + @HostBinding('style.height') h = '200px'; + @HostBinding('style.color') c = 'red'; + } + + @Component({template: '
'}) + class Cmp { + @ViewChild('target', {read: DirOne, static: true}) one !: DirOne; + @ViewChild('target', {read: DirTwo, static: true}) two !: DirTwo; + } + + TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(div.style.opacity).toEqual('0.5'); + expect(div.style.color).toEqual('red'); + expect(div.style.width).toEqual('100px'); + expect(div.style.height).toEqual('200px'); + + comp.two.h = '300px'; + fixture.detectChanges(); + expect(div.style.opacity).toEqual('0.5'); + expect(div.style.color).toEqual('red'); + expect(div.style.width).toEqual('100px'); + expect(div.style.height).toEqual('300px'); + }); + + it('should work with NO_CHANGE values if they are applied to bindings ', () => { + @Component({ + template: ` +
+ ` + }) + class Cmp { + w: any = null; + h: any = null; + o: any = null; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + + comp.w = '100px'; + comp.h = '200px'; + comp.o = '0.5'; + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(div.style.width).toEqual('100px'); + expect(div.style.height).toEqual('200px'); + expect(div.style.opacity).toEqual('0.5'); + + comp.w = '500px'; + comp.o = '1'; + fixture.detectChanges(); + + expect(div.style.width).toEqual('500px'); + expect(div.style.height).toEqual('200px'); + expect(div.style.opacity).toEqual('1'); + }); + + it('should allow [ngStyle] and [ngClass] to be used together', () => { + @Component({ + template: ` +
+ ` + }) + class Cmp { + c: any = 'foo bar'; + s: any = {width: '200px'}; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(div.style.width).toEqual('200px'); + expect(div.classList.contains('foo')).toBeTruthy(); + expect(div.classList.contains('bar')).toBeTruthy(); + }); + + it('should allow detectChanges to be run in a property change that causes additional styling to be rendered', + () => { + @Component({ + selector: 'child', + template: ` +
+ `, + }) + class ChildCmp { + readyTpl = false; + + @HostBinding('class.ready-host') + readyHost = false; + } + + @Component({ + selector: 'parent', + template: ` +
+
+

{{prop}}

+
+ `, + host: { + '[style.color]': 'color', + }, + }) + class ParentCmp { + private _prop = ''; + + @ViewChild('template', {read: ViewContainerRef, static: false}) + vcr: ViewContainerRef = null !; + + private child: ComponentRef = null !; + + @Input() + set prop(value: string) { + this._prop = value; + if (this.child && value === 'go') { + this.child.instance.readyHost = true; + this.child.instance.readyTpl = true; + this.child.changeDetectorRef.detectChanges(); + } + } + + get prop() { return this._prop; } + + ngAfterViewInit() { + const factory = this.componentFactoryResolver.resolveComponentFactory(ChildCmp); + this.child = this.vcr.createComponent(factory); + } + + constructor(private componentFactoryResolver: ComponentFactoryResolver) {} + } + + @Component({ + template: ``, + }) + class App { + prop = 'a'; + } + + @NgModule({ + entryComponents: [ChildCmp], + declarations: [ChildCmp], + }) + class ChildCmpModule { + } + + TestBed.configureTestingModule({declarations: [App, ParentCmp], imports: [ChildCmpModule]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(false); + + let readyHost = fixture.nativeElement.querySelector('.ready-host'); + let readyChild = fixture.nativeElement.querySelector('.ready-child'); + + expect(readyHost).toBeFalsy(); + expect(readyChild).toBeFalsy(); + + fixture.componentInstance.prop = 'go'; + fixture.detectChanges(false); + + readyHost = fixture.nativeElement.querySelector('.ready-host'); + readyChild = fixture.nativeElement.querySelector('.ready-child'); + expect(readyHost).toBeTruthy(); + expect(readyChild).toBeTruthy(); + }); + + it('should allow detectChanges to be run in a hook that causes additional styling to be rendered', + () => { + @Component({ + selector: 'child', + template: ` +
+ `, + }) + class ChildCmp { + readyTpl = false; + + @HostBinding('class.ready-host') + readyHost = false; + } + + @Component({ + selector: 'parent', + template: ` +
+
+

{{prop}}

+
+ `, + }) + class ParentCmp { + updateChild = false; + + @ViewChild('template', {read: ViewContainerRef, static: false}) + vcr: ViewContainerRef = null !; + + private child: ComponentRef = null !; + + ngDoCheck() { + if (this.updateChild) { + this.child.instance.readyHost = true; + this.child.instance.readyTpl = true; + this.child.changeDetectorRef.detectChanges(); + } + } + + ngAfterViewInit() { + const factory = this.componentFactoryResolver.resolveComponentFactory(ChildCmp); + this.child = this.vcr.createComponent(factory); + } + + constructor(private componentFactoryResolver: ComponentFactoryResolver) {} + } + + @Component({ + template: ``, + }) + class App { + @ViewChild('parent', {static: true}) public parent: ParentCmp|null = null; + } + + @NgModule({ + entryComponents: [ChildCmp], + declarations: [ChildCmp], + }) + class ChildCmpModule { + } + + TestBed.configureTestingModule({declarations: [App, ParentCmp], imports: [ChildCmpModule]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(false); + + let readyHost = fixture.nativeElement.querySelector('.ready-host'); + let readyChild = fixture.nativeElement.querySelector('.ready-child'); + expect(readyHost).toBeFalsy(); + expect(readyChild).toBeFalsy(); + + const parent = fixture.componentInstance.parent !; + parent.updateChild = true; + fixture.detectChanges(false); + + readyHost = fixture.nativeElement.querySelector('.ready-host'); + readyChild = fixture.nativeElement.querySelector('.ready-child'); + expect(readyHost).toBeTruthy(); + expect(readyChild).toBeTruthy(); + }); }); + +function getDebugNode(element: Node): DebugNode|null { + const lContext = loadLContextFromNode(element); + const lViewDebug = toDebug(lContext.lView) as LViewDebug; + const debugNodes = lViewDebug.nodes || []; + for (let i = 0; i < debugNodes.length; i++) { + const n = debugNodes[i]; + if (n.native === element) { + return n; + } + } + return null; +} + +function assertStyleCounters(countForSet: number, countForRemove: number) { + expect(ngDevMode !.rendererSetStyle).toEqual(countForSet); + expect(ngDevMode !.rendererRemoveStyle).toEqual(countForRemove); +} + +function assertStyle(element: HTMLElement, prop: string, value: any) { + expect((element.style as any)[prop]).toEqual(value); +}