Skip to content

Commit

Permalink
feat(devtools): add support for Angular elements
Browse files Browse the repository at this point in the history
Currently, we only show which components are Angular elements. We don't visualize them in their position in the component tree, but only indicate with different coloring.

In the profiler we should also somehow show which tiles correspond to elements. We can discuss this further in rangle/angular-devtools#112.
  • Loading branch information
mgechev authored and AleksanderBodurri committed Mar 11, 2020
1 parent ef16144 commit becdcca
Show file tree
Hide file tree
Showing 20 changed files with 145 additions and 28 deletions.
12 changes: 12 additions & 0 deletions cypress/integration/elements.e2e.js
@@ -0,0 +1,12 @@
describe('Angular Elements', () => {
beforeEach(() => {
cy.visit('/');
});

it('should recognize the zippy as an Angular Element', () => {
cy.get('mat-tree')
.find('mat-tree-node:contains("app-zippy")')
.its('length')
.should('eq', 1);
});
});
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -36,6 +36,7 @@
"@angular/common": "~9.0.0",
"@angular/compiler": "~9.0.0",
"@angular/core": "~9.0.0",
"@angular/elements": "9.0.2",
"@angular/forms": "~9.0.0",
"@angular/material": "~9.0.0",
"@angular/platform-browser": "~9.0.0",
Expand All @@ -44,6 +45,7 @@
"@bahmutov/add-typescript-to-cypress": "^2.1.2",
"angular-split": "^3.0.3",
"clone-deep": "^4.0.1",
"document-register-element": "^1.7.2",
"file-saver": "^2.0.2",
"ngx-flamegraph": "0.0.3",
"ngx-vis": "3.0.3",
Expand Down
Expand Up @@ -200,6 +200,7 @@ export const prepareForestForSerialization = (roots: ComponentTreeNode[]): Seria
component: node.component
? {
name: node.component.name,
isElement: node.component.isElement,
id: getDirectiveId(node.component.instance),
}
: null,
Expand Down
6 changes: 3 additions & 3 deletions projects/ng-devtools-backend/src/lib/component-tree.ts
Expand Up @@ -7,10 +7,9 @@ import {
DirectivesProperties,
UpdatedStateData,
} from 'protocol';
import { getComponentName } from './highlighter';
import { DebuggingAPI } from './interfaces';
import { IndexedNode } from './observer/identity-tracker';
import { buildDirectiveTree } from './lview-transform';
import { buildDirectiveTree, METADATA_PROPERTY_NAME } from './lview-transform';

const ngDebug = (window as any).ng;

Expand All @@ -22,6 +21,7 @@ export interface DirectiveInstanceType {
export interface ComponentInstanceType {
instance: any;
name: string;
isElement: boolean;
}

export interface ComponentTreeNode extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType> {
Expand Down Expand Up @@ -63,7 +63,7 @@ export const getLatestComponentState = (query: ComponentExplorerViewQuery): Dire

export const buildDirectiveForest = (ngd: DebuggingAPI): ComponentTreeNode[] => {
const roots = Array.from(document.documentElement.querySelectorAll('[ng-version]')).map(
el => ngd.getComponent(el).__ngContext__
el => ngd.getComponent(el)[METADATA_PROPERTY_NAME]
);
return Array.prototype.concat.apply([], roots.map(buildDirectiveTree));
};
Expand Down
8 changes: 4 additions & 4 deletions projects/ng-devtools-backend/src/lib/highlighter.ts
@@ -1,11 +1,11 @@
import { Type } from '@angular/core';

let overlay;
let overlayContent;
let overlay: any;
let overlayContent: any;

declare const ng: any;

export const DevToolsHighlightNodeId = '____ngDevToolsHighlight';
export const DEV_TOOLS_HIGHLIGHT_NODE_ID = '____ngDevToolsHighlight';

function init(): void {
if (overlay) {
Expand All @@ -20,7 +20,7 @@ function init(): void {
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.borderRadius = '3px';
overlay.setAttribute('id', DevToolsHighlightNodeId);
overlay.setAttribute('id', DEV_TOOLS_HIGHLIGHT_NODE_ID);
overlayContent = document.createElement('div');
overlayContent.style.backgroundColor = 'rgba(104, 182, 255, 0.9)';
overlayContent.style.fontFamily = 'monospace';
Expand Down
27 changes: 22 additions & 5 deletions projects/ng-devtools-backend/src/lib/lview-transform.ts
@@ -1,26 +1,43 @@
import { ComponentTreeNode } from './component-tree';
import { ComponentTreeNode, ComponentInstanceType } from './component-tree';
import { isCustomElement } from './utils';

const HEADER_OFFSET = 19;
const TYPE = 1;
const ELEMENT = 0;
const LVIEW_TVIEW = 1;
const COMPONENTS = 8;
export const METADATA_PROPERTY_NAME = '__ngContext__';

export function isLContainer(value: any): boolean {
return Array.isArray(value) && value[TYPE] === true;
}

export const getDirectiveHostElement = (dir: any) => {
const ctx = dir[METADATA_PROPERTY_NAME];
if (ctx[0] !== null) {
return ctx[0];
}
const components = ctx[LVIEW_TVIEW].components;
if (!components || components.length !== 1) {
return false;
}
return ctx[components[0]][0];
};

const getNode = (lView: any, data: any, idx: number): ComponentTreeNode => {
const directives = [];
let component = null;
let component: ComponentInstanceType | null = null;
const tNode = data[idx];
const element = (lView[idx][ELEMENT].tagName || lView[idx][ELEMENT].nodeName).toLowerCase();
const node = lView[idx][ELEMENT] || lView[idx][ELEMENT];
const elementName = (node.tagName || node.nodeName).toLowerCase();
for (let i = tNode.directiveStart; i < tNode.directiveEnd; i++) {
const dir = lView[i];
const dirMeta = data[i];
if (dirMeta && dirMeta.template) {
component = {
name: element,
name: elementName,
instance: dir,
isElement: isCustomElement(node),
};
} else if (dirMeta) {
directives.push({
Expand All @@ -30,7 +47,7 @@ const getNode = (lView: any, data: any, idx: number): ComponentTreeNode => {
}
}
return {
element,
element: elementName,
nativeElement: lView[idx][ELEMENT],
directives,
component,
Expand Down
12 changes: 9 additions & 3 deletions projects/ng-devtools-backend/src/lib/observer/index.ts
@@ -1,6 +1,6 @@
import { ComponentTreeObserver } from './observer';
import { ElementPosition, ProfilerFrame, ElementProfile, DirectiveProfile, LifecycleProfile } from 'protocol';
import { runOutsideAngular } from '../utils';
import { runOutsideAngular, isCustomElement } from '../utils';
import { getComponentName } from '../highlighter';
import { InsertionTrie } from './insertion-trie';
import { ComponentTreeNode } from '../component-tree';
Expand All @@ -23,16 +23,17 @@ export const start = (onFrame: (frame: ProfilerFrame) => void): void => {
observer = new ComponentTreeObserver({
// We flush here because it's possible the current node to overwrite
// an existing removed node.
onCreate(directive: any, id: number, isComponent: boolean, position: ElementPosition): void {
onCreate(directive: any, node: Node, id: number, isComponent: boolean, position: ElementPosition): void {
eventMap.set(directive, {
name: getComponentName(directive),
isElement: isCustomElement(node),
isComponent,
changeDetection: 0,
lifecycle: {},
});
insertionTrie.insert(position);
},
onChangeDetection(component: any, id: number, position: ElementPosition, duration: number): void {
onChangeDetection(component: any, node: Node, id: number, position: ElementPosition, duration: number): void {
if (!inChangeDetection) {
inChangeDetection = true;
const source = getChangeDetectionSource();
Expand All @@ -46,6 +47,7 @@ export const start = (onFrame: (frame: ProfilerFrame) => void): void => {
if (!eventMap.has(component)) {
eventMap.set(component, {
name: getComponentName(component),
isElement: isCustomElement(node),
isComponent: true,
changeDetection: 0,
lifecycle: {},
Expand All @@ -61,6 +63,7 @@ export const start = (onFrame: (frame: ProfilerFrame) => void): void => {
},
onLifecycleHook(
directive: any,
node: Node,
id: number,
isComponent: boolean,
hook: keyof LifecycleProfile,
Expand All @@ -69,6 +72,7 @@ export const start = (onFrame: (frame: ProfilerFrame) => void): void => {
if (!eventMap.has(directive)) {
eventMap.set(directive, {
name: getComponentName(directive),
isElement: isCustomElement(node),
isComponent: true,
changeDetection: 0,
lifecycle: {},
Expand Down Expand Up @@ -174,6 +178,7 @@ const prepareInitialFrame = (source: string) => {
const directives = node.directives.map(d => {
return {
isComponent: false,
isElement: false,
name: d.name,
lifecycle: {},
changeDetection: 0,
Expand All @@ -182,6 +187,7 @@ const prepareInitialFrame = (source: string) => {
if (node.component) {
directives.push({
changeDetection: 0,
isElement: node.component.isElement,
isComponent: true,
lifecycle: {},
name: node.component.name,
Expand Down
27 changes: 22 additions & 5 deletions projects/ng-devtools-backend/src/lib/observer/observer.ts
@@ -1,24 +1,33 @@
import { ElementPosition, LifecycleProfile } from 'protocol';
import { componentMetadata } from '../utils';
import { IdentityTracker, IndexedNode } from './identity-tracker';
import { DevToolsHighlightNodeId } from '../highlighter';
import { DEV_TOOLS_HIGHLIGHT_NODE_ID } from '../highlighter';
import { METADATA_PROPERTY_NAME, getDirectiveHostElement } from '../lview-transform';

export type CreationCallback = (
componentOrDirective: any,
node: Node,
id: number,
isComponent: boolean,
position: ElementPosition
) => void;

export type LifecycleCallback = (
componentOrDirective: any,
node: Node,
id: number,
isComponent: boolean,
hook: keyof LifecycleProfile | 'unknown',
duration: number
) => void;

export type ChangeDetectionCallback = (component: any, id: number, position: ElementPosition, duration: number) => void;
export type ChangeDetectionCallback = (
component: any,
node: Node,
id: number,
position: ElementPosition,
duration: number
) => void;

export type DestroyCallback = (
componentOrDirective: any,
Expand Down Expand Up @@ -148,7 +157,13 @@ export class ComponentTreeObserver {
return;
}
const position = this._tracker.getDirectivePosition(component);
this._config.onCreate(component, this._tracker.getDirectiveId(component), isComponent, position);
this._config.onCreate(
component,
getDirectiveHostElement(component),
this._tracker.getDirectiveId(component),
isComponent,
position
);
}

private _fireDestroyCallback(component: any, isComponent: boolean): void {
Expand All @@ -172,6 +187,7 @@ export class ComponentTreeObserver {
if (self._tracker.hasDirective(component)) {
self._config.onChangeDetection(
component,
getDirectiveHostElement(component),
self._tracker.getDirectiveId(component),
self._tracker.getDirectivePosition(component),
performance.now() - start
Expand All @@ -185,7 +201,7 @@ export class ComponentTreeObserver {
}

private _observeLifecycle(directive: any, isComponent: boolean): void {
const ctx = directive.__ngContext__;
const ctx = directive[METADATA_PROPERTY_NAME];
const tview = ctx[1];
hookTViewProperties.forEach(hook => {
const current = tview[hook];
Expand All @@ -204,6 +220,7 @@ export class ComponentTreeObserver {
if (self._tracker.hasDirective(this)) {
self._config.onLifecycleHook(
this,
getDirectiveHostElement(this),
self._tracker.getDirectiveId(this),
isComponent,
getLifeCycleName(el.name),
Expand Down Expand Up @@ -242,7 +259,7 @@ const containsInternalElements = (nodes: NodeList): boolean => {
continue;
}
const attr = node.getAttribute('id');
if (attr === DevToolsHighlightNodeId) {
if (attr === DEV_TOOLS_HIGHLIGHT_NODE_ID) {
return true;
}
}
Expand Down
@@ -1,4 +1,5 @@
import { Descriptor, NestedProp, PropType } from 'protocol';
import { METADATA_PROPERTY_NAME } from '../lview-transform';

interface PropData {
type: PropType;
Expand Down Expand Up @@ -26,7 +27,7 @@ interface CommonTypeCases {
unknownCase?: () => any;
}

const ignoreList = new Set(['__ngContext__', '__ngSimpleChanges__']);
const ignoreList = new Set([METADATA_PROPERTY_NAME, '__ngSimpleChanges__']);

const shallowPropTypeToTreeMetaData = {
[PropType.String]: {
Expand Down
Expand Up @@ -4,8 +4,9 @@ import {
createNestedSerializedDescriptor,
createShallowSerializedDescriptor,
} from './serialized-descriptor-factory';
import { METADATA_PROPERTY_NAME } from '../lview-transform';

const ignoreList = new Set(['__ngContext__', '__ngSimpleChanges__']);
const ignoreList = new Set([METADATA_PROPERTY_NAME, '__ngSimpleChanges__']);

const commonTypes = {
boolean: PropType.Boolean,
Expand Down
11 changes: 11 additions & 0 deletions projects/ng-devtools-backend/src/lib/utils.ts
Expand Up @@ -20,3 +20,14 @@ export const patchTemplate = (instance: any, fn: () => void) => {

return original;
};

export const isCustomElement = (node: Node) => {
if (typeof customElements === 'undefined') {
return false;
}
if (!(node instanceof HTMLElement)) {
return false;
}
const tagName = node.tagName.toLowerCase();
return !!customElements.get(tagName);
};
Expand Up @@ -117,3 +117,16 @@
height: calc(100% - 50px);
overflow-y: auto;
}

.angular-element {
content: '';
color: blue;
}

.angular-element::before {
content: '<';
}

.angular-element::after {
content: '/>';
}
Expand Up @@ -23,7 +23,7 @@
matTreeNodePadding
>
<button disabled></button>
<span class="element-name">{{ node.name }}</span>
<span class="element-name" [class.angular-element]="isElement(node)">{{ node.name }}</span>
<span *ngIf="node.directives" class="dir-names">[{{ node.directives }}]</span>
</mat-tree-node>
<mat-tree-node
Expand All @@ -46,9 +46,9 @@
{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
</mat-icon>
</button>
<span class="element-name">{{ node.name }}</span>
<span class="element-name" [class.angular-element]="isElement(node)">{{ node.name }}</span>
<span *ngIf="node.directives" class="dir-names">[{{ node.directives }}]</span>
</mat-tree-node>
</mat-tree>
</div>
<ng-breadcrumbs (handleSelect)="handleSelect($event)" [parents]="parents"></ng-breadcrumbs>
<ng-breadcrumbs (handleSelect)="handleSelect($event)" [parents]="parents"></ng-breadcrumbs>
Expand Up @@ -286,4 +286,8 @@ export class DirectiveForestComponent {
!!this.highlightIDinTreeFromElement && this.highlightIDinTreeFromElement.join(',') === node.position.join(',')
);
}

isElement(node: FlatNode) {
return node.original.component && node.original.component.isElement;
}
}

0 comments on commit becdcca

Please sign in to comment.