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 {
- 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: `
-
-
- {{ 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: `
-
- `,
- 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: `
-
- `,
- })
- 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: `
+
+
+ {{ 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: `
+
+ `,
+ 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: `
+
+ `,
+ })
+ 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);
+}