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
This commit is contained in:
Matias Niemelä 2019-09-24 16:15:35 -07:00 committed by atscott
parent 747f0cff9e
commit c32b2ae0a8
3 changed files with 106 additions and 24 deletions

View File

@ -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.
*

View File

@ -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<any>): {} {
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);
}

View File

@ -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: `<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);
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) {