Inside `*ngFor` the second run of the styling instructions can get into situation where it tries to read a value from a binding which has not yet executed. As a result the read is `NO_CHANGE` value and subsequent property read cause an exception as it is of wrong type. Fix #35118 PR Close #35133
		
			
				
	
	
		
			3339 lines
		
	
	
		
			116 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			3339 lines
		
	
	
		
			116 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @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 {CommonModule} from '@angular/common';
 | |
| import {Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, HostBinding, Input, NgModule, Renderer2, ViewChild, ViewContainerRef} from '@angular/core';
 | |
| import {bypassSanitizationTrustStyle} from '@angular/core/src/sanitization/bypass';
 | |
| import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
 | |
| import {TestBed} from '@angular/core/testing';
 | |
| import {getElementClasses, getElementStyles, getSortedClassName, getSortedStyle} from '@angular/core/testing/src/styling';
 | |
| import {By, DomSanitizer, SafeStyle} from '@angular/platform-browser';
 | |
| import {expect} from '@angular/platform-browser/testing/src/matchers';
 | |
| import {ivyEnabled, modifiedInIvy, onlyInIvy} from '@angular/private/testing';
 | |
| 
 | |
| describe('styling', () => {
 | |
|   beforeEach(ngDevModeResetPerfCounters);
 | |
| 
 | |
|   describe('apply in prioritization order', () => {
 | |
|     it('should perform static bindings', () => {
 | |
|       @Component({template: `<div class="STATIC" style="color: blue"></div>`})
 | |
|       class Cmp {
 | |
|       }
 | |
| 
 | |
|       TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|       const fixture = TestBed.createComponent(Cmp);
 | |
| 
 | |
|       const [staticDiv] = fixture.nativeElement.querySelectorAll('div');
 | |
|       expect(getSortedClassName(staticDiv)).toEqual('STATIC');
 | |
|       expect(getSortedStyle(staticDiv)).toEqual('color: blue;');
 | |
|     });
 | |
| 
 | |
|     it('should perform prop bindings', () => {
 | |
|       @Component({
 | |
|         template: `<div [class.dynamic]="true" 
 | |
|                         [style.color]="'blue'"
 | |
|                         [style.width.px]="100"></div>`
 | |
|       })
 | |
|       class Cmp {
 | |
|       }
 | |
| 
 | |
|       TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|       const fixture = TestBed.createComponent(Cmp);
 | |
|       fixture.detectChanges();
 | |
| 
 | |
|       const div = fixture.nativeElement.querySelector('div');
 | |
|       expect(getSortedClassName(div)).toEqual('dynamic');
 | |
|       expect(getSortedStyle(div)).toEqual('color: blue; width: 100px;');
 | |
|     });
 | |
| 
 | |
|     onlyInIvy('style merging is ivy only feature').it('should perform map bindings', () => {
 | |
|       @Component({
 | |
|         template: `<div [class]="{dynamic: true}" 
 | |
|                         [style]="{color: 'blue', width: '100px'}"></div>`
 | |
|       })
 | |
|       class Cmp {
 | |
|       }
 | |
| 
 | |
|       TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|       const fixture = TestBed.createComponent(Cmp);
 | |
|       fixture.detectChanges();
 | |
| 
 | |
|       const div = fixture.nativeElement.querySelector('div');
 | |
|       expect(getSortedClassName(div)).toEqual('dynamic');
 | |
|       expect(getSortedStyle(div)).toEqual('color: blue; width: 100px;');
 | |
|     });
 | |
| 
 | |
|     onlyInIvy('style merging is ivy only feature')
 | |
|         .it('should perform interpolation bindings', () => {
 | |
|           @Component({
 | |
|             // TODO(misko): change `style-x` to `style` once #34202 lands
 | |
|             template: `<div class="static {{'dynamic'}}"
 | |
|                         style.color="blu{{'e'}}"
 | |
|                         style-x="width: {{'100'}}px"></div>`
 | |
|           })
 | |
|           class Cmp {
 | |
|           }
 | |
| 
 | |
|           TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|           const fixture = TestBed.createComponent(Cmp);
 | |
|           fixture.detectChanges();
 | |
| 
 | |
|           const div = fixture.nativeElement.querySelector('div');
 | |
|           expect(getSortedClassName(div)).toEqual('dynamic static');
 | |
|           expect(getSortedStyle(div)).toEqual('color: blue;');
 | |
|         });
 | |
| 
 | |
|     onlyInIvy('style merging is ivy only feature').it('should support hostBindings', () => {
 | |
|       @Component({
 | |
|         template:
 | |
|             `<div my-host-bindings-2 my-host-bindings-1 class="STATIC" style="color: blue"></div>`
 | |
|       })
 | |
|       class Cmp {
 | |
|       }
 | |
|       @Directive({
 | |
|         selector: '[my-host-bindings-1]',
 | |
|         host: {'class': 'HOST_STATIC_1', 'style': 'font-family: "c1"'}
 | |
|       })
 | |
|       class Dir1 {
 | |
|       }
 | |
| 
 | |
|       @Directive({
 | |
|         selector: '[my-host-bindings-2]',
 | |
|         host: {'class': 'HOST_STATIC_2', 'style': 'font-family: "c2"'}
 | |
|       })
 | |
|       class Dir2 {
 | |
|       }
 | |
| 
 | |
|       TestBed.configureTestingModule({
 | |
|         declarations: [
 | |
|           // Order of directives in the template does not matter.
 | |
|           // Order of declarations matters as it determines the relative priority for overrides.
 | |
|           Dir1,
 | |
|           Dir2,
 | |
|           // Even thought component is at the end, it will still have lowest priority because
 | |
|           // components are special that way.
 | |
|           Cmp,
 | |
|         ]
 | |
|       });
 | |
|       const fixture = TestBed.createComponent(Cmp);
 | |
|       fixture.detectChanges();
 | |
| 
 | |
|       const div = fixture.nativeElement.querySelector('div');
 | |
|       expect(getSortedClassName(div)).toEqual('HOST_STATIC_1 HOST_STATIC_2 STATIC');
 | |
|       expect(getSortedStyle(div)).toEqual('color: blue; font-family: c2;');
 | |
|     });
 | |
| 
 | |
|     it('should support hostBindings inheritance', () => {
 | |
|       @Component({template: `<div my-host-bindings class="STATIC" style="color: blue;"></div>`})
 | |
|       class Cmp {
 | |
|       }
 | |
|       @Directive({host: {'class': 'SUPER_STATIC', 'style': 'font-family: "super"; width: "1px";'}})
 | |
|       class SuperDir {
 | |
|       }
 | |
|       @Directive({
 | |
|         selector: '[my-host-bindings]',
 | |
|         host: {'class': 'HOST_STATIC', 'style': 'font-family: "host font"'}
 | |
|       })
 | |
|       class Dir extends SuperDir {
 | |
|       }
 | |
| 
 | |
|       TestBed.configureTestingModule({declarations: [Cmp, Dir]});
 | |
|       const fixture = TestBed.createComponent(Cmp);
 | |
|       fixture.detectChanges();
 | |
| 
 | |
|       const div = fixture.nativeElement.querySelector('div');
 | |
|       expect(getSortedClassName(div))
 | |
|           .toEqual(ivyEnabled ? 'HOST_STATIC STATIC SUPER_STATIC' : 'HOST_STATIC STATIC');
 | |
|       // Browsers keep the '"' around the font name, but Domino removes it some we do search and
 | |
|       // replace. Yes we could do `replace(/"/g, '')` but that fails on android.
 | |
|       expect(getSortedStyle(div).replace('"', '').replace('"', ''))
 | |
|           .toEqual(
 | |
|               ivyEnabled ? 'color: blue; font-family: host font; width: 1px;' :
 | |
|                            'color: blue; font-family: host font;');
 | |
|     });
 | |
| 
 | |
|     onlyInIvy('style merging is ivy only feature')
 | |
|         .it('should apply template classes in correct order', () => {
 | |
|           @Component({
 | |
|             template: `
 | |
|         <div class="STATIC DELETE_MAP_A DELETE_PROP_B"
 | |
|              [class]="{foo: true, DELETE_MAP_A: false}"
 | |
|              [class.bar]="true"
 | |
|              [class.DELETE_PROP_B]="false"></div>
 | |
|         `
 | |
|           })
 | |
|           class Cmp {
 | |
|           }
 | |
| 
 | |
|           TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|           const fixture = TestBed.createComponent(Cmp);
 | |
|           fixture.detectChanges();
 | |
| 
 | |
|           const classDiv = fixture.nativeElement.querySelector('div');
 | |
|           expect(getSortedClassName(classDiv)).toEqual('STATIC bar foo');
 | |
|         });
 | |
| 
 | |
|     onlyInIvy('style merging is ivy only feature')
 | |
|         .it('should apply template styles in correct order', () => {
 | |
|           @Component({
 | |
|             template: `
 | |
|         <div style="width: 100px; height: 200px: color: red; background-color: yellow"
 | |
|              [style]="{width: '110px', height: null}"
 | |
|              [style.color]=" 'blue' "
 | |
|              [style.height.px]="undefined"></div>
 | |
|         `
 | |
|           })
 | |
|           class Cmp {
 | |
|           }
 | |
| 
 | |
|           TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|           const fixture = TestBed.createComponent(Cmp);
 | |
|           fixture.detectChanges();
 | |
| 
 | |
|           const styleDiv = fixture.nativeElement.querySelector('div');
 | |
|           expect(getSortedStyle(styleDiv))
 | |
|               .toEqual('background-color: yellow; color: blue; width: 110px;');
 | |
|         });
 | |
| 
 | |
|     it('should work with ngClass/ngStyle', () => {
 | |
|       @Component(
 | |
|           {template: `<div [ngClass]="['dynamic']" [ngStyle]="{'font-family': 'dynamic'}"></div>`})
 | |
|       class Cmp {
 | |
|       }
 | |
|       TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|       const fixture = TestBed.createComponent(Cmp);
 | |
|       fixture.detectChanges();
 | |
| 
 | |
|       const div = fixture.nativeElement.querySelector('div');
 | |
|       expect(getSortedClassName(div)).toEqual('dynamic');
 | |
|       expect(getSortedStyle(div)).toEqual('font-family: dynamic;');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('css variables', () => {
 | |
|     onlyInIvy('css variables').it('should support css variables', () => {
 | |
|       // This test only works in browsers which support CSS variables.
 | |
|       if (!(typeof getComputedStyle !== 'undefined' && typeof CSS !== 'undefined' &&
 | |
|             typeof CSS.supports !== 'undefined' && CSS.supports('color', 'var(--fake-var)')))
 | |
|         return;
 | |
|       @Component({
 | |
|         template: `
 | |
|             <div [style.--my-var]=" '100px' ">
 | |
|               <span style="width: var(--my-var)">CONTENT</span>
 | |
|             </div>`
 | |
|       })
 | |
|       class Cmp {
 | |
|       }
 | |
|       TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|       const fixture = TestBed.createComponent(Cmp);
 | |
|       // document.body.appendChild(fixture.nativeElement);
 | |
|       fixture.detectChanges();
 | |
| 
 | |
|       const span = fixture.nativeElement.querySelector('span') as HTMLElement;
 | |
|       expect(getComputedStyle(span).getPropertyValue('width')).toEqual('100px');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   modifiedInIvy('shadow bindings include static portion')
 | |
|       .it('should bind [class] as input to directive', () => {
 | |
|         // VE Behavior https://stackblitz.com/edit/angular-cycpsf
 | |
|         // IVY behavior is slightly different see next test with same name.
 | |
|         @Component({
 | |
|           template: `
 | |
|       <div class="s1" [class]=" 'd1' " dir-shadows-class-input></div>
 | |
|       <div class="s2 {{'d2'}}" dir-shadows-class-input></div>
 | |
|       `
 | |
|         })
 | |
|         class Cmp {
 | |
|         }
 | |
| 
 | |
|         @Directive({selector: '[dir-shadows-class-input]'})
 | |
|         class DirectiveShadowsClassInput {
 | |
|           constructor(private elementRef: ElementRef) {}
 | |
|           @Input('class')
 | |
|           set klass(value: string) {
 | |
|             this.elementRef.nativeElement.setAttribute('shadow-class', value);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]});
 | |
|         const fixture = TestBed.createComponent(Cmp);
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         const [div1, div2] = fixture.nativeElement.querySelectorAll('div');
 | |
|         // Static value `class="s1"` is always written to the DOM.
 | |
|         expect(div1.className).toEqual('s1');
 | |
|         // VE passes the dynamic portion of `class` to the directive.
 | |
|         expect(div1.getAttribute('shadow-class')).toEqual('d1');
 | |
|         // Interpolation `class="s2 {{'d2'}}"` does not have a static portion and so no value is
 | |
|         // written to DOM.
 | |
|         expect(div2.className).toEqual('');
 | |
|         expect(div2.getAttribute('shadow-class')).toEqual('s2 d2');
 | |
|       });
 | |
| 
 | |
| 
 | |
|   onlyInIvy('shadow bindings include static portion')
 | |
|       .it('should bind [class] as input to directive', () => {
 | |
|         // VE Behavior https://stackblitz.com/edit/angular-cycpsf
 | |
|         // IVY behavior is slightly different see next test with same name.
 | |
|         @Component({
 | |
|           template: `
 | |
|       <div class="s1" [class]=" 'd1' " dir-shadows-class-input></div>
 | |
|       <div class="s2 {{'d2'}}" dir-shadows-class-input></div>
 | |
|       `
 | |
|         })
 | |
|         class Cmp {
 | |
|         }
 | |
| 
 | |
|         @Directive({selector: '[dir-shadows-class-input]'})
 | |
|         class DirectiveShadowsClassInput {
 | |
|           constructor(private elementRef: ElementRef) {}
 | |
|           @Input('class')
 | |
|           set klass(value: string) {
 | |
|             this.elementRef.nativeElement.setAttribute('shadow-class', value);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]});
 | |
|         const fixture = TestBed.createComponent(Cmp);
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         const [div1, div2] = fixture.nativeElement.querySelectorAll('div');
 | |
|         // Static value `class="s1"` is always written to the DOM.
 | |
|         expect(div1.className).toEqual('s1');
 | |
|         // VE has weird behavior where it calls the @Input('class') with either `class="static` or
 | |
|         // `[class]="dynamic"` but never both. This is determined at compile time. Due to locality
 | |
|         // we don't know if `[class]` is coming if we see `class` only. So we need to combine the
 | |
|         // static and dynamic parte. This results in slightly different calling sequence, but should
 | |
|         // result in the same final DOM.
 | |
|         expect(div1.getAttribute('shadow-class')).toEqual('s1 d1');
 | |
| 
 | |
|         expect(div2.className).toEqual('');
 | |
|         expect(div2.getAttribute('shadow-class')).toEqual('s2 d2');
 | |
|       });
 | |
| 
 | |
| 
 | |
|   modifiedInIvy('shadow bindings include static portion')
 | |
|       .it('should bind [style] as input to directive', () => {
 | |
|         // VE Behavior https://stackblitz.com/edit/angular-cycpsf
 | |
|         @Component({
 | |
|           template: `
 | |
|           <div style="color: red;" [style]=" 'width: 100px;' " dir-shadows-style-input></div>
 | |
|           `
 | |
|         })
 | |
|         class Cmp {
 | |
|         }
 | |
| 
 | |
|         @Directive({selector: '[dir-shadows-style-input]'})
 | |
|         class DirectiveShadowsClassInput {
 | |
|           constructor(private elementRef: ElementRef) {}
 | |
|           @Input('style')
 | |
|           set style(value: string) {
 | |
|             this.elementRef.nativeElement.setAttribute('shadow-style', value);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]});
 | |
|         const fixture = TestBed.createComponent(Cmp);
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         const div = fixture.nativeElement.querySelector('div');
 | |
|         expect(div.style.cssText).toEqual('color: red;');
 | |
|         // VE has weird behavior where it calls the @Input('class') with either `class="static` or
 | |
|         // `[class]="dynamic"` but never both. This is determined at compile time. Due to locality
 | |
|         // we
 | |
|         // don't know if `[class]` is coming if we see `class` only. So we need to combine the two
 | |
|         // This results in slightly different calling sequence, but should result in the same final
 | |
|         // DOM.
 | |
|         expect(div.getAttribute('shadow-style')).toEqual('width: 100px;');
 | |
|       });
 | |
| 
 | |
|   onlyInIvy('shadow bindings include static portion')
 | |
|       .it('should bind [style] as input to directive', () => {
 | |
|         // VE Behavior https://stackblitz.com/edit/angular-cycpsf
 | |
|         @Component({
 | |
|           template: `
 | |
|           <div style="color: red;" [style]=" 'width: 100px;' " dir-shadows-style-input></div>
 | |
|           `
 | |
|         })
 | |
|         class Cmp {
 | |
|         }
 | |
| 
 | |
|         @Directive({selector: '[dir-shadows-style-input]'})
 | |
|         class DirectiveShadowsClassInput {
 | |
|           constructor(private elementRef: ElementRef) {}
 | |
|           @Input('style')
 | |
|           set style(value: string) {
 | |
|             this.elementRef.nativeElement.setAttribute('shadow-style', value);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]});
 | |
|         const fixture = TestBed.createComponent(Cmp);
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         const div = fixture.nativeElement.querySelector('div');
 | |
|         expect(div.style.cssText).toEqual('color: red;');
 | |
|         // VE has weird behavior where it calls the @Input('class') with either `class="static` or
 | |
|         // `[class]="dynamic"` but never both. This is determined at compile time. Due to locality
 | |
|         // we
 | |
|         // don't know if `[class]` is coming if we see `class` only. So we need to combine the two
 | |
|         // This results in slightly different calling sequence, but should result in the same final
 | |
|         // DOM.
 | |
|         expect(div.getAttribute('shadow-style')).toEqual('color: red; width: 100px;');
 | |
|       });
 | |
| 
 | |
|   it('should prevent circular ExpressionChangedAfterItHasBeenCheckedError on shadow inputs', () => {
 | |
|     @Component({template: `<div class="s1" dir-shadows-class-input></div>`})
 | |
|     class Cmp {
 | |
|     }
 | |
| 
 | |
|     @Directive({selector: '[dir-shadows-class-input]'})
 | |
|     class DirectiveShadowsClassInput {
 | |
|       @Input('class')
 | |
|       klass: string|undefined;
 | |
| 
 | |
|       @HostBinding('class')
 | |
|       get hostClasses() { return `${this.klass} SUFFIX`; }
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     expect(() => fixture.detectChanges()).not.toThrow();
 | |
| 
 | |
|     const div = fixture.nativeElement.querySelector('div');
 | |
|     expect(div.className).toEqual('s1 SUFFIX');
 | |
|   });
 | |
| 
 | |
|   it('should recover from exceptions', () => {
 | |
|     @Component({
 | |
|       template: `
 | |
|       <div [id]="maybeThrow(id)">
 | |
|         <span my-dir [class]="maybeThrow(klass)" [class.foo]="maybeThrow(foo)"></span>
 | |
|       </div>
 | |
|       `
 | |
|     })
 | |
|     class Cmp {
 | |
|       id = 'throw_id';
 | |
|       klass: string|string[] = 'throw_klass';
 | |
|       foo = `throw_foo`;
 | |
| 
 | |
|       maybeThrow(value: any) {
 | |
|         if (typeof value === 'string' && value.indexOf('throw') === 0) {
 | |
|           throw new Error(value);
 | |
|         }
 | |
|         return value;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let myDirHostBinding = false;
 | |
|     @Directive({selector: '[my-dir]'})
 | |
|     class MyDirective {
 | |
|       @HostBinding('class.myDir')
 | |
|       get myDir(): boolean {
 | |
|         if (myDirHostBinding === false) {
 | |
|           throw new Error('class.myDir');
 | |
|         }
 | |
|         return myDirHostBinding;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp, MyDirective]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     const cmp = fixture.componentInstance;
 | |
|     const div = fixture.nativeElement.querySelector('div');
 | |
|     const span = fixture.nativeElement.querySelector('span');
 | |
| 
 | |
|     expect(() => fixture.detectChanges()).toThrowError(/throw_id/);
 | |
|     expect(div.id).toBeFalsy();
 | |
|     expectClass(span).toEqual({});
 | |
| 
 | |
|     cmp.id = 'myId';
 | |
|     expect(() => fixture.detectChanges()).toThrowError(/throw_klass/);
 | |
|     expect(div.id).toEqual('myId');
 | |
|     expectClass(span).toEqual({});
 | |
| 
 | |
|     cmp.klass = ['BAR'];
 | |
|     expect(() => fixture.detectChanges()).toThrowError(/throw_foo/);
 | |
|     expect(div.id).toEqual('myId');
 | |
|     expectClass(span).toEqual(ivyEnabled ? {BAR: true} : {});
 | |
| 
 | |
|     cmp.foo = 'foo';
 | |
|     expect(() => fixture.detectChanges()).toThrowError(/class.myDir/);
 | |
|     expect(div.id).toEqual('myId');
 | |
|     expectClass(span).toEqual(ivyEnabled ? {BAR: true, foo: true} : {});
 | |
| 
 | |
|     myDirHostBinding = true;
 | |
|     fixture.detectChanges();
 | |
|     expect(div.id).toEqual('myId');
 | |
|     expectClass(span).toEqual({BAR: true, foo: true, myDir: true});
 | |
|   });
 | |
| 
 | |
|   it('should render inline style and class attribute values on the element before a directive is instantiated',
 | |
|      () => {
 | |
|        @Component({
 | |
|          template: `
 | |
|         <div directive-expecting-styling style="width:200px" class="abc xyz"></div>
 | |
|       `
 | |
|        })
 | |
|        class Cmp {
 | |
|        }
 | |
| 
 | |
|        @Directive({selector: '[directive-expecting-styling]'})
 | |
|        class DirectiveExpectingStyling {
 | |
|          constructor(elm: ElementRef) {
 | |
|            const native = elm.nativeElement;
 | |
|            native.setAttribute('data-captured-width', native.style.width);
 | |
|            native.setAttribute('data-captured-classes', native.className);
 | |
|          }
 | |
|        }
 | |
| 
 | |
|        TestBed.configureTestingModule({declarations: [Cmp, DirectiveExpectingStyling]});
 | |
|        const fixture = TestBed.createComponent(Cmp);
 | |
|        fixture.detectChanges();
 | |
| 
 | |
|        const element = fixture.nativeElement.querySelector('div');
 | |
|        expect(element.style.width).toEqual('200px');
 | |
|        expect(element.getAttribute('data-captured-width')).toEqual('200px');
 | |
|        expect(element.className.trim()).toEqual('abc xyz');
 | |
|        expect(element.getAttribute('data-captured-classes')).toEqual('abc xyz');
 | |
|      });
 | |
| 
 | |
|   it('should only render the same initial styling values once before a directive runs', () => {
 | |
|     @Component({
 | |
|       template: `
 | |
|         <div directive-expecting-styling style="width:200px" class="abc"></div>
 | |
|       `
 | |
|     })
 | |
|     class Cmp {
 | |
|     }
 | |
| 
 | |
|     @Directive({selector: '[directive-expecting-styling]'})
 | |
|     class DirectiveExpectingStyling {
 | |
|       constructor(elm: ElementRef) {
 | |
|         const native = elm.nativeElement;
 | |
|         native.style.width = '300px';
 | |
|         native.classList.remove('abc');
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp, DirectiveExpectingStyling]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const element = fixture.nativeElement.querySelector('div');
 | |
|     expect(element.style.width).toEqual('300px');
 | |
|     expect(element.classList.contains('abc')).toBeFalsy();
 | |
|   });
 | |
| 
 | |
|   it('should ensure that static classes are assigned to ng-container elements and picked up for content projection',
 | |
|      () => {
 | |
|        @Component({
 | |
|          template: `
 | |
|             <project>
 | |
|               outer
 | |
|               <ng-container class="inner">
 | |
|                 inner
 | |
|               </ng-container>
 | |
|             </project>
 | |
|           `
 | |
|        })
 | |
|        class MyApp {
 | |
|        }
 | |
| 
 | |
|        @Component({
 | |
|          selector: 'project',
 | |
|          template: `
 | |
|             <div class="outer-area">
 | |
|               <ng-content></ng-content>
 | |
|             </div>
 | |
|             <div class="inner-area">
 | |
|               <ng-content select=".inner"></ng-content>
 | |
|             </div>
 | |
|           `
 | |
|        })
 | |
|        class ProjectCmp {
 | |
|        }
 | |
| 
 | |
|        TestBed.configureTestingModule({declarations: [MyApp, ProjectCmp]});
 | |
|        const fixture = TestBed.createComponent(MyApp);
 | |
|        const element = fixture.nativeElement;
 | |
|        fixture.detectChanges();
 | |
| 
 | |
|        const inner = element.querySelector('.inner-area');
 | |
|        expect(inner.textContent.trim()).toEqual('inner');
 | |
|        const outer = element.querySelector('.outer-area');
 | |
|        expect(outer.textContent.trim()).toEqual('outer');
 | |
|      });
 | |
| 
 | |
|   it('should render initial styling for repeated nodes that a component host', () => {
 | |
|     @Component({
 | |
|       selector: '[comp]',
 | |
|       template: '',
 | |
|     })
 | |
|     class Comp {
 | |
|     }
 | |
| 
 | |
|     @Component({
 | |
|       template: `
 | |
|         <ng-template ngFor [ngForOf]="items" let-item>
 | |
|           <p comp class="a">A</p>
 | |
|         </ng-template>
 | |
|       `
 | |
|     })
 | |
|     class App {
 | |
|       items = [1, 2, 3];
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({
 | |
|       declarations: [App, Comp],
 | |
|     });
 | |
|     const fixture = TestBed.createComponent(App);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(fixture.debugElement.queryAll(By.css('.a')).length).toBe(3);
 | |
|   });
 | |
| 
 | |
|   it('should do nothing for empty style bindings', () => {
 | |
|     @Component({template: '<div [style.color]></div>'})
 | |
|     class App {
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [App]});
 | |
|     const fixture = TestBed.createComponent(App);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(fixture.nativeElement.innerHTML).toBe('<div></div>');
 | |
|   });
 | |
| 
 | |
|   it('should do nothing for empty class bindings', () => {
 | |
|     @Component({template: '<div [class.is-open]></div>'})
 | |
|     class App {
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [App]});
 | |
|     const fixture = TestBed.createComponent(App);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(fixture.nativeElement.innerHTML).toBe('<div></div>');
 | |
|   });
 | |
| 
 | |
|   it('should be able to bind zero', () => {
 | |
|     @Component({template: '<div #div [style.opacity]="opacity"></div>'})
 | |
|     class App {
 | |
|       @ViewChild('div') div !: ElementRef<HTMLElement>;
 | |
|       opacity = 0;
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [App]});
 | |
|     const fixture = TestBed.createComponent(App);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(fixture.componentInstance.div.nativeElement.style.opacity).toBe('0');
 | |
|   });
 | |
| 
 | |
|   it('should be able to bind a SafeValue to backgroundImage', () => {
 | |
|     @Component({template: '<div [style.backgroundImage]="image"></div>'})
 | |
|     class Cmp {
 | |
|       image !: SafeStyle;
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     const sanitizer: DomSanitizer = TestBed.inject(DomSanitizer);
 | |
| 
 | |
|     fixture.componentInstance.image = sanitizer.bypassSecurityTrustStyle('url("#test")');
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const div = fixture.nativeElement.querySelector('div') as HTMLDivElement;
 | |
|     expect(div.style.backgroundImage).toBe('url("#test")');
 | |
| 
 | |
|     onlyInIvy('perf counters').expectPerfCounters({
 | |
|       rendererSetStyle: 1,
 | |
|       tNode: 3,
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   it('should not write to the native element if a directive shadows the class input', () => {
 | |
|     // This ex is a bit contrived. In real apps, you might have a shared class that is extended
 | |
|     // both by components with host elements and by directives on template nodes. In that case, the
 | |
|     // host styles for the template directives should just be ignored.
 | |
|     @Directive({selector: 'ng-template[styleDir]', host: {'[style.display]': 'display'}})
 | |
|     class StyleDir {
 | |
|       display = 'block';
 | |
|     }
 | |
| 
 | |
|     @Component({selector: 'app-comp', template: `<ng-template styleDir></ng-template>`})
 | |
|     class MyApp {
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [MyApp, StyleDir]});
 | |
|     TestBed.createComponent(MyApp).detectChanges();
 | |
|   });
 | |
| 
 | |
|   it('should be able to bind a SafeValue to clip-path', () => {
 | |
|     @Component({template: '<div [style.clip-path]="path"></div>'})
 | |
|     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.*\)/);
 | |
|   });
 | |
| 
 | |
|   it('should support interpolations inside a class binding', () => {
 | |
|     @Component({
 | |
|       template: `
 | |
|         <div class="a{{one}}b{{two}}c{{three}}d{{four}}e{{five}}f{{six}}g{{seven}}h{{eight}}i{{nine}}j"></div>
 | |
|         <div class="a{{one}}b{{two}}c{{three}}d{{four}}e{{five}}f{{six}}g{{seven}}h{{eight}}i"></div>
 | |
|         <div class="a{{one}}b{{two}}c{{three}}d{{four}}e{{five}}f{{six}}g{{seven}}h"></div>
 | |
|         <div class="a{{one}}b{{two}}c{{three}}d{{four}}e{{five}}f{{six}}g"></div>
 | |
|         <div class="a{{one}}b{{two}}c{{three}}d{{four}}e{{five}}f"></div>
 | |
|         <div class="a{{one}}b{{two}}c{{three}}d{{four}}e"></div>
 | |
|         <div class="a{{one}}b{{two}}c{{three}}d"></div>
 | |
|         <div class="a{{one}}b{{two}}c"></div>
 | |
|         <div class="a{{one}}b"></div>
 | |
|         <div class="{{one}}"></div>
 | |
|       `
 | |
|     })
 | |
|     class Cmp {
 | |
|       one = 'one';
 | |
|       two = 'two';
 | |
|       three = 'three';
 | |
|       four = 'four';
 | |
|       five = 'five';
 | |
|       six = 'six';
 | |
|       seven = 'seven';
 | |
|       eight = 'eight';
 | |
|       nine = 'nine';
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     const instance = fixture.componentInstance;
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const divs = fixture.nativeElement.querySelectorAll('div');
 | |
| 
 | |
|     expect(divs[0].getAttribute('class')).toBe('aonebtwocthreedfourefivefsixgsevenheightininej');
 | |
|     expect(divs[1].getAttribute('class')).toBe('aonebtwocthreedfourefivefsixgsevenheighti');
 | |
|     expect(divs[2].getAttribute('class')).toBe('aonebtwocthreedfourefivefsixgsevenh');
 | |
|     expect(divs[3].getAttribute('class')).toBe('aonebtwocthreedfourefivefsixg');
 | |
|     expect(divs[4].getAttribute('class')).toBe('aonebtwocthreedfourefivef');
 | |
|     expect(divs[5].getAttribute('class')).toBe('aonebtwocthreedfoure');
 | |
|     expect(divs[6].getAttribute('class')).toBe('aonebtwocthreed');
 | |
|     expect(divs[7].getAttribute('class')).toBe('aonebtwoc');
 | |
|     expect(divs[8].getAttribute('class')).toBe('aoneb');
 | |
|     expect(divs[9].getAttribute('class')).toBe('one');
 | |
| 
 | |
|     instance.one = instance.two = instance.three = instance.four = instance.five = instance.six =
 | |
|         instance.seven = instance.eight = instance.nine = '';
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(divs[0].getAttribute('class')).toBe('abcdefghij');
 | |
|     expect(divs[1].getAttribute('class')).toBe('abcdefghi');
 | |
|     expect(divs[2].getAttribute('class')).toBe('abcdefgh');
 | |
|     expect(divs[3].getAttribute('class')).toBe('abcdefg');
 | |
|     expect(divs[4].getAttribute('class')).toBe('abcdef');
 | |
|     expect(divs[5].getAttribute('class')).toBe('abcde');
 | |
|     expect(divs[6].getAttribute('class')).toBe('abcd');
 | |
|     expect(divs[7].getAttribute('class')).toBe('abc');
 | |
|     expect(divs[8].getAttribute('class')).toBe('ab');
 | |
|     expect(divs[9].getAttribute('class')).toBeFalsy();
 | |
|   });
 | |
| 
 | |
|   it('should support interpolations inside a class binding when other classes are present', () => {
 | |
|     @Component({template: '<div class="zero i-{{one}} {{two}} three"></div>'})
 | |
|     class Cmp {
 | |
|       one = 'one';
 | |
|       two = 'two';
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     fixture.detectChanges();
 | |
|     const classList = fixture.nativeElement.querySelector('div').classList;
 | |
| 
 | |
|     expect(classList).toContain('zero');
 | |
|     expect(classList).toContain('i-one');
 | |
|     expect(classList).toContain('two');
 | |
|     expect(classList).toContain('three');
 | |
| 
 | |
|     fixture.componentInstance.one = fixture.componentInstance.two = '';
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(classList).toContain('zero');
 | |
|     expect(classList).toContain('i-');
 | |
|     expect(classList).toContain('three');
 | |
|     expect(classList).not.toContain('i-one');
 | |
|     expect(classList).not.toContain('two');
 | |
|   });
 | |
| 
 | |
|   it('should support interpolations inside a style property binding', () => {
 | |
|     @Component({
 | |
|       template: `
 | |
|         <div style.font-family="f{{one}}{{two}}{{three}}{{four}}{{five}}{{six}}{{seven}}{{eight}}{{nine}}"></div>
 | |
|         <div style.font-family="f{{one}}{{two}}{{three}}{{four}}{{five}}{{six}}{{seven}}{{eight}}"></div>
 | |
|         <div style.font-family="f{{one}}{{two}}{{three}}{{four}}{{five}}{{six}}{{seven}}"></div>
 | |
|         <div style.font-family="f{{one}}{{two}}{{three}}{{four}}{{five}}{{six}}"></div>
 | |
|         <div style.font-family="f{{one}}{{two}}{{three}}{{four}}{{five}}"></div>
 | |
|         <div style.font-family="f{{one}}{{two}}{{three}}{{four}}"></div>
 | |
|         <div style.font-family="f{{one}}{{two}}{{three}}"></div>
 | |
|         <div style.font-family="f{{one}}{{two}}"></div>
 | |
|         <div style.font-family="f{{one}}"></div>
 | |
|         <div style.width="{{singleBinding}}"></div>
 | |
|       `
 | |
|     })
 | |
|     class Cmp {
 | |
|       singleBinding: string|null = '1337px';
 | |
|       one = 1;
 | |
|       two = 2;
 | |
|       three = 3;
 | |
|       four = 4;
 | |
|       five = 5;
 | |
|       six = 6;
 | |
|       seven = 7;
 | |
|       eight = 8;
 | |
|       nine = 9;
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     const instance = fixture.componentInstance;
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const divs: NodeListOf<HTMLElement> = fixture.nativeElement.querySelectorAll('div');
 | |
| 
 | |
|     expect(divs[0].style.fontFamily).toBe('f123456789');
 | |
|     expect(divs[1].style.fontFamily).toBe('f12345678');
 | |
|     expect(divs[2].style.fontFamily).toBe('f1234567');
 | |
|     expect(divs[3].style.fontFamily).toBe('f123456');
 | |
|     expect(divs[4].style.fontFamily).toBe('f12345');
 | |
|     expect(divs[5].style.fontFamily).toBe('f1234');
 | |
|     expect(divs[6].style.fontFamily).toBe('f123');
 | |
|     expect(divs[7].style.fontFamily).toBe('f12');
 | |
|     expect(divs[8].style.fontFamily).toBe('f1');
 | |
|     expect(divs[9].style.width).toBe('1337px');
 | |
| 
 | |
|     instance.singleBinding = null;
 | |
|     instance.one = instance.two = instance.three = instance.four = instance.five = instance.six =
 | |
|         instance.seven = instance.eight = instance.nine = 1;
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(divs[0].style.fontFamily).toBe('f111111111');
 | |
|     expect(divs[1].style.fontFamily).toBe('f11111111');
 | |
|     expect(divs[2].style.fontFamily).toBe('f1111111');
 | |
|     expect(divs[3].style.fontFamily).toBe('f111111');
 | |
|     expect(divs[4].style.fontFamily).toBe('f11111');
 | |
|     expect(divs[5].style.fontFamily).toBe('f1111');
 | |
|     expect(divs[6].style.fontFamily).toBe('f111');
 | |
|     expect(divs[7].style.fontFamily).toBe('f11');
 | |
|     expect(divs[8].style.fontFamily).toBe('f1');
 | |
|     expect(divs[9].style.width).toBeFalsy();
 | |
|   });
 | |
| 
 | |
|   it('should support interpolations when a style property has a unit suffix', () => {
 | |
|     @Component({template: '<div style.width.px="{{one}}{{three}}{{three}}7"></div>'})
 | |
|     class Cmp {
 | |
|       one = 1;
 | |
|       three = 3;
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     fixture.detectChanges();
 | |
|     const div = fixture.nativeElement.querySelector('div');
 | |
| 
 | |
|     expect(div.style.width).toBe('1337px');
 | |
| 
 | |
|     fixture.componentInstance.one = 2;
 | |
|     fixture.componentInstance.three = 6;
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(div.style.width).toBe('2667px');
 | |
|   });
 | |
| 
 | |
|   it('should not write to a `class` input binding in the event that there is no static class value',
 | |
|      () => {
 | |
|        let capturedClassBindingCount = 0;
 | |
|        let capturedClassBindingValue: string|null|undefined = undefined;
 | |
|        let capturedMyClassBindingCount = 0;
 | |
|        let capturedMyClassBindingValue: string|null|undefined = undefined;
 | |
| 
 | |
|        @Component({template: '<div [class]="c" [my-class-dir]="x"></div>'})
 | |
|        class Cmp {
 | |
|          c: any = null;
 | |
|          x = 'foo';
 | |
|        }
 | |
| 
 | |
|        @Directive({selector: '[my-class-dir]'})
 | |
|        class MyClassDir {
 | |
|          @Input('class')
 | |
|          set classVal(v: string) {
 | |
|            capturedClassBindingCount++;
 | |
|            capturedClassBindingValue = v;
 | |
|          }
 | |
| 
 | |
|          @Input('my-class-dir')
 | |
|          set myClassVal(v: string) {
 | |
|            capturedMyClassBindingCount++;
 | |
|            capturedMyClassBindingValue = v;
 | |
|          }
 | |
|        }
 | |
| 
 | |
|        TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]});
 | |
|        const fixture = TestBed.createComponent(Cmp);
 | |
|        fixture.detectChanges();
 | |
| 
 | |
|        expect(capturedClassBindingCount).toEqual(1);
 | |
|        expect(capturedClassBindingValue as any).toEqual(null);
 | |
|        expect(capturedMyClassBindingCount).toEqual(1);
 | |
|        expect(capturedMyClassBindingValue !).toEqual('foo');
 | |
| 
 | |
|        fixture.componentInstance.c = 'dynamic-value';
 | |
|        fixture.detectChanges();
 | |
| 
 | |
|        expect(capturedClassBindingCount).toEqual(2);
 | |
|        expect(capturedClassBindingValue !).toEqual('dynamic-value');
 | |
|        expect(capturedMyClassBindingCount).toEqual(1);
 | |
|        expect(capturedMyClassBindingValue !).toEqual('foo');
 | |
| 
 | |
|        fixture.componentInstance.c = null;
 | |
|        fixture.detectChanges();
 | |
| 
 | |
|        expect(capturedClassBindingCount).toEqual(3);
 | |
|        expect(capturedClassBindingValue as any).toEqual(null);
 | |
|        expect(capturedMyClassBindingCount).toEqual(1);
 | |
|        expect(capturedMyClassBindingValue !).toEqual('foo');
 | |
| 
 | |
|        fixture.componentInstance.c = '';
 | |
|        fixture.detectChanges();
 | |
| 
 | |
|        expect(capturedClassBindingCount).toEqual(4);
 | |
|        expect(capturedClassBindingValue as any).toEqual('');
 | |
|        expect(capturedMyClassBindingCount).toEqual(1);
 | |
|        expect(capturedMyClassBindingValue !).toEqual('foo');
 | |
|      });
 | |
| 
 | |
|   it('should write to [class] binding during `update` mode if there is an instantiation-level value',
 | |
|      () => {
 | |
|        let capturedClassBindingCount = 0;
 | |
|        let capturedClassBindingValue: string|null|undefined = undefined;
 | |
| 
 | |
|        @Component({template: '<div [class]="c" my-class-dir></div>'})
 | |
|        class Cmp {
 | |
|          c: any = 'bar';
 | |
|        }
 | |
| 
 | |
|        @Directive({selector: '[my-class-dir]'})
 | |
|        class MyClassDir {
 | |
|          @Input('class')
 | |
|          set classVal(v: string) {
 | |
|            capturedClassBindingCount++;
 | |
|            capturedClassBindingValue = v;
 | |
|          }
 | |
|        }
 | |
| 
 | |
|        TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]});
 | |
|        const fixture = TestBed.createComponent(Cmp);
 | |
|        expect(capturedClassBindingCount).toEqual(0);
 | |
|        fixture.detectChanges();
 | |
| 
 | |
|        expect(capturedClassBindingCount).toEqual(1);
 | |
|        expect(capturedClassBindingValue as any).toEqual('bar');
 | |
| 
 | |
|        fixture.componentInstance.c = 'dynamic-bar';
 | |
|        fixture.detectChanges();
 | |
| 
 | |
|        expect(capturedClassBindingCount).toEqual(2);
 | |
|        expect(capturedClassBindingValue !).toEqual('dynamic-bar');
 | |
|      });
 | |
| 
 | |
|   it('should write to a `class` input binding if there is a static class value', () => {
 | |
|     let capturedClassBindingCount = 0;
 | |
|     let capturedClassBindingValue: string|null = null;
 | |
|     let capturedMyClassBindingCount = 0;
 | |
|     let capturedMyClassBindingValue: string|null = null;
 | |
| 
 | |
|     @Component({template: '<div class="static-val" [my-class-dir]="x"></div>'})
 | |
|     class Cmp {
 | |
|       x = 'foo';
 | |
|     }
 | |
| 
 | |
|     @Directive({selector: '[my-class-dir]'})
 | |
|     class MyClassDir {
 | |
|       @Input('class')
 | |
|       set classVal(v: string) {
 | |
|         capturedClassBindingCount++;
 | |
|         capturedClassBindingValue = v;
 | |
|       }
 | |
| 
 | |
|       @Input('my-class-dir')
 | |
|       set myClassVal(v: string) {
 | |
|         capturedMyClassBindingCount++;
 | |
|         capturedMyClassBindingValue = v;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(capturedClassBindingValue !).toEqual('static-val');
 | |
|     expect(capturedClassBindingCount).toEqual(1);
 | |
|     expect(capturedMyClassBindingValue !).toEqual('foo');
 | |
|     expect(capturedMyClassBindingCount).toEqual(1);
 | |
|   });
 | |
| 
 | |
|   it('should write to a `className` input binding', () => {
 | |
|     @Component({
 | |
|       selector: 'comp',
 | |
|       template: `{{className}}`,
 | |
|     })
 | |
|     class Comp {
 | |
|       @Input() className: string = '';
 | |
|     }
 | |
|     @Component({
 | |
|       template: `<comp [className]="'my-className'"></comp>`,
 | |
|     })
 | |
|     class App {
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Comp, App]});
 | |
|     const fixture = TestBed.createComponent(App);
 | |
|     fixture.detectChanges();
 | |
|     expect(fixture.debugElement.nativeElement.firstChild.innerHTML).toBe('my-className');
 | |
|   });
 | |
| 
 | |
|   onlyInIvy('only ivy combines static and dynamic class-related attr values')
 | |
|       .it('should write to a `className` input binding, when static `class` is present', () => {
 | |
|         @Component({
 | |
|           selector: 'comp',
 | |
|           template: `{{className}}`,
 | |
|         })
 | |
|         class Comp {
 | |
|           @Input() className: string = '';
 | |
|         }
 | |
| 
 | |
|         @Component({
 | |
|           template: `<comp class="static" [className]="'my-className'"></comp>`,
 | |
|         })
 | |
|         class App {
 | |
|         }
 | |
| 
 | |
|         TestBed.configureTestingModule({declarations: [Comp, App]});
 | |
|         const fixture = TestBed.createComponent(App);
 | |
|         fixture.detectChanges();
 | |
|         expect(fixture.debugElement.nativeElement.firstChild.innerHTML).toBe('static my-className');
 | |
|       });
 | |
| 
 | |
|   onlyInIvy('in Ivy [class] and [className] bindings on the same element are not allowed')
 | |
|       .it('should throw an error in case [class] and [className] bindings are used on the same element',
 | |
|           () => {
 | |
|             @Component({
 | |
|               selector: 'comp',
 | |
|               template: `{{class}} - {{className}}`,
 | |
|             })
 | |
|             class Comp {
 | |
|               @Input() class: string = '';
 | |
|               @Input() className: string = '';
 | |
|             }
 | |
|             @Component({
 | |
|               template: `<comp [class]="'my-class'" [className]="'className'"></comp>`,
 | |
|             })
 | |
|             class App {
 | |
|             }
 | |
| 
 | |
|             TestBed.configureTestingModule({declarations: [Comp, App]});
 | |
|             expect(() => {
 | |
|               const fixture = TestBed.createComponent(App);
 | |
|               fixture.detectChanges();
 | |
|             })
 | |
|                 .toThrowError(
 | |
|                     '[class] and [className] bindings cannot be used on the same element simultaneously');
 | |
|           });
 | |
| 
 | |
|   onlyInIvy('only ivy persists static class/style attrs with their binding counterparts')
 | |
|       .it('should write to a `class` input binding if there is a static class value and there is a binding value',
 | |
|           () => {
 | |
|             let capturedClassBindingCount = 0;
 | |
|             let capturedClassBindingValue: string|null = null;
 | |
|             let capturedMyClassBindingCount = 0;
 | |
|             let capturedMyClassBindingValue: string|null = null;
 | |
| 
 | |
|             @Component({template: '<div class="static-val" [class]="c" [my-class-dir]="x"></div>'})
 | |
|             class Cmp {
 | |
|               c: any = null;
 | |
|               x: any = 'foo';
 | |
|             }
 | |
| 
 | |
|             @Directive({selector: '[my-class-dir]'})
 | |
|             class MyClassDir {
 | |
|               @Input('class')
 | |
|               set classVal(v: string) {
 | |
|                 capturedClassBindingCount++;
 | |
|                 capturedClassBindingValue = v;
 | |
|               }
 | |
| 
 | |
|               @Input('my-class-dir')
 | |
|               set myClassVal(v: string) {
 | |
|                 capturedMyClassBindingCount++;
 | |
|                 capturedMyClassBindingValue = v;
 | |
|               }
 | |
|             }
 | |
| 
 | |
|             TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]});
 | |
|             const fixture = TestBed.createComponent(Cmp);
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             expect(capturedClassBindingCount)
 | |
|                 .toEqual(
 | |
|                     2
 | |
|                     // '2' is not ideal as '1' would be preferred.
 | |
|                     // The reason for two writes is that one is for the  static
 | |
|                     // `class="static-val"` and one for `[class]="c"`. This means that
 | |
|                     // `class="static-val"` is written during the create block which is not ideal.
 | |
|                     // To do this correctly we would have to delay the `class="static-val"` until
 | |
|                     // the update block, but that would be expensive since it would require that we
 | |
|                     // would check if we possibly have this situation on every `advance()`
 | |
|                     // instruction. We don't think this is worth it, and we are just going to live
 | |
|                     // with this.
 | |
|                     );
 | |
|             expect(capturedClassBindingValue).toEqual(null);
 | |
|             expect(capturedMyClassBindingCount).toEqual(1);
 | |
|             expect(capturedMyClassBindingValue !).toEqual('foo');
 | |
| 
 | |
|             capturedClassBindingCount = 0;
 | |
|             fixture.componentInstance.c = 'dynamic-val';
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             expect(capturedClassBindingCount).toEqual(1);
 | |
|             expect(capturedClassBindingValue !).toEqual('static-val dynamic-val');
 | |
|             expect(capturedMyClassBindingCount).toEqual(1);
 | |
|             expect(capturedMyClassBindingValue !).toEqual('foo');
 | |
|           });
 | |
| 
 | |
|   onlyInIvy('only ivy balances styling across directives and component host bindings')
 | |
|       .it('should allow multiple directives to set dynamic and static classes independent of one another',
 | |
|           () => {
 | |
|             @Component({
 | |
|               template: `
 | |
|         <div dir-one dir-two></div>
 | |
|       `
 | |
|             })
 | |
|             class Cmp {
 | |
|             }
 | |
| 
 | |
|             @Directive({selector: '[dir-one]', host: {'[class.dir-one]': 'dirOneExp'}})
 | |
|             class DirOne {
 | |
|               dirOneExp = true;
 | |
|             }
 | |
| 
 | |
|             @Directive({selector: '[dir-two]', host: {'class': 'dir-two'}})
 | |
|             class DirTwo {
 | |
|             }
 | |
| 
 | |
|             TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]});
 | |
|             const fixture = TestBed.createComponent(Cmp);
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             const element = fixture.nativeElement.querySelector('div');
 | |
|             expect(element.classList.contains('dir-one')).toBeTruthy();
 | |
|             expect(element.classList.contains('dir-two')).toBeTruthy();
 | |
|           });
 | |
| 
 | |
|   describe('NgClass', () => {
 | |
| 
 | |
|     // We had a bug where NgClass would not allocate sufficient slots for host bindings,
 | |
|     // so it would overwrite information about other directives nearby. This test checks
 | |
|     // that TestDir's injector is not overwritten by NgClass, so TestDir should still
 | |
|     // be found by DI when ChildDir is instantiated.
 | |
|     it('should not overwrite other directive info when using NgClass', () => {
 | |
|       @Directive({selector: '[test-dir]'})
 | |
|       class TestDir {
 | |
|       }
 | |
| 
 | |
|       @Directive({selector: '[child-dir]'})
 | |
|       class ChildDir {
 | |
|         constructor(public parent: TestDir) {}
 | |
|       }
 | |
| 
 | |
|       @Component({
 | |
|         selector: 'app',
 | |
|         template: `
 | |
|           <div class="my-class" [ngClass]="classMap" test-dir>
 | |
|             <div *ngIf="showing" child-dir>Hello</div>
 | |
|           </div>
 | |
|         `
 | |
|       })
 | |
|       class AppComponent {
 | |
|         classMap = {'with-button': true};
 | |
|         showing = false;
 | |
|       }
 | |
| 
 | |
|       TestBed.configureTestingModule({declarations: [AppComponent, TestDir, ChildDir]});
 | |
|       const fixture = TestBed.createComponent(AppComponent);
 | |
|       fixture.detectChanges();
 | |
|       const testDirDiv = fixture.debugElement.nativeElement.querySelector('div');
 | |
|       expect(testDirDiv.classList).toContain('with-button');
 | |
|       expect(fixture.debugElement.nativeElement.textContent).not.toContain('Hello');
 | |
| 
 | |
|       fixture.componentInstance.classMap = {'with-button': false};
 | |
|       fixture.componentInstance.showing = true;
 | |
|       fixture.detectChanges();
 | |
| 
 | |
|       const childDir = fixture.debugElement.query(By.directive(ChildDir)).injector.get(ChildDir);
 | |
|       expect(childDir.parent).toBeAnInstanceOf(TestDir);
 | |
|       expect(testDirDiv.classList).not.toContain('with-button');
 | |
|       expect(fixture.debugElement.nativeElement.textContent).toContain('Hello');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   it('should be able to name inputs starting with `class` or `style`', () => {
 | |
|     @Directive({selector: '[dir]'})
 | |
|     class Dir {
 | |
|       @Input('classesInSchool') classes = '';
 | |
|       @Input('styleOfClothing') style = '';
 | |
|     }
 | |
| 
 | |
|     @Component({
 | |
|       template: '<span dir [classesInSchool]="classes" [styleOfClothing]="style"></span>',
 | |
|     })
 | |
|     class App {
 | |
|       @ViewChild(Dir) dir !: Dir;
 | |
| 
 | |
|       classes = 'math';
 | |
|       style = '80s';
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [App, Dir]});
 | |
|     const fixture = TestBed.createComponent(App);
 | |
|     fixture.detectChanges();
 | |
|     const directive = fixture.componentInstance.dir;
 | |
| 
 | |
|     expect(directive.classes).toBe('math');
 | |
|     expect(directive.style).toBe('80s');
 | |
|   });
 | |
| 
 | |
|   it('should be able to bind to `className`', () => {
 | |
|     @Component({template: ''})
 | |
|     class App {
 | |
|       @HostBinding('className')
 | |
|       klass = 'one two';
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [App]});
 | |
|     const fixture = TestBed.createComponent(App);
 | |
|     fixture.detectChanges();
 | |
|     const classList = fixture.nativeElement.classList;
 | |
| 
 | |
|     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: `
 | |
|         <div [style.width]="w"
 | |
|              [style.height]="h"
 | |
|              [style.opacity]="o"
 | |
|              style="width:200px; height:200px;"
 | |
|              [class.abc]="abc"
 | |
|              [class.xyz]="xyz"></div>
 | |
|       `
 | |
|             })
 | |
|             class Cmp {
 | |
|               w: string|null|undefined = '100px';
 | |
|               h: string|null|undefined = '100px';
 | |
|               o: string|null|undefined = '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 = undefined;
 | |
|             fixture.componentInstance.h = undefined;
 | |
|             fixture.componentInstance.o = undefined;
 | |
|             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();
 | |
| 
 | |
|             fixture.componentInstance.w = null;
 | |
|             fixture.componentInstance.h = null;
 | |
|             fixture.componentInstance.o = null;
 | |
|             fixture.detectChanges();
 | |
|             expect(element.style.width).toBeFalsy();
 | |
|             expect(element.style.height).toBeFalsy();
 | |
|             expect(element.style.opacity).toBeFalsy();
 | |
|           });
 | |
| 
 | |
|   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: `
 | |
|         <div [style.width]="w0"
 | |
|              [dir-that-sets-width]="w1"
 | |
|              [another-dir-that-sets-width]="w2">
 | |
|       `
 | |
|         })
 | |
|         class Cmp {
 | |
|           w0: string|null|undefined = null;
 | |
|           w1: string|null|undefined = null;
 | |
|           w2: string|null|undefined = 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 = undefined;
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         expect(element.style.width).toEqual('300px');
 | |
| 
 | |
|         fixture.componentInstance.w2 = undefined;
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         expect(element.style.width).toEqual('200px');
 | |
| 
 | |
|         fixture.componentInstance.w1 = undefined;
 | |
|         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: `
 | |
|         <comp-with-styling
 | |
|           [style.opacity]="opacity"
 | |
|           dir-with-styling>...</comp-with-styling>
 | |
|       `
 | |
|             })
 | |
|             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');
 | |
| 
 | |
|             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 !.rendererSetStyle).toEqual(4);
 | |
|             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');
 | |
| 
 | |
|             // once for the template flush and again for the host bindings
 | |
|             expect(ngDevMode !.rendererSetStyle).toEqual(4);
 | |
|           });
 | |
| 
 | |
|   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: `
 | |
|         <comp-with-styling
 | |
|           [style.opacity]="opacity"
 | |
|           [style.width]="width"
 | |
|           [class.tpl]="tplClass"
 | |
|           dir-with-styling>...</comp-with-styling>
 | |
|       `
 | |
|             })
 | |
|             class Cmp {
 | |
|               opacity: string|null|undefined = '0.5';
 | |
|               width: string|null|undefined = 'auto';
 | |
|               tplClass = true;
 | |
|             }
 | |
| 
 | |
|             TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]});
 | |
|             const fixture = TestBed.createComponent(Cmp);
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             const element = fixture.nativeElement.querySelector('comp-with-styling');
 | |
| 
 | |
|             expectStyle(element).toEqual({
 | |
|               'color': 'red',
 | |
|               'font-size': '100px',
 | |
|               'height': '900px',
 | |
|               'opacity': '0.5',
 | |
|               'width': 'auto',
 | |
|             });
 | |
|             expectClass(element).toEqual({
 | |
|               'dir': true,
 | |
|               'comp': true,
 | |
|               'tpl': true,
 | |
|             });
 | |
| 
 | |
|             fixture.componentInstance.width = undefined;
 | |
|             fixture.componentInstance.opacity = undefined;
 | |
|             fixture.componentInstance.tplClass = false;
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             expectStyle(element).toEqual(
 | |
|                 {'color': 'red', 'width': '900px', 'height': '900px', 'font-size': '100px'});
 | |
|             expectClass(element).toEqual({
 | |
|               'dir': true,
 | |
|               'comp': true,
 | |
|             });
 | |
| 
 | |
|             fixture.componentInstance.width = null;
 | |
|             fixture.componentInstance.opacity = null;
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             expectStyle(element).toEqual({'color': 'red', 'height': '900px', 'font-size': '100px'});
 | |
|           });
 | |
| 
 | |
|   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: `
 | |
|           <div sub-class-dir [style.width]="w3"></div>
 | |
|       `
 | |
|             })
 | |
|             class Cmp {
 | |
|               w3: string|null|undefined = '300px';
 | |
|             }
 | |
| 
 | |
|             TestBed.configureTestingModule(
 | |
|                 {declarations: [Cmp, SuperClassDirective, SubClassDirective]});
 | |
|             const fixture = TestBed.createComponent(Cmp);
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             const element = fixture.nativeElement.querySelector('div');
 | |
| 
 | |
|             expectStyle(element).toEqual({
 | |
|               'width': '300px',
 | |
|             });
 | |
| 
 | |
|             fixture.componentInstance.w3 = null;
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             expectStyle(element).toEqual({});
 | |
| 
 | |
|             fixture.componentInstance.w3 = undefined;
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             expectStyle(element).toEqual({
 | |
|               'width': '200px',
 | |
|             });
 | |
|           });
 | |
| 
 | |
| 
 | |
|   onlyInIvy('only ivy has style debugging support')
 | |
|       .it('should apply map-based style and class entries', () => {
 | |
|         @Component({template: '<div [style]="s" [class]="c"></div>'})
 | |
|         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');
 | |
|         expectStyle(element).toEqual({width: '100px', height: '200px'});
 | |
|         expectClass(element).toEqual({abc: true});
 | |
| 
 | |
|         comp.reset();
 | |
|         comp.updateStyles('width', '500px');
 | |
|         comp.updateStyles('height', null);
 | |
|         comp.updateClasses('def');
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         expectStyle(element).toEqual({width: '500px'});
 | |
|         expectClass(element).toEqual({def: true});
 | |
|       });
 | |
| 
 | |
|   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: `
 | |
|         <div [style.width]="width"
 | |
|              [style]="map"
 | |
|              style="width:200px; font-size:99px"
 | |
|              dir-that-sets-styling
 | |
|              #dir
 | |
|              [class.xyz]="xyz"></div>
 | |
|       `
 | |
|             })
 | |
|             class Cmp {
 | |
|               map: any = {width: '111px', opacity: '0.5'};
 | |
|               width: string|null|undefined = '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');
 | |
|             expectStyle(element).toEqual({
 | |
|               'width': '555px',
 | |
|               'color': 'red',
 | |
|               'font-size': '99px',
 | |
|               'opacity': '0.5',
 | |
|             });
 | |
| 
 | |
|             comp.width = undefined;
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             expectStyle(element).toEqual({
 | |
|               'width': '111px',
 | |
|               'color': 'red',
 | |
|               'font-size': '99px',
 | |
|               'opacity': '0.5',
 | |
|             });
 | |
| 
 | |
|             comp.map = null;
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             expectStyle(element).toEqual({
 | |
|               'width': '200px',
 | |
|               'color': 'red',
 | |
|               'font-size': '99px',
 | |
|             });
 | |
| 
 | |
|             comp.dir.map = null;
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             expectStyle(element).toEqual({
 | |
|               'width': '200px',
 | |
|               'font-size': '99px',
 | |
|             });
 | |
|           });
 | |
| 
 | |
|   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]', host: {'style': 'width:0px; height:0px'}})
 | |
|             class DirThatSetsStyling {
 | |
|               @HostBinding('style') public map: any = {width: '999px', height: '999px'};
 | |
|             }
 | |
| 
 | |
|             @Component({
 | |
|               template: `
 | |
|                 <div #dir
 | |
|                   [style.width]="width"
 | |
|                   [style.height]="height"
 | |
|                   [style]="map"
 | |
|                   dir-that-sets-styling></div>
 | |
|               `
 | |
|             })
 | |
|             class Cmp {
 | |
|               width: string|null|undefined = '111px';
 | |
|               height: string|null|undefined = '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');
 | |
| 
 | |
|             assertStyleCounters(4, 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 = undefined;
 | |
|             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();
 | |
| 
 | |
|             // No change, hence no write
 | |
|             assertStyleCounters(0, 0);
 | |
|             assertStyle(element, 'width', '123px');
 | |
|             assertStyle(element, 'height', '123px');
 | |
| 
 | |
|             comp.width = undefined;
 | |
|             ngDevModeResetPerfCounters();
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             assertStyleCounters(1, 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: '1100px', color: 'red'};
 | |
|             ngDevModeResetPerfCounters();
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             assertStyleCounters(2, 0);
 | |
|             assertStyle(element, 'width', '1000px');
 | |
|             assertStyle(element, 'height', '123px');
 | |
|             assertStyle(element, 'color', 'red');
 | |
| 
 | |
|             comp.height = undefined;
 | |
|             ngDevModeResetPerfCounters();
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             // height gets applied twice and all other
 | |
|             // values get applied
 | |
|             assertStyleCounters(1, 0);
 | |
|             assertStyle(element, 'width', '1000px');
 | |
|             assertStyle(element, 'height', '1100px');
 | |
|             assertStyle(element, 'color', 'red');
 | |
| 
 | |
|             comp.map = {color: 'blue', width: '2000px', opacity: '0.5'};
 | |
|             ngDevModeResetPerfCounters();
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             assertStyleCounters(3, 0);
 | |
|             assertStyle(element, 'width', '2000px');
 | |
|             assertStyle(element, 'height', '1100px');
 | |
|             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(0, 1);
 | |
|             assertStyle(element, 'width', '2000px');
 | |
|             assertStyle(element, 'height', '1100px');
 | |
|             assertStyle(element, 'color', 'blue');
 | |
|             assertStyle(element, 'opacity', '');
 | |
|           });
 | |
| 
 | |
|   onlyInIvy('only ivy has [style.prop] support')
 | |
|       .it('should sanitize style values before writing them', () => {
 | |
|         @Component({
 | |
|           template: `
 | |
|                     <div [style.width]="widthExp"
 | |
|                          [style.background-image]="bgImageExp"></div>
 | |
|                   `
 | |
|         })
 | |
|         class Cmp {
 | |
|           widthExp = '';
 | |
|           bgImageExp = '';
 | |
|           styleMapExp: any = {};
 | |
|         }
 | |
| 
 | |
|         TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|         const fixture = TestBed.createComponent(Cmp);
 | |
|         const comp = fixture.componentInstance;
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         const div = fixture.nativeElement.querySelector('div');
 | |
| 
 | |
|         comp.bgImageExp = 'url("javascript:img")';
 | |
|         fixture.detectChanges();
 | |
|         // for some reasons `background-image: unsafe` is suppressed
 | |
|         expect(getSortedStyle(div)).toEqual('');
 | |
|         fixture.detectChanges();
 | |
|         expect(getSortedStyle(div)).not.toContain('javascript');
 | |
| 
 | |
|         // Prove that bindings work.
 | |
|         comp.widthExp = '789px';
 | |
|         comp.bgImageExp = bypassSanitizationTrustStyle(comp.bgImageExp) as string;
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         expect(div.style.getPropertyValue('background-image')).toEqual('url("javascript:img")');
 | |
|         expect(div.style.getPropertyValue('width')).toEqual('789px');
 | |
|       });
 | |
| 
 | |
|   onlyInIvy('only ivy has [style] support')
 | |
|       .it('should sanitize style values before writing them', () => {
 | |
|         @Component({
 | |
|           template: `
 | |
|                     <div [style.width]="widthExp"
 | |
|                          [style]="styleMapExp"></div>
 | |
|                   `
 | |
|         })
 | |
|         class Cmp {
 | |
|           widthExp = '';
 | |
|           styleMapExp: {[key: string]: any} = {};
 | |
|         }
 | |
| 
 | |
|         TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|         const fixture = TestBed.createComponent(Cmp);
 | |
|         const comp = fixture.componentInstance;
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         const div = fixture.nativeElement.querySelector('div');
 | |
| 
 | |
|         comp.styleMapExp['background-image'] = 'url("javascript:img")';
 | |
|         fixture.detectChanges();
 | |
|         // for some reasons `background-image: unsafe` is suppressed
 | |
|         expect(getSortedStyle(div)).toEqual('');
 | |
| 
 | |
|         // for some reasons `border-image: unsafe` is NOT suppressed
 | |
|         fixture.detectChanges();
 | |
|         expect(getSortedStyle(div)).not.toContain('javascript');
 | |
| 
 | |
|         // Prove that bindings work.
 | |
|         comp.widthExp = '789px';
 | |
|         comp.styleMapExp = {
 | |
|           'background-image': bypassSanitizationTrustStyle(comp.styleMapExp['background-image'])
 | |
|         };
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         expect(div.style.getPropertyValue('background-image')).toEqual('url("javascript:img")');
 | |
|         expect(div.style.getPropertyValue('width')).toEqual('789px');
 | |
|       });
 | |
| 
 | |
|   it('should apply a unit to a style before writing it', () => {
 | |
|     @Component({
 | |
|       template: `
 | |
|             <div [style.width.px]="widthExp"
 | |
|                  [style.height.em]="heightExp"></div>
 | |
|           `
 | |
|     })
 | |
|     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 div = fixture.nativeElement.querySelector('div');
 | |
| 
 | |
|     comp.widthExp = '200';
 | |
|     comp.heightExp = 10;
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(getSortedStyle(div)).toEqual('height: 10em; width: 200px;');
 | |
| 
 | |
|     comp.widthExp = 0;
 | |
|     comp.heightExp = null;
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(getSortedStyle(div)).toEqual('width: 0px;');
 | |
|   });
 | |
| 
 | |
|   it('should be able to bind a SafeValue to clip-path', () => {
 | |
|     @Component({template: '<div [style.clip-path]="path"></div>'})
 | |
|     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.*\)/);
 | |
|   });
 | |
| 
 | |
|   it('should handle values wrapped into SafeValue', () => {
 | |
|     @Component({
 | |
|       template: `
 | |
|         <!-- Verify sanitizable style prop values wrapped in SafeValue -->
 | |
|         <div [style.background]="getBackgroundSafe()"></div>
 | |
| 
 | |
|         <!-- Verify regular style prop values wrapped in SafeValue -->
 | |
|         <p [style.width]="getWidthSafe()" [style.height]="getHeightSafe()"></p>
 | |
| 
 | |
|         <!-- Verify regular style prop values not wrapped in SafeValue -->
 | |
|         <span [style.color]="getColorUnsafe()"></span>
 | |
|       `,
 | |
|     })
 | |
|     class MyComp {
 | |
|       constructor(private sanitizer: DomSanitizer) {}
 | |
|       public width: string = 'calc(20%)';
 | |
|       public height: string = '10px';
 | |
|       public background: string = '1.png';
 | |
|       public color: string = 'red';
 | |
| 
 | |
|       private getSafeStyle(value: string) { return this.sanitizer.bypassSecurityTrustStyle(value); }
 | |
| 
 | |
|       getBackgroundSafe() { return this.getSafeStyle(`url("/${this.background}")`); }
 | |
|       getWidthSafe() { return this.getSafeStyle(this.width); }
 | |
|       getHeightSafe() { return this.getSafeStyle(this.height); }
 | |
|       getColorUnsafe() { return this.color; }
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({
 | |
|       imports: [CommonModule],
 | |
|       declarations: [MyComp],
 | |
|     });
 | |
|     const fixture = TestBed.createComponent(MyComp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const comp = fixture.componentInstance;
 | |
|     const div = fixture.nativeElement.querySelector('div');
 | |
|     const p = fixture.nativeElement.querySelector('p');
 | |
|     const span = fixture.nativeElement.querySelector('span');
 | |
| 
 | |
|     expect(div.style.background).toContain('url("/1.png")');
 | |
|     expect(p.style.width).toBe('calc(20%)');
 | |
|     expect(p.style.height).toBe('10px');
 | |
|     expect(span.style.color).toBe('red');
 | |
| 
 | |
|     comp.background = '2.png';
 | |
|     comp.width = '5px';
 | |
|     comp.height = '100%';
 | |
|     comp.color = 'green';
 | |
| 
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(div.style.background).toContain('url("/2.png")');
 | |
|     expect(p.style.width).toBe('5px');
 | |
|     expect(p.style.height).toBe('100%');
 | |
|     expect(span.style.color).toBe('green');
 | |
|   });
 | |
| 
 | |
|   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: `
 | |
|         <div #div
 | |
|               [style]="map"
 | |
|               dir-with-styling
 | |
|               dir-with-styling-part2></div>
 | |
|       `
 | |
|         })
 | |
|         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');
 | |
|         expectStyle(element).toEqual({
 | |
|           color: 'red',
 | |
|           width: '200px',
 | |
|         });
 | |
|       });
 | |
| 
 | |
|   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: `
 | |
|             <div *ngFor="let item of items"
 | |
|                   class="initial-class item-{{ item }}">
 | |
|               {{ item }}
 | |
|             </div>
 | |
|           `
 | |
|         })
 | |
|         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 class="header">header</header>
 | |
|             <div *ngFor="let item of items" class="item item-{{ item }}">
 | |
|               {{ item }}
 | |
|             </div>
 | |
|             <footer class="footer">footer</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: `
 | |
|             <dir-one></dir-one>
 | |
|             <div class="initial"></div>
 | |
|             <dir-two></dir-two>
 | |
|           `
 | |
|         })
 | |
|         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: `
 | |
|                 <div class="a" [style.width.px]="w" one></div>
 | |
|                 <div class="b" [style.height.px]="h" one two></div>
 | |
|                 <div class="c" [style.color]="c" two></div>
 | |
|               `
 | |
|             })
 | |
|             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: `
 | |
|                 <div
 | |
|                   class="item"
 | |
|                   *ngFor="let item of items; let i = index"
 | |
|                   [style.color]="c"
 | |
|                   [style.height.px]="h * i"
 | |
|                   some-dir-with-styling>
 | |
|                   {{ item }}
 | |
|                 </div>
 | |
|                 <section [style.width.px]="w"></section>
 | |
|                 <p [style.height.px]="h"></p>
 | |
|               `
 | |
|             })
 | |
|             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: '<div #target one two></div>'})
 | |
|             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: `
 | |
|             <div
 | |
|               [style.width]="w"
 | |
|               style.height="{{ h }}"
 | |
|               [style.opacity]="o"></div>
 | |
|           `
 | |
|     })
 | |
|     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: `
 | |
|             <div [ngClass]="c" [ngStyle]="s"></div>
 | |
|           `
 | |
|     })
 | |
|     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 to reset style property value defined using ngStyle', () => {
 | |
|     @Component({
 | |
|       template: `
 | |
|         <div [ngStyle]="s"></div>
 | |
|       `
 | |
|     })
 | |
|     class Cmp {
 | |
|       s: any = {opacity: '1'};
 | |
| 
 | |
|       clearStyle(): void { this.s = null; }
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     const comp = fixture.componentInstance;
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const div = fixture.nativeElement.querySelector('div');
 | |
|     expect(div.style.opacity).toEqual('1');
 | |
| 
 | |
|     comp.clearStyle();
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(div.style.opacity).toEqual('');
 | |
|   });
 | |
| 
 | |
|   it('should allow detectChanges to be run in a property change that causes additional styling to be rendered',
 | |
|      () => {
 | |
|        @Component({
 | |
|          selector: 'child',
 | |
|          template: `
 | |
|           <div [class.ready-child]="readyTpl"></div>
 | |
|         `,
 | |
|        })
 | |
|        class ChildCmp {
 | |
|          readyTpl = false;
 | |
| 
 | |
|          @HostBinding('class.ready-host')
 | |
|          readyHost = false;
 | |
|        }
 | |
| 
 | |
|        @Component({
 | |
|          selector: 'parent',
 | |
|          template: `
 | |
|         <div>
 | |
|           <div #template></div>
 | |
|           <p>{{prop}}</p>
 | |
|         </div>
 | |
|       `,
 | |
|          host: {
 | |
|            '[style.color]': 'color',
 | |
|          },
 | |
|        })
 | |
|        class ParentCmp {
 | |
|          private _prop = '';
 | |
| 
 | |
|          @ViewChild('template', {read: ViewContainerRef})
 | |
|          vcr: ViewContainerRef = null !;
 | |
| 
 | |
|          private child: ComponentRef<ChildCmp> = 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: `<parent [prop]="prop"></parent>`,
 | |
|        })
 | |
|        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: `
 | |
|           <div [class.ready-child]="readyTpl"></div>
 | |
|         `,
 | |
|        })
 | |
|        class ChildCmp {
 | |
|          readyTpl = false;
 | |
| 
 | |
|          @HostBinding('class.ready-host')
 | |
|          readyHost = false;
 | |
|        }
 | |
| 
 | |
|        @Component({
 | |
|          selector: 'parent',
 | |
|          template: `
 | |
|           <div>
 | |
|             <div #template></div>
 | |
|             <p>{{prop}}</p>
 | |
|           </div>
 | |
|         `,
 | |
|        })
 | |
|        class ParentCmp {
 | |
|          updateChild = false;
 | |
| 
 | |
|          @ViewChild('template', {read: ViewContainerRef})
 | |
|          vcr: ViewContainerRef = null !;
 | |
| 
 | |
|          private child: ComponentRef<ChildCmp> = 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: `<parent #parent></parent>`,
 | |
|        })
 | |
|        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();
 | |
|      });
 | |
| 
 | |
|   onlyInIvy('only ivy allows for multiple styles/classes to be balanced across directives')
 | |
|       .it('should allow various duplicate properties to be defined in various styling maps within the template and directive styling bindings',
 | |
|           () => {
 | |
|             @Component({
 | |
|               template: `
 | |
|            <div [style.width]="w"
 | |
|                 [style.height]="h"
 | |
|                 [style]="s1"
 | |
|                 [dir-with-styling]="s2">
 | |
|          `
 | |
|             })
 | |
|             class Cmp {
 | |
|               h = '100px';
 | |
|               w = '100px';
 | |
|               s1: any = {border: '10px solid black', width: '200px'};
 | |
|               s2: any = {border: '10px solid red', width: '300px'};
 | |
|             }
 | |
| 
 | |
|             @Directive({selector: '[dir-with-styling]'})
 | |
|             class DirectiveExpectingStyling {
 | |
|               @Input('dir-with-styling') @HostBinding('style') public styles: any = null;
 | |
|             }
 | |
| 
 | |
|             TestBed.configureTestingModule({declarations: [Cmp, DirectiveExpectingStyling]});
 | |
|             const fixture = TestBed.createComponent(Cmp);
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             const element = fixture.nativeElement.querySelector('div');
 | |
|             expect(element.style.border).toEqual('10px solid black');
 | |
|             expect(element.style.width).toEqual('100px');
 | |
|             expect(element.style.height).toEqual('100px');
 | |
| 
 | |
|             fixture.componentInstance.s1 = null;
 | |
|             fixture.detectChanges();
 | |
| 
 | |
|             expect(element.style.border).toEqual('10px solid red');
 | |
|             expect(element.style.width).toEqual('100px');
 | |
|             expect(element.style.height).toEqual('100px');
 | |
|           });
 | |
| 
 | |
|   it('should retrieve styles set via Renderer2', () => {
 | |
|     let dirInstance: any;
 | |
|     @Directive({
 | |
|       selector: '[dir]',
 | |
|     })
 | |
|     class Dir {
 | |
|       constructor(public elementRef: ElementRef, public renderer: Renderer2) { dirInstance = this; }
 | |
| 
 | |
|       setStyles() {
 | |
|         const nativeEl = this.elementRef.nativeElement;
 | |
|         this.renderer.setStyle(nativeEl, 'transform', 'translate3d(0px, 0px, 0px)');
 | |
|         this.renderer.addClass(nativeEl, 'my-class');
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     @Component({template: `<div dir></div>`})
 | |
|     class App {
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({
 | |
|       declarations: [App, Dir],
 | |
|     });
 | |
|     const fixture = TestBed.createComponent(App);
 | |
|     fixture.detectChanges();
 | |
|     dirInstance.setStyles();
 | |
| 
 | |
|     const div = fixture.debugElement.children[0];
 | |
|     expect(div.styles.transform).toMatch(/translate3d\(0px\s*,\s*0px\s*,\s*0px\)/);
 | |
|     expect(div.classes['my-class']).toBe(true);
 | |
|   });
 | |
| 
 | |
|   it('should not set classes when falsy value is passed while a sanitizer is present', () => {
 | |
|     @Component({
 | |
|       // Note that we use `background` here because it needs to be sanitized.
 | |
|       template: `
 | |
|         <span class="container" [ngClass]="{disabled: isDisabled}"></span>
 | |
|         <div [style.background]="background"></div>
 | |
|       `,
 | |
|     })
 | |
| 
 | |
|     class AppComponent {
 | |
|       isDisabled = false;
 | |
|       background = 'orange';
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [AppComponent]});
 | |
|     const fixture = TestBed.createComponent(AppComponent);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const span = fixture.nativeElement.querySelector('span');
 | |
|     expect(span.classList).not.toContain('disabled');
 | |
| 
 | |
|     // The issue we're testing for happens after the second change detection.
 | |
|     fixture.detectChanges();
 | |
|     expect(span.classList).not.toContain('disabled');
 | |
|   });
 | |
| 
 | |
|   it('should not set classes when falsy value is passed while a sanitizer from host bindings is present',
 | |
|      () => {
 | |
|        @Directive({selector: '[blockStyles]'})
 | |
|        class StylesDirective {
 | |
|          @HostBinding('style.border')
 | |
|          border = '1px solid red';
 | |
| 
 | |
|          @HostBinding('style.background')
 | |
|          background = 'white';
 | |
|        }
 | |
| 
 | |
|        @Component({
 | |
|          template: `<div class="container" [ngClass]="{disabled: isDisabled}" blockStyles></div>`,
 | |
|        })
 | |
|        class AppComponent {
 | |
|          isDisabled = false;
 | |
|        }
 | |
| 
 | |
|        TestBed.configureTestingModule({declarations: [AppComponent, StylesDirective]});
 | |
|        const fixture = TestBed.createComponent(AppComponent);
 | |
|        fixture.detectChanges();
 | |
| 
 | |
|        const div = fixture.nativeElement.querySelector('div');
 | |
|        expect(div.classList.contains('disabled')).toBe(false);
 | |
| 
 | |
|        // The issue we're testing for happens after the second change detection.
 | |
|        fixture.detectChanges();
 | |
|        expect(div.classList.contains('disabled')).toBe(false);
 | |
|      });
 | |
| 
 | |
|   it('should throw an error if a prop-based style/class binding value is changed during checkNoChanges',
 | |
|      () => {
 | |
|        @Component({
 | |
|          template: `
 | |
|         <div [style.color]="color" [class.foo]="fooClass"></div>
 | |
|       `
 | |
|        })
 | |
|        class Cmp {
 | |
|          color = 'red';
 | |
|          fooClass = true;
 | |
| 
 | |
|          ngAfterViewInit() {
 | |
|            this.color = 'blue';
 | |
|            this.fooClass = false;
 | |
|          }
 | |
|        }
 | |
| 
 | |
|        TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|        const fixture = TestBed.createComponent(Cmp);
 | |
| 
 | |
|        expect(() => {
 | |
|          fixture.detectChanges();
 | |
|        }).toThrowError(/ExpressionChangedAfterItHasBeenCheckedError/);
 | |
|      });
 | |
| 
 | |
|   onlyInIvy('only ivy allows for map-based style AND class bindings')
 | |
|       .it('should throw an error if a map-based style/class binding value is changed during checkNoChanges',
 | |
|           () => {
 | |
|             @Component({
 | |
|               template: `
 | |
|                 <div [style]="style" [class]="klass"></div>
 | |
|               `
 | |
|             })
 | |
|             class Cmp {
 | |
|               style: any = 'width: 100px';
 | |
|               klass: any = 'foo';
 | |
| 
 | |
|               ngAfterViewInit() {
 | |
|                 this.style = 'height: 200px';
 | |
|                 this.klass = 'bar';
 | |
|               }
 | |
|             }
 | |
| 
 | |
|             TestBed.configureTestingModule({declarations: [Cmp]});
 | |
|             const fixture = TestBed.createComponent(Cmp);
 | |
| 
 | |
|             expect(() => {
 | |
|               fixture.detectChanges();
 | |
|             }).toThrowError(/ExpressionChangedAfterItHasBeenCheckedError/);
 | |
|           });
 | |
| 
 | |
|   it('should properly merge class interpolation with class-based directives', () => {
 | |
|     @Component(
 | |
|         {template: `<div class="zero {{one}}" [class.two]="true" [ngClass]="'three'"></div>`})
 | |
|     class MyComp {
 | |
|       one = 'one';
 | |
|     }
 | |
| 
 | |
|     const fixture =
 | |
|         TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(fixture.debugElement.nativeElement.innerHTML).toContain('zero');
 | |
|     expect(fixture.debugElement.nativeElement.innerHTML).toContain('one');
 | |
|     expect(fixture.debugElement.nativeElement.innerHTML).toContain('two');
 | |
|     expect(fixture.debugElement.nativeElement.innerHTML).toContain('three');
 | |
|   });
 | |
| 
 | |
|   it('should allow to reset style property value defined using [style.prop.px] binding', () => {
 | |
|     @Component({
 | |
|       template: '<div [style.left.px]="left"></div>',
 | |
|     })
 | |
|     class MyComp {
 | |
|       left = '';
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [MyComp]});
 | |
|     const fixture = TestBed.createComponent(MyComp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const checks = [
 | |
|       ['15', '15px'],
 | |
|       [undefined, ''],
 | |
|       [null, ''],
 | |
|       ['', ''],
 | |
|       ['0', '0px'],
 | |
|     ];
 | |
|     const div = fixture.nativeElement.querySelector('div');
 | |
|     checks.forEach((check: any[]) => {
 | |
|       const [fieldValue, expectedValue] = check;
 | |
|       fixture.componentInstance.left = fieldValue;
 | |
|       fixture.detectChanges();
 | |
|       expect(div.style.left).toBe(expectedValue);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   onlyInIvy('only ivy treats [class] in concert with other class bindings')
 | |
|       .it('should retain classes added externally', () => {
 | |
|         @Component({template: `<div [class]="exp"></div>`})
 | |
|         class MyComp {
 | |
|           exp = '';
 | |
|         }
 | |
| 
 | |
|         const fixture =
 | |
|             TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         const div = fixture.nativeElement.querySelector('div') !;
 | |
|         div.className += ' abc';
 | |
|         expect(splitSortJoin(div.className)).toEqual('abc');
 | |
| 
 | |
|         fixture.componentInstance.exp = '1 2 3';
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         expect(splitSortJoin(div.className)).toEqual('1 2 3 abc');
 | |
| 
 | |
|         fixture.componentInstance.exp = '4 5 6 7';
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         expect(splitSortJoin(div.className)).toEqual('4 5 6 7 abc');
 | |
| 
 | |
|         function splitSortJoin(s: string) { return s.split(/\s+/).sort().join(' ').trim(); }
 | |
|       });
 | |
| 
 | |
|   describe('ExpressionChangedAfterItHasBeenCheckedError', () => {
 | |
|     it('should not throw when bound to SafeValue', () => {
 | |
|       @Component({template: `<div [style.background-image]="iconSafe"></div>`})
 | |
|       class MyComp {
 | |
|         icon = 'https://i.imgur.com/4AiXzf8.jpg';
 | |
|         get iconSafe() { return this.sanitizer.bypassSecurityTrustStyle(`url("${this.icon}")`); }
 | |
| 
 | |
|         constructor(private sanitizer: DomSanitizer) {}
 | |
|       }
 | |
| 
 | |
|       const fixture =
 | |
|           TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
 | |
|       fixture.detectChanges(true /* Verify that check no changes does not cause an exception */);
 | |
|       const div: HTMLElement = fixture.nativeElement.querySelector('div');
 | |
|       expect(div.style.getPropertyValue('background-image'))
 | |
|           .toEqual('url("https://i.imgur.com/4AiXzf8.jpg")');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   isBrowser && it('should process <style> tag contents extracted from template', () => {
 | |
|     @Component({
 | |
|       template: `
 | |
|         <style>
 | |
|           div { width: 10px; }
 | |
|         </style>
 | |
|         <div></div>
 | |
|       `,
 | |
|       styles: [
 | |
|         'div { width: 100px; }',
 | |
|       ]
 | |
|     })
 | |
|     class MyComp {
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({
 | |
|       declarations: [MyComp],
 | |
|     });
 | |
| 
 | |
|     const fixture = TestBed.createComponent(MyComp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     // `styles` array values are applied first, styles from <style> tags second.
 | |
|     const div = fixture.nativeElement.querySelector('div');
 | |
|     expect(getComputedStyle(div).width).toBe('10px');
 | |
|   });
 | |
| 
 | |
|   onlyInIvy('[style] binding is supported in Ivy only')
 | |
|       .it('should allow multiple styling bindings to work alongside property/attribute bindings',
 | |
|           () => {
 | |
|             @Component({
 | |
|               template: `
 | |
|         <div 
 | |
|             dir-that-sets-styles
 | |
|             [style]="{'font-size': '300px'}"
 | |
|             [attr.title]="'my-title'"
 | |
|             [attr.data-foo]="'my-foo'">
 | |
|         </div>`
 | |
|             })
 | |
|             class MyComp {
 | |
|             }
 | |
| 
 | |
|             @Directive({selector: '[dir-that-sets-styles]'})
 | |
|             class DirThatSetsStyling {
 | |
|               @HostBinding('style.width') public w = '100px';
 | |
|               @HostBinding('style.height') public h = '200px';
 | |
|             }
 | |
| 
 | |
|             const fixture =
 | |
|                 TestBed.configureTestingModule({declarations: [MyComp, DirThatSetsStyling]})
 | |
|                     .createComponent(MyComp);
 | |
|             fixture.detectChanges();
 | |
|             const div = fixture.nativeElement.querySelector('div') !;
 | |
|             expect(div.style.getPropertyValue('width')).toEqual('100px');
 | |
|             expect(div.style.getPropertyValue('height')).toEqual('200px');
 | |
|             expect(div.style.getPropertyValue('font-size')).toEqual('300px');
 | |
|             expect(div.getAttribute('title')).toEqual('my-title');
 | |
|             expect(div.getAttribute('data-foo')).toEqual('my-foo');
 | |
|           });
 | |
| 
 | |
|   onlyInIvy('VE clobers in case of  @HostBinding("class")')
 | |
|       .it('should allow host styling on the root element with external styling', () => {
 | |
|         @Component({template: '...'})
 | |
|         class MyComp {
 | |
|           @HostBinding('class') public classes = '';
 | |
|         }
 | |
| 
 | |
|         const fixture =
 | |
|             TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
 | |
|         fixture.detectChanges();
 | |
|         const root = fixture.nativeElement as HTMLElement;
 | |
|         expect(root.className).toEqual('');
 | |
| 
 | |
|         fixture.componentInstance.classes = '1 2 3';
 | |
|         fixture.detectChanges();
 | |
|         expect(root.className.split(/\s+/).sort().join(' ')).toEqual('1 2 3');
 | |
| 
 | |
|         root.classList.add('0');
 | |
|         expect(root.className.split(/\s+/).sort().join(' ')).toEqual('0 1 2 3');
 | |
| 
 | |
|         fixture.componentInstance.classes = '1 2 3 4';
 | |
|         fixture.detectChanges();
 | |
|         expect(root.className.split(/\s+/).sort().join(' ')).toEqual('0 1 2 3 4');
 | |
|       });
 | |
| 
 | |
|   it('should apply camelCased class names', () => {
 | |
|     @Component({template: `<div [class]="'fooBar'" [class.barFoo]="true"></div>`})
 | |
|     class MyComp {
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({
 | |
|       declarations: [MyComp],
 | |
|     });
 | |
|     const fixture = TestBed.createComponent(MyComp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const classList = (fixture.nativeElement.querySelector('div') as HTMLDivElement).classList;
 | |
|     expect(classList.contains('fooBar')).toBeTruthy();
 | |
|     expect(classList.contains('barFoo')).toBeTruthy();
 | |
|   });
 | |
| 
 | |
|   onlyInIvy('[style] bindings are ivy only')
 | |
|       .it('should convert camelCased style property names to snake-case', () => {
 | |
|         @Component({template: `<div [style]="myStyles"></div>`})
 | |
|         class MyComp {
 | |
|           myStyles = {};
 | |
|         }
 | |
| 
 | |
|         TestBed.configureTestingModule({
 | |
|           declarations: [MyComp],
 | |
|         });
 | |
|         const fixture = TestBed.createComponent(MyComp);
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         const div = fixture.nativeElement.querySelector('div') as HTMLDivElement;
 | |
|         fixture.componentInstance.myStyles = {fontSize: '200px'};
 | |
|         fixture.detectChanges();
 | |
| 
 | |
|         expect(div.style.getPropertyValue('font-size')).toEqual('200px');
 | |
|       });
 | |
| 
 | |
|   it('should recover from an error thrown in styling bindings', () => {
 | |
|     let raiseWidthError = false;
 | |
| 
 | |
|     @Component({template: `<div [style.width]="myWidth" [style.height]="'200px'"></div>`})
 | |
|     class MyComp {
 | |
|       get myWidth() {
 | |
|         if (raiseWidthError) {
 | |
|           throw new Error('...');
 | |
|         }
 | |
|         return '100px';
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [MyComp]});
 | |
|     const fixture = TestBed.createComponent(MyComp);
 | |
| 
 | |
|     raiseWidthError = true;
 | |
|     expect(() => fixture.detectChanges()).toThrow();
 | |
| 
 | |
|     raiseWidthError = false;
 | |
|     fixture.detectChanges();
 | |
|     const div = fixture.nativeElement.querySelector('div') as HTMLDivElement;
 | |
|     expect(div.style.getPropertyValue('width')).toEqual('100px');
 | |
|     expect(div.style.getPropertyValue('height')).toEqual('200px');
 | |
|   });
 | |
| 
 | |
|   onlyInIvy('Prioritization works in Ivy only')
 | |
|       .it('should prioritize host bindings for templates first, then directives and finally components',
 | |
|           () => {
 | |
|             @Component({selector: 'my-comp-with-styling', template: ''})
 | |
|             class MyCompWithStyling {
 | |
|               @HostBinding('style')
 | |
|               myStyles: any = {width: '300px'};
 | |
| 
 | |
|               @HostBinding('style.height')
 | |
|               myHeight: any = '305px';
 | |
|             }
 | |
| 
 | |
|             @Directive({selector: '[my-dir-with-styling]'})
 | |
|             class MyDirWithStyling {
 | |
|               @HostBinding('style')
 | |
|               myStyles: any = {width: '200px'};
 | |
| 
 | |
|               @HostBinding('style.height')
 | |
|               myHeight: any = '205px';
 | |
|             }
 | |
| 
 | |
|             @Component({
 | |
|               template: `
 | |
|           <my-comp-with-styling
 | |
|             style="height:1px; width:2px"
 | |
|             my-dir-with-styling
 | |
|             [style.height]="myHeight"
 | |
|             [style]="myStyles">
 | |
|           </my-comp-with-styling>
 | |
|       `
 | |
|             })
 | |
|             class MyComp {
 | |
|               myStyles: {width?: string} = {width: '100px'};
 | |
|               myHeight: string|null|undefined = '100px';
 | |
| 
 | |
|               @ViewChild(MyDirWithStyling) dir !: MyDirWithStyling;
 | |
|               @ViewChild(MyCompWithStyling) comp !: MyCompWithStyling;
 | |
|             }
 | |
| 
 | |
|             TestBed.configureTestingModule(
 | |
|                 {declarations: [MyComp, MyCompWithStyling, MyDirWithStyling]});
 | |
|             const fixture = TestBed.createComponent(MyComp);
 | |
|             const comp = fixture.componentInstance;
 | |
|             const elm = fixture.nativeElement.querySelector('my-comp-with-styling') !;
 | |
| 
 | |
|             fixture.detectChanges();
 | |
|             expect(elm.style.width).toEqual('100px');
 | |
|             expect(elm.style.height).toEqual('100px');
 | |
| 
 | |
|             comp.myStyles = {};
 | |
|             comp.myHeight = undefined;
 | |
|             fixture.detectChanges();
 | |
|             expect(elm.style.width).toEqual('2px');
 | |
|             expect(elm.style.height).toEqual('1px');
 | |
| 
 | |
|             comp.comp.myStyles = {};
 | |
|             comp.comp.myHeight = undefined;
 | |
|             fixture.detectChanges();
 | |
|             expect(elm.style.width).toEqual('2px');
 | |
|             expect(elm.style.height).toEqual('1px');
 | |
| 
 | |
|             comp.dir.myStyles = {};
 | |
|             comp.dir.myHeight = undefined;
 | |
|             fixture.detectChanges();
 | |
|             expect(elm.style.width).toEqual('2px');
 | |
|             expect(elm.style.height).toEqual('1px');
 | |
|           });
 | |
| 
 | |
|   onlyInIvy('Prioritization works in Ivy only')
 | |
|       .it('should prioritize directive static bindings over components', () => {
 | |
|         @Component({selector: 'my-comp-with-styling', host: {style: 'color: blue'}, template: ''})
 | |
|         class MyCompWithStyling {
 | |
|         }
 | |
| 
 | |
|         @Directive({selector: '[my-dir-with-styling]', host: {style: 'color: red'}})
 | |
|         class MyDirWithStyling {
 | |
|         }
 | |
| 
 | |
|         @Component({template: `<my-comp-with-styling my-dir-with-styling></my-comp-with-styling>`})
 | |
|         class MyComp {
 | |
|         }
 | |
| 
 | |
|         TestBed.configureTestingModule(
 | |
|             {declarations: [MyComp, MyCompWithStyling, MyDirWithStyling]});
 | |
|         const fixture = TestBed.createComponent(MyComp);
 | |
|         const elm = fixture.nativeElement.querySelector('my-comp-with-styling') !;
 | |
| 
 | |
|         fixture.detectChanges();
 | |
|         expect(elm.style.color).toEqual('red');
 | |
|       });
 | |
| 
 | |
| 
 | |
|   it('should combine host class.foo bindings from multiple directives', () => {
 | |
| 
 | |
|     @Directive({
 | |
|       selector: '[dir-that-sets-one-two]',
 | |
|       exportAs: 'one',
 | |
|     })
 | |
|     class DirThatSetsOneTwo {
 | |
|       @HostBinding('class.one') one = false;
 | |
|       @HostBinding('class.two') two = false;
 | |
|     }
 | |
| 
 | |
|     @Directive({
 | |
|       selector: '[dir-that-sets-three-four]',
 | |
|       exportAs: 'two',
 | |
|     })
 | |
|     class DirThatSetsThreeFour {
 | |
|       @HostBinding('class.three') three = false;
 | |
|       @HostBinding('class.four') four = false;
 | |
|     }
 | |
| 
 | |
|     @Component({
 | |
|       template: `
 | |
|            <div #div1
 | |
|              dir-that-sets-one-two
 | |
|              dir-that-sets-three-four></div>
 | |
|            <div #div2
 | |
|              [class.zero]="zero"
 | |
|              dir-that-sets-one-two
 | |
|              dir-that-sets-three-four></div>
 | |
|          `
 | |
|     })
 | |
|     class MyComp {
 | |
|       @ViewChild('div1', {static: true, read: DirThatSetsOneTwo})
 | |
|       public dirOneA: DirThatSetsOneTwo|null = null;
 | |
| 
 | |
|       @ViewChild('div1', {static: true, read: DirThatSetsThreeFour})
 | |
|       public dirTwoA: DirThatSetsThreeFour|null = null;
 | |
| 
 | |
|       @ViewChild('div2', {static: true, read: DirThatSetsOneTwo})
 | |
|       public dirOneB: DirThatSetsOneTwo|null = null;
 | |
| 
 | |
|       @ViewChild('div2', {static: true, read: DirThatSetsThreeFour})
 | |
|       public dirTwoB: DirThatSetsThreeFour|null = null;
 | |
| 
 | |
|       zero = false;
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule(
 | |
|         {declarations: [MyComp, DirThatSetsThreeFour, DirThatSetsOneTwo]});
 | |
| 
 | |
|     const fixture = TestBed.createComponent(MyComp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const [div1, div2] = fixture.nativeElement.querySelectorAll('div') as HTMLDivElement[];
 | |
| 
 | |
|     expect(div1.className).toBe('');
 | |
|     expect(div2.className).toBe('');
 | |
| 
 | |
|     const comp = fixture.componentInstance;
 | |
|     comp.dirOneA !.one = comp.dirOneB !.one = true;
 | |
|     comp.dirOneA !.two = comp.dirOneB !.two = true;
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(div1.classList.contains('one')).toBeTruthy();
 | |
|     expect(div1.classList.contains('two')).toBeTruthy();
 | |
|     expect(div1.classList.contains('three')).toBeFalsy();
 | |
|     expect(div1.classList.contains('four')).toBeFalsy();
 | |
|     expect(div2.classList.contains('one')).toBeTruthy();
 | |
|     expect(div2.classList.contains('two')).toBeTruthy();
 | |
|     expect(div2.classList.contains('three')).toBeFalsy();
 | |
|     expect(div2.classList.contains('four')).toBeFalsy();
 | |
|     expect(div2.classList.contains('zero')).toBeFalsy();
 | |
| 
 | |
|     comp.dirTwoA !.three = comp.dirTwoB !.three = true;
 | |
|     comp.dirTwoA !.four = comp.dirTwoB !.four = true;
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(div1.classList.contains('one')).toBeTruthy();
 | |
|     expect(div1.classList.contains('two')).toBeTruthy();
 | |
|     expect(div1.classList.contains('three')).toBeTruthy();
 | |
|     expect(div1.classList.contains('four')).toBeTruthy();
 | |
|     expect(div2.classList.contains('one')).toBeTruthy();
 | |
|     expect(div2.classList.contains('two')).toBeTruthy();
 | |
|     expect(div2.classList.contains('three')).toBeTruthy();
 | |
|     expect(div2.classList.contains('four')).toBeTruthy();
 | |
|     expect(div2.classList.contains('zero')).toBeFalsy();
 | |
| 
 | |
|     comp.zero = true;
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(div1.classList.contains('one')).toBeTruthy();
 | |
|     expect(div1.classList.contains('two')).toBeTruthy();
 | |
|     expect(div1.classList.contains('three')).toBeTruthy();
 | |
|     expect(div1.classList.contains('four')).toBeTruthy();
 | |
|     expect(div2.classList.contains('one')).toBeTruthy();
 | |
|     expect(div2.classList.contains('two')).toBeTruthy();
 | |
|     expect(div2.classList.contains('three')).toBeTruthy();
 | |
|     expect(div2.classList.contains('four')).toBeTruthy();
 | |
|     expect(div2.classList.contains('zero')).toBeTruthy();
 | |
|   });
 | |
| 
 | |
|   it('should combine static host classes with component "class" host attribute', () => {
 | |
|     @Component({selector: 'comp-with-classes', template: '', host: {'class': 'host'}})
 | |
|     class CompWithClasses {
 | |
|       constructor(ref: ElementRef) { ref.nativeElement.classList.add('custom'); }
 | |
|     }
 | |
| 
 | |
|     @Component({
 | |
|       template: `<comp-with-classes class="inline" *ngFor="let item of items"></comp-with-classes>`
 | |
|     })
 | |
|     class MyComp {
 | |
|       items = [1, 2, 3];
 | |
|     }
 | |
| 
 | |
|     const fixture = TestBed
 | |
|                         .configureTestingModule({
 | |
|                           declarations: [MyComp, CompWithClasses],
 | |
|                         })
 | |
|                         .createComponent(MyComp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const [one, two, three] =
 | |
|         fixture.nativeElement.querySelectorAll('comp-with-classes') as HTMLDivElement[];
 | |
| 
 | |
|     expect(one.classList.contains('custom')).toBeTruthy();
 | |
|     expect(one.classList.contains('inline')).toBeTruthy();
 | |
|     expect(one.classList.contains('host')).toBeTruthy();
 | |
| 
 | |
|     expect(two.classList.contains('custom')).toBeTruthy();
 | |
|     expect(two.classList.contains('inline')).toBeTruthy();
 | |
|     expect(two.classList.contains('host')).toBeTruthy();
 | |
| 
 | |
|     expect(three.classList.contains('custom')).toBeTruthy();
 | |
|     expect(three.classList.contains('inline')).toBeTruthy();
 | |
|     expect(three.classList.contains('host')).toBeTruthy();
 | |
|   });
 | |
| 
 | |
|   it('should allow a single style host binding on an element', () => {
 | |
|     @Component({template: `<div single-host-style-dir></div>`})
 | |
|     class Cmp {
 | |
|     }
 | |
| 
 | |
|     @Directive({selector: '[single-host-style-dir]'})
 | |
|     class SingleHostStyleDir {
 | |
|       @HostBinding('style.width')
 | |
|       width = '100px';
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp, SingleHostStyleDir]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const element = fixture.nativeElement.querySelector('div');
 | |
|     expect(element.style.width).toEqual('100px');
 | |
|   });
 | |
| 
 | |
|   it('should override class bindings when a directive extends another directive', () => {
 | |
|     @Component({template: `<child-comp class="template"></child-comp>`})
 | |
|     class Cmp {
 | |
|     }
 | |
| 
 | |
|     @Component({
 | |
|       selector: 'parent-comp',
 | |
|       host: {'class': 'parent-comp', '[class.parent-comp-active]': 'true'},
 | |
|       template: '...',
 | |
|     })
 | |
|     class ParentComp {
 | |
|     }
 | |
| 
 | |
|     @Component({
 | |
|       selector: 'child-comp',
 | |
|       host: {
 | |
|         'class': 'child-comp',
 | |
|         '[class.child-comp-active]': 'true',
 | |
|         '[class.parent-comp]': 'false',
 | |
|         '[class.parent-comp-active]': 'false'
 | |
|       },
 | |
|       template: '...',
 | |
|     })
 | |
|     class ChildComp extends ParentComp {
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [Cmp, ChildComp, ParentComp]});
 | |
|     const fixture = TestBed.createComponent(Cmp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     const element = fixture.nativeElement.querySelector('child-comp');
 | |
|     expect(element.classList.contains('template')).toBeTruthy();
 | |
| 
 | |
|     expect(element.classList.contains('child-comp')).toBeTruthy();
 | |
|     expect(element.classList.contains('child-comp-active')).toBeTruthy();
 | |
| 
 | |
|     expect(element.classList.contains('parent-comp')).toBeFalsy();
 | |
|     expect(element.classList.contains('parent-comp-active')).toBeFalsy();
 | |
|   });
 | |
| 
 | |
|   it('should not set inputs called class if they are not being used in the template', () => {
 | |
|     const logs: string[] = [];
 | |
| 
 | |
|     @Directive({selector: '[test]'})
 | |
|     class MyDir {
 | |
|       @Input('class')
 | |
|       set className(value: string) { logs.push(value); }
 | |
|     }
 | |
| 
 | |
|     @Component({
 | |
|       // Note that we shouldn't have a `class` attribute here.
 | |
|       template: `<div test></div>`
 | |
|     })
 | |
|     class MyComp {
 | |
|     }
 | |
| 
 | |
|     TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
 | |
|     const fixture = TestBed.createComponent(MyComp);
 | |
|     fixture.detectChanges();
 | |
| 
 | |
|     expect(logs).toEqual([]);
 | |
|   });
 | |
| 
 | |
|   describe('regression', () => {
 | |
|     onlyInIvy('styling priority resolution is Ivy only feature.')
 | |
|         .it('should allow lookahead binding on second pass #35118', () => {
 | |
|           @Component({
 | |
|             selector: 'my-cmp',
 | |
|             template: ``,
 | |
|             host: {
 | |
|               '[class.foo]': 'hostClass',
 | |
|             }
 | |
|           })
 | |
|           class MyCmp {
 | |
|             hostClass = true;
 | |
|           }
 | |
| 
 | |
|           @Directive({
 | |
|             selector: '[host-styling]',
 | |
|             host: {
 | |
|               '[class]': 'hostClass',
 | |
|             }
 | |
|           })
 | |
|           class HostStylingsDir {
 | |
|             hostClass = {'bar': true};
 | |
|           }
 | |
| 
 | |
|           @Component({template: `<my-cmp *ngFor="let i of [1,2]" host-styling></my-cmp>`})
 | |
|           class MyApp {
 | |
|             // When the first view in the list gets CD-ed, everything works.
 | |
|             // When the second view gets CD-ed, the styling has already created the data structures
 | |
|             // in the `TView`. As a result when
 | |
|             // `[class.foo]` runs it already knows that `[class]` is a duplicate and hence it
 | |
|             // should check with it. While the resolution is happening it reads the value of the
 | |
|             // `[class]`, however `[class]` has not yet executed and therefore it does not have
 | |
|             // normalized value in its `LView`. The result is that the assertions fails as it
 | |
|             // expects an
 | |
|             // `KeyValueArray`.
 | |
|           }
 | |
| 
 | |
|           TestBed.configureTestingModule({declarations: [MyApp, MyCmp, HostStylingsDir]});
 | |
|           const fixture = TestBed.createComponent(MyApp);
 | |
|           expect(() => fixture.detectChanges()).not.toThrow();
 | |
|           const [cmp1, cmp2] = fixture.nativeElement.querySelectorAll('my-cmp');
 | |
|           expectClass(cmp1).toEqual({foo: true, bar: true});
 | |
|           expectClass(cmp2).toEqual({foo: true, bar: true});
 | |
|         });
 | |
|   });
 | |
| });
 | |
| 
 | |
| 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 expectStyle(element: HTMLElement) {
 | |
|   return expect(getElementStyles(element));
 | |
| }
 | |
| 
 | |
| function expectClass(element: HTMLElement) {
 | |
|   return expect(getElementClasses(element));
 | |
| } |