diff --git a/packages/core/src/debug/debug_node.ts b/packages/core/src/debug/debug_node.ts index d0cb0ac07c..9a8f4d41e4 100644 --- a/packages/core/src/debug/debug_node.ts +++ b/packages/core/src/debug/debug_node.ts @@ -21,6 +21,7 @@ import {findComponentView} from '../render3/util/view_traversal_utils'; import {getComponentViewByIndex, getNativeByTNodeOrNull} from '../render3/util/view_utils'; import {assertDomNode} from '../util/assert'; import {DebugContext} from '../view/index'; +import {createProxy} from './proxy'; @@ -345,12 +346,42 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme return attributes; } - get styles(): {[key: string]: string | null;} { - return _getStylingDebugInfo(this.nativeElement, false); + get styles(): {[key: string]: string | null} { + if (this.nativeElement && (this.nativeElement as HTMLElement).style) { + return (this.nativeElement as HTMLElement).style as{[key: string]: any}; + } + return {}; } + private _classesProxy !: {}; get classes(): {[key: string]: boolean;} { - return _getStylingDebugInfo(this.nativeElement, true); + if (!this._classesProxy) { + const element = this.nativeElement; + + // we use a proxy here because VE code expects `.classes` to keep + // track of which classes have been added and removed. Because we + // do not make use of a debug renderer anymore, the return value + // must always be `false` in the event that a class does not exist + // on the element (even if it wasn't added and removed beforehand). + this._classesProxy = createProxy({ + get(target: {}, prop: string) { + return element ? element.classList.contains(prop) : false; + }, + set(target: {}, prop: string, value: any) { + return element ? element.classList.toggle(prop, !!value) : false; + }, + ownKeys() { return element ? Array.from(element.classList).sort() : []; }, + getOwnPropertyDescriptor(k: any) { + // we use a special property descriptor here so that enumeration operations + // such as `Object.keys` will work on this proxy. + return { + enumerable: true, + configurable: true, + }; + }, + }); + } + return this._classesProxy; } get childNodes(): DebugNode[] { @@ -418,26 +449,6 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme } } -function _getStylingDebugInfo(element: any, isClassBased: boolean) { - const context = loadLContext(element, false); - if (!context) { - return {}; - } - - const lView = context.lView; - const tData = lView[TVIEW].data; - const tNode = tData[context.nodeIndex] as TNode; - if (isClassBased) { - return isStylingContext(tNode.classes) ? - new NodeStylingDebug(tNode.classes as TStylingContext, lView, true).values : - stylingMapToStringMap(tNode.classes as StylingMapArray | null); - } else { - return isStylingContext(tNode.styles) ? - new NodeStylingDebug(tNode.styles as TStylingContext, lView, false).values : - stylingMapToStringMap(tNode.styles as StylingMapArray | null); - } -} - /** * Walk the TNode tree to find matches for the predicate. * diff --git a/packages/core/src/debug/proxy.ts b/packages/core/src/debug/proxy.ts new file mode 100644 index 0000000000..95f46b7041 --- /dev/null +++ b/packages/core/src/debug/proxy.ts @@ -0,0 +1,30 @@ +/** + * @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 {global} from '../util/global'; + +/** + * Used to inform TS about the `Proxy` class existing globally. + */ +interface GlobalWithProxy { + Proxy: typeof Proxy; +} + +/** + * Creates an instance of a `Proxy` and creates with an empty target object and binds it to the + * provided handler. + * + * The reason why this function exists is because IE doesn't support + * the `Proxy` class. For this reason an error must be thrown. + */ +export function createProxy(handler: ProxyHandler): {} { + const g = global as any as GlobalWithProxy; + if (!g.Proxy) { + throw new Error('Proxy is not supported in this browser'); + } + return new g.Proxy({}, handler); +} diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index d15ebe85d7..feca76c020 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, HostBinding, Input, NgModule, ViewChild, ViewContainerRef} from '@angular/core'; +import {Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, HostBinding, Input, NgModule, Renderer2, ViewChild, ViewContainerRef} from '@angular/core'; import {getDebugNode} from '@angular/core/src/render3/util/discovery_utils'; import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {TestBed} from '@angular/core/testing'; @@ -2034,6 +2034,47 @@ describe('styling', () => { 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() { + this.renderer.setStyle( + this.elementRef.nativeElement, 'transform', 'translate3d(0px, 0px, 0px)'); + this.renderer.addClass(this.elementRef.nativeElement, 'my-class'); + } + } + + @Component({template: `
`}) + 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); + + div.classes['other-class'] = true; + div.styles['width'] = '200px'; + expect(div.styles.width).toEqual('200px'); + expect(div.classes['other-class']).toBe(true); + + if (ivyEnabled) { + expect(div.nativeElement.classList.contains('other-class')).toBeTruthy(); + expect(div.nativeElement.style['width']).toEqual('200px'); + } + }); }); function assertStyleCounters(countForSet: number, countForRemove: number) {