From c32b2ae0a8f469652f332f0e3967059265c32ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Tue, 24 Sep 2019 16:15:35 -0700 Subject: [PATCH] fix(ivy): ensure class/style values are debuggable through `DebugElement` (#32842) This patch changes the Ivy `DebugElement` code to always read style and class values directly from the native element instead of reading them through the styling contexts. The reason for this change is because Ivy does not make use of a debug renderer and will therefore not have access to any classes/styles applied directly through the renderer (unless it reads the values directly from the element). PR Close #32842 --- packages/core/src/debug/debug_node.ts | 57 +++++++++++-------- packages/core/src/debug/proxy.ts | 30 ++++++++++ packages/core/test/acceptance/styling_spec.ts | 43 +++++++++++++- 3 files changed, 106 insertions(+), 24 deletions(-) create mode 100644 packages/core/src/debug/proxy.ts 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) {