mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
GitHub
parent
544d82fcc3
commit
285b5dbe91
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+11
-2
@@ -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 = {};
|
||||
|
||||
+11
-8
@@ -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) {
|
||||
|
||||
+16
@@ -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>
|
||||
+24
@@ -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;
|
||||
}
|
||||
+37
@@ -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;
|
||||
}
|
||||
}
|
||||
+29
-13
@@ -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>
|
||||
|
||||
+33
-12
@@ -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;
|
||||
}
|
||||
|
||||
+8
-1
@@ -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
-4
@@ -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;
|
||||
}
|
||||
|
||||
+9
-2
@@ -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 {}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user