feat(devtools): allow user to view change detection strategy and view encapsulation of a component in the property explorer (rangle/angular-devtools#257)

This commit is contained in:
AleksanderBodurri
2020-04-08 14:35:50 -04:00
committed by GitHub
parent 544d82fcc3
commit 285b5dbe91
12 changed files with 209 additions and 55 deletions
@@ -3,6 +3,7 @@ import { deeplySerializeSelectedProperties, serializeDirectiveState } from './st
import {
ComponentExplorerViewQuery,
DevToolsNode,
DirectiveMetadata,
DirectivesProperties,
ElementPosition,
PropertyQueryTypes,
@@ -10,6 +11,7 @@ import {
} from 'protocol';
import { IndexedNode } from './observer/identity-tracker';
import { buildDirectiveTree, getLViewFromDirectiveOrElementInstance } from './lview-transform';
import { ViewEncapsulation } from '@angular/core';
const ngDebug = () => (window as any).ng;
@@ -40,15 +42,13 @@ export const getLatestComponentState = (query: ComponentExplorerViewQuery): Dire
if (query.propertyQuery.type === PropertyQueryTypes.All) {
result[dir.name] = {
props: serializeDirectiveState(dir.instance),
inputs: getDirectiveMetaData(dir.instance).inputs(),
outputs: getDirectiveMetaData(dir.instance).outputs(),
metadata: getDirectiveMetadata(dir.instance),
};
}
if (query.propertyQuery.type === PropertyQueryTypes.Specified) {
result[dir.name] = {
props: deeplySerializeSelectedProperties(dir.instance, query.propertyQuery.properties[dir.name] || []),
inputs: getDirectiveMetaData(dir.instance).inputs(),
outputs: getDirectiveMetaData(dir.instance).outputs(),
metadata: getDirectiveMetadata(dir.instance),
};
}
};
@@ -61,19 +61,28 @@ export const getLatestComponentState = (query: ComponentExplorerViewQuery): Dire
return result;
};
export const getDirectiveMetaData = (dir: any) => {
const getDirInputOrOutput = (inputOrOutputKey: 'inputs' | 'outputs') => {
const enum DirectiveMetadataKey {
INPUTS = 'inputs',
OUTPUTS = 'outputs',
ENCAPSULATION = 'encapsulation',
ON_PUSH = 'onPush',
}
export const getDirectiveMetadata = (dir: any): DirectiveMetadata => {
const safelyGrabMetadata = (key: DirectiveMetadataKey) => {
try {
return dir.constructor.ɵcmp ? dir.constructor.ɵcmp[inputOrOutputKey] : dir.constructor.ɵdir[inputOrOutputKey];
return dir.constructor.ɵcmp ? dir.constructor.ɵcmp[key] : dir.constructor.ɵdir[key];
} catch {
console.warn('Could not find metadata for: ', dir);
return {};
console.warn(`Could not find metadata for key: ${key} in directive:`, dir);
return undefined;
}
};
return {
inputs: () => getDirInputOrOutput('inputs'),
outputs: () => getDirInputOrOutput('outputs'),
inputs: safelyGrabMetadata(DirectiveMetadataKey.INPUTS),
outputs: safelyGrabMetadata(DirectiveMetadataKey.OUTPUTS),
encapsulation: safelyGrabMetadata(DirectiveMetadataKey.ENCAPSULATION),
onPush: safelyGrabMetadata(DirectiveMetadataKey.ON_PUSH),
};
};
@@ -5,6 +5,7 @@ import { FlatTreeControl } from '@angular/cdk/tree';
import { getExpandedDirectiveProperties } from './property-expanded-directive-properties';
import { Observable } from 'rxjs';
import { Property, FlatNode } from './element-property-resolver';
import { ViewEncapsulation } from '@angular/core';
export interface DirectiveTreeData {
dataSource: PropertyDataSource;
@@ -91,6 +92,14 @@ export class DirectivePropertyResolver {
return this._directivePosition;
}
get directiveViewEncapsulation(): ViewEncapsulation | undefined {
return this._props.metadata?.encapsulation;
}
get directiveHasOnPushStrategy(): boolean | undefined {
return this._props.metadata?.onPush;
}
getExpandedProperties(): NestedProp[] {
return [
...getExpandedDirectiveProperties(this._inputsDataSource.data),
@@ -156,8 +165,8 @@ export class DirectivePropertyResolver {
outputProps: { [name: string]: Descriptor };
stateProps: { [name: string]: Descriptor };
} {
const inputLabels: Set<string> = new Set(Object.keys(this._props.inputs || {}));
const outputLabels: Set<string> = new Set(Object.keys(this._props.outputs || {}));
const inputLabels: Set<string> = new Set(Object.keys(this._props.metadata?.inputs || {}));
const outputLabels: Set<string> = new Set(Object.keys(this._props.metadata?.outputs || {}));
const inputProps = {};
const outputProps = {};
@@ -33,14 +33,7 @@ export class ElementPropertyResolver {
}
setProperties(indexedNode: IndexedNode, data: DirectivesProperties): void {
// To prevent memory leaks when a directive no longer exists on an element
const currentProps = [...this._directivePropertiesController.keys()];
const incomingProps = new Set(Object.keys(data));
for (const prop of currentProps) {
if (!incomingProps.has(prop)) {
this._directivePropertiesController.delete(prop);
}
}
this._flushDeletedProperties(data);
Object.keys(data).forEach((key) => {
const controller = this._directivePropertiesController.get(key);
@@ -62,6 +55,16 @@ export class ElementPropertyResolver {
});
}
private _flushDeletedProperties(data: DirectivesProperties): void {
const currentProps = [...this._directivePropertiesController.keys()];
const incomingProps = new Set(Object.keys(data));
for (const prop of currentProps) {
if (!incomingProps.has(prop)) {
this._directivePropertiesController.delete(prop);
}
}
}
getExpandedProperties(): ComponentExplorerViewProperties {
const result: ComponentExplorerViewProperties = {};
for (const [directive] of this._directivePropertiesController) {
@@ -0,0 +1,16 @@
<a
class="view-link"
*ngIf="viewEncapsulation; let viewEncapsulation"
href="https://angular.io/api/core/ViewEncapsulation"
target="_blank"
>
View Encapsulation: <span class="meta-data">{{ viewEncapsulation }}</span>
</a>
<a
class="view-link"
*ngIf="changeDetectionStrategy; let changeDetectionStrategy"
href="https://angular.io/api/core/ChangeDetectionStrategy"
target="_blank"
>
Change Detection Strategy: <span class="meta-data">{{ changeDetectionStrategy }}</span>
</a>
@@ -0,0 +1,24 @@
.view-link {
display: block;
font-size: 11px;
padding: 2px;
color: unset;
text-decoration: unset;
&:focus {
outline: none;
}
&:hover {
background-color: #eee;
cursor: pointer;
}
&:active {
background-color: #bcbcbc;
}
}
.meta-data {
font-weight: 400;
}
@@ -0,0 +1,37 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { DirectivePropertyResolver } from '../../../property-resolver/directive-property-resolver';
import { ElementPropertyResolver } from '../../../property-resolver/element-property-resolver';
import { ComponentType } from 'protocol';
@Component({
selector: 'ng-component-metadata',
templateUrl: './component-metadata.component.html',
styleUrls: ['./component-metadata.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ComponentMetadataComponent {
@Input() currentSelectedComponent: ComponentType;
constructor(private _nestedProps: ElementPropertyResolver) {}
viewEncapsulationModes = ['Emulated', 'Native', 'None', 'ShadowDom'];
get controller(): DirectivePropertyResolver | undefined {
if (!this.currentSelectedComponent) {
return;
}
return this._nestedProps.getDirectiveController(this.currentSelectedComponent.name);
}
get viewEncapsulation(): string | undefined {
const encapsulationIndex = this?.controller?.directiveViewEncapsulation;
if (encapsulationIndex !== undefined) {
return this.viewEncapsulationModes[encapsulationIndex];
}
}
get changeDetectionStrategy(): string | undefined {
const onPush = this?.controller?.directiveHasOnPushStrategy;
return onPush ? 'On Push' : onPush !== undefined ? 'Default' : undefined;
}
}
@@ -1,19 +1,35 @@
<ng-container *ngIf="currentSelectedElement; else emptyState">
<div class="element-header">
<ng-container *ngIf="currentSelectedElement.component; else currentElementIsDOMElement">
<div class="component-name">{{ currentSelectedElement.component.name }}</div>
<button mat-icon-button (click)="viewSource.emit()">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z" />
</svg>
</button>
</ng-container>
<ng-template #currentElementIsDOMElement>
<ng-container *ngIf="currentSelectedElement.component; else currentElementIsDOMElement">
<mat-accordion class="property-tab-header">
<div>
<mat-expansion-panel [hideToggle]="true">
<mat-expansion-panel-header collapsedHeight="25px" expandedHeight="25px">
<mat-panel-title>
<div class="element-header">
<div class="component-name">{{ currentSelectedElement.component.name }}</div>
<button mat-icon-button (click)="handleViewSource($event)">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path
d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"
/>
</svg>
</button>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-component-metadata [currentSelectedComponent]="currentSelectedElement.component"></ng-component-metadata>
</mat-expansion-panel>
</div>
</mat-accordion>
</ng-container>
<ng-template #currentElementIsDOMElement>
<div class="element-header">
<div class="element-name">{{ currentSelectedElement.element }}</div>
</ng-template>
</div>
</div>
</ng-template>
</ng-container>
<ng-template #emptyState>
<div class="element-header">
<div class="element-name">No selected Element</div>
@@ -1,9 +1,16 @@
.element-header {
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
font-size: 12px;
.mat-icon-button {
height: 25px;
width: 25px;
line-height: 25px;
}
.component-name {
margin-left: 10px;
font-weight: 700;
@@ -18,17 +25,31 @@
}
::ng-deep {
.mat-button {
height: 25px;
width: 25px;
line-height: 25px;
margin-right: 5px;
.property-tab-header {
&.mat-accordion {
.mat-expansion-panel {
&:not(.mat-expanded) {
.mat-expansion-panel-header {
&:hover:not([aria-disabled='true']) {
background: #fff;
}
}
}
.mat-expansion-panel-header {
padding: 0;
.mat-expansion-panel-header-title {
margin-right: 0;
}
}
.mat-expansion-panel-content {
.mat-expansion-panel-body {
padding: 0;
}
}
}
}
}
}
.mat-icon-button {
height: 25px;
width: 25px;
line-height: 25px;
margin-right: 5px;
}
@@ -1,12 +1,19 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { IndexedNode } from '../../directive-forest/index-forest';
@Component({
templateUrl: './property-tab-header.component.html',
selector: 'ng-property-tab-header',
styleUrls: ['./property-tab-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PropertyTabHeaderComponent {
@Input() currentSelectedElement: IndexedNode;
@Input() currentDirectives: string[] | undefined;
@Output() viewSource = new EventEmitter<void>();
handleViewSource(event: MouseEvent): void {
event.stopPropagation();
this.viewSource.emit();
}
}
@@ -1,6 +1,5 @@
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { IndexedNode } from '../directive-forest/index-forest';
import { PropertyTabBodyComponent } from './property-tab-body/property-tab-body.component';
import { FlatNode } from '../property-resolver/element-property-resolver';
import { DirectivePosition } from 'protocol';
@@ -12,6 +11,4 @@ export class PropertyTabComponent {
@Input() currentSelectedElement: IndexedNode;
@Output() viewSource = new EventEmitter<void>();
@Output() inspectFunction = new EventEmitter<{ node: FlatNode; directivePosition: DirectivePosition }>();
@ViewChild(PropertyTabBodyComponent) propertyTabBody: PropertyTabBodyComponent;
}
@@ -5,10 +5,17 @@ import { PropertyTabHeaderComponent } from './property-tab-header/property-tab-h
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { PropertyTabBodyComponent } from './property-tab-body/property-tab-body.component';
import { MatExpansionModule } from '@angular/material/expansion';
import { ComponentMetadataComponent } from './property-tab-header/component-metadata/component-metadata.component';
@NgModule({
declarations: [PropertyTabComponent, PropertyTabHeaderComponent, PropertyTabBodyComponent],
imports: [PropertyViewModule, CommonModule, MatButtonModule],
declarations: [
PropertyTabComponent,
PropertyTabHeaderComponent,
PropertyTabBodyComponent,
ComponentMetadataComponent,
],
imports: [PropertyViewModule, CommonModule, MatButtonModule, MatExpansionModule],
exports: [PropertyTabComponent],
})
export class PropertyTabModule {}
+10 -2
View File
@@ -1,3 +1,5 @@
import { ViewEncapsulation } from '@angular/core';
export interface DirectiveType {
name: string;
id: number;
@@ -45,10 +47,16 @@ export interface DirectivesProperties {
[name: string]: Properties;
}
export interface DirectiveMetadata {
inputs: { [name: string]: string };
outputs: { [name: string]: string };
encapsulation: ViewEncapsulation;
onPush: boolean;
}
export interface Properties {
props: { [name: string]: Descriptor };
inputs?: { [name: string]: string };
outputs?: { [name: string]: string };
metadata?: DirectiveMetadata;
}
export type ElementPosition = number[];