diff --git a/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts b/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts index c60a96900ffaf..a5c9266bccffc 100644 --- a/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts +++ b/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts @@ -2,7 +2,7 @@ import { DirectiveID, DirectivesProperties, ElementID, Events, MessageBus } from import { onChangeDetection } from './change-detection-tracker'; import { ComponentTreeNode, - getDirectiveForest, + getDirectiveForest, getForestWithNativeElements, getLatestComponentState, queryComponentTree, trimComponents, @@ -10,6 +10,7 @@ import { import { start as startProfiling, stop as stopProfiling } from './recording'; import { serializeComponentState } from './state-serializer'; import { ComponentInspector } from './component-inspector'; +import { setConsoleReference } from './selected-component'; const inspector: ComponentInspector = new ComponentInspector(); @@ -47,6 +48,15 @@ export const subscribeToClientEvents = (messageBus: MessageBus): void => } }); + messageBus.on('setSelectedComponent', (id: ElementID) => { + const node = queryComponentTree(id, getForestWithNativeElements); + // If message bus implements a getWindow function, use that window as the reference for setConsoleReference(). + // This is done so that if the dev tools are communicating with an iframe, the variable $ng0 + // is set at the parent window of that iframe instead of the iframe window. + const bus = (messageBus as any); + setConsoleReference(typeof bus.getWindow === 'function' ? bus.getWindow() : window, node); + }); + messageBus.on('getNestedProperties', (id: DirectiveID, propPath: string[]) => { const node = queryComponentTree(id.element); if (node) { diff --git a/projects/ng-devtools-backend/src/lib/component-tree.ts b/projects/ng-devtools-backend/src/lib/component-tree.ts index bf5832c3241cc..43ab7035727ca 100644 --- a/projects/ng-devtools-backend/src/lib/component-tree.ts +++ b/projects/ng-devtools-backend/src/lib/component-tree.ts @@ -69,26 +69,30 @@ export const trimComponents = (roots: ComponentTreeNode[]): ComponentTreeNode[] : null, directives: node.directives.map(d => ({ name: d.name })), children: trimComponents(node.children), + nativeElement: node.nativeElement } as ComponentTreeNode; }); }; +export const getForestWithNativeElements = (root = document.documentElement): ComponentTreeNode[] => + buildDirectiveForest(root, { element: '__ROOT__', component: null, directives: [], children: [] }, { getDirectives: true, includeNativeElement: true}); + export const getDirectiveForest = (root = document.documentElement): ComponentTreeNode[] => - buildDirectiveForest(root, { element: '__ROOT__', component: null, directives: [], children: [] }, true); + buildDirectiveForest(root, { element: '__ROOT__', component: null, directives: [], children: [] }, { getDirectives: true}); export const getComponentForest = (root = document.documentElement): ComponentTreeNode[] => - buildDirectiveForest(root, { element: '__ROOT__', component: null, directives: [], children: [] }, false); + buildDirectiveForest(root, { element: '__ROOT__', component: null, directives: [], children: [] }); const buildDirectiveForest = ( node: Element, tree: ComponentTreeNode | undefined, - getDirectives: boolean + options: { [option: string]: boolean } = {} ): ComponentTreeNode[] => { if (!node) { return [tree]; } let dirs = []; - if (tree.element !== '__ROOT__' && getDirectives) { + if (tree.element !== '__ROOT__' && options.getDirectives) { // Need to make sure we're in a component tree // otherwise, ng.getDirectives will throw without // a root node. @@ -100,7 +104,7 @@ const buildDirectiveForest = ( } const cmp = ng.getComponent(node); if (!cmp && !dirs.length) { - Array.from(node.children).forEach(c => buildDirectiveForest(c, tree, getDirectives)); + Array.from(node.children).forEach(c => buildDirectiveForest(c, tree, options)); return tree.children; } const current: ComponentTreeNode = { @@ -115,6 +119,10 @@ const buildDirectiveForest = ( children: [], }; + if (options.includeNativeElement) { + current.nativeElement = () => node; + } + if (cmp) { current.component = { instance: cmp, @@ -125,17 +133,17 @@ const buildDirectiveForest = ( current.element = node.tagName.toLowerCase(); } tree.children.push(current); - Array.from(node.children).forEach(c => buildDirectiveForest(c, current, getDirectives)); + Array.from(node.children).forEach(c => buildDirectiveForest(c, current, options)); return tree.children; }; // Based on an ElementID we return a specific component node. // If we can't find any, we return null. -export const queryComponentTree = (id: ElementID): ComponentTreeNode | null => { +export const queryComponentTree = (id: ElementID, treeGenerator: (root?: HTMLElement) => ComponentTreeNode[] = getDirectiveForest): ComponentTreeNode | null => { if (!id.length) { return null; } - let forest = getDirectiveForest(); + let forest = treeGenerator(); let node: null | ComponentTreeNode = null; for (const i of id) { node = forest[i]; diff --git a/projects/ng-devtools-backend/src/lib/selected-component.ts b/projects/ng-devtools-backend/src/lib/selected-component.ts new file mode 100644 index 0000000000000..656cc4634e2d4 --- /dev/null +++ b/projects/ng-devtools-backend/src/lib/selected-component.ts @@ -0,0 +1,17 @@ +import { ComponentTreeNode } from './component-tree'; + +declare const ng: any; + +const SELECTED_COMPONENT_PROPERTY_KEY_BASE = '$ng0'; + +export const setConsoleReference = (windowRef: Window, node: ComponentTreeNode) => { + Object.defineProperty(windowRef, SELECTED_COMPONENT_PROPERTY_KEY_BASE, { + get: () => { + if (node) { + return ng.getComponent(node.nativeElement()) || ng.getDebugNode(node.nativeElement()); + } + return node.nativeElement(); + }, + configurable: true + }); +}; diff --git a/projects/ng-devtools-backend/src/lib/utils.ts b/projects/ng-devtools-backend/src/lib/utils.ts index 829b1f24af621..8818583e380c5 100644 --- a/projects/ng-devtools-backend/src/lib/utils.ts +++ b/projects/ng-devtools-backend/src/lib/utils.ts @@ -12,8 +12,8 @@ export const patchTemplate = (instance: any, fn: () => void) => { const metadata = componentMetadata(instance); const original = metadata.template; - metadata.tView.template = metadata.template = function () { - const result = original.apply(this, arguments); + metadata.tView.template = metadata.template = (...args) => { + const result = original(...args); fn(); return result; }; diff --git a/projects/ng-devtools/src/lib/devtools-tabs/component-explorer/component-explorer.component.ts b/projects/ng-devtools/src/lib/devtools-tabs/component-explorer/component-explorer.component.ts index ae4b58357dae4..aab9dc2e8e371 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/component-explorer/component-explorer.component.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/component-explorer/component-explorer.component.ts @@ -31,6 +31,7 @@ export class ComponentExplorerComponent implements OnInit { handleNodeSelection(node: IndexedNode): void { this.currentSelectedElement = node; this.messageBus.emit('getElementDirectivesProperties', [node.id]); + this.messageBus.emit('setSelectedComponent', [node.id]); } ngOnInit(): void { diff --git a/projects/protocol/src/lib/messages.ts b/projects/protocol/src/lib/messages.ts index 5f09d1a144718..471120dccd9c1 100644 --- a/projects/protocol/src/lib/messages.ts +++ b/projects/protocol/src/lib/messages.ts @@ -11,6 +11,7 @@ export interface Node { directives: DirType[]; component: CmpType | null; children: Node[]; + nativeElement?: () => Element; } export enum PropType { @@ -122,6 +123,8 @@ export interface Events { getNestedProperties: (id: DirectiveID, path: string[]) => void; nestedProperties: (id: DirectiveID, data: Properties, path: string[]) => void; + setSelectedComponent: (id: ElementID) => void; + componentTreeDirty: () => void; getLatestComponentExplorerView: (query: ComponentExplorerViewQuery) => void; latestComponentExplorerView: (view: ComponentExplorerView) => void; diff --git a/src/iframe-message-bus.ts b/src/iframe-message-bus.ts index 2c9d13ccf4cc6..2b116bdb021c3 100644 --- a/src/iframe-message-bus.ts +++ b/src/iframe-message-bus.ts @@ -66,8 +66,12 @@ export class IFrameMessageBus extends MessageBus { ); } - destroy() { + destroy(): void { this._listeners.forEach(l => window.removeEventListener('message', l)); this._listeners = []; } + + getWindow(): Window { + return this._docWindow; + } } diff --git a/src/zone-unaware-iframe-message-bus.ts b/src/zone-unaware-iframe-message-bus.ts index 4ad8d30623b01..435c077589308 100644 --- a/src/zone-unaware-iframe-message-bus.ts +++ b/src/zone-unaware-iframe-message-bus.ts @@ -52,4 +52,8 @@ export class ZoneUnawareIFrameMessageBus extends MessageBus { destroy() { this._delegate.destroy(); } + + getWindow(): Window { + return this._delegate.getWindow(); + } }