mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(devtools): implement input/output/state preview functionality with the reworked property explorer
This commit is contained in:
parent
39b65dd05a
commit
8ea84d5776
15 changed files with 289 additions and 48 deletions
|
|
@ -86,7 +86,7 @@ describe('Search items in component tree', () => {
|
|||
checkComponentName('app-todos');
|
||||
|
||||
// should display correct title for properties panel
|
||||
cy.get('header').should('have.text', ' Properties of app-todos ');
|
||||
cy.get('header span').should('have.text', ' Properties of app-todos ');
|
||||
|
||||
// should show correct component properties
|
||||
cy.get('ng-property-view').find('mat-tree-node');
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ const sameDirectives = (a: IndexedNode, b: IndexedNode) => {
|
|||
],
|
||||
})
|
||||
export class DirectiveExplorerComponent implements OnInit {
|
||||
directivesData: DirectivesProperties | null = null;
|
||||
currentSelectedElement: IndexedNode | null = null;
|
||||
forest: DevToolsNode[];
|
||||
highlightIDinTreeFromElement: ElementPosition | null = null;
|
||||
|
|
@ -189,7 +188,7 @@ export class DirectiveExplorerComponent implements OnInit {
|
|||
let data = {};
|
||||
const controller = this._propResolver.getDirectiveController(directive);
|
||||
if (controller) {
|
||||
data = controller.getDirectiveProperties();
|
||||
data = controller.directiveProperties;
|
||||
}
|
||||
if (!e.clipboardData) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ import { getExpandedDirectiveProperties } from './property-expanded-directive-pr
|
|||
import { Observable } from 'rxjs';
|
||||
import { Property, FlatNode } from './element-property-resolver';
|
||||
|
||||
export enum PropertyViewFilterOptions {
|
||||
INPUTS,
|
||||
OUTPUTS,
|
||||
STATE,
|
||||
}
|
||||
|
||||
const expandable = (prop: Descriptor) => {
|
||||
if (!prop) {
|
||||
return false;
|
||||
|
|
@ -60,10 +66,18 @@ export class DirectivePropertyResolver {
|
|||
};
|
||||
}
|
||||
|
||||
getDirectiveProperties(): { [name: string]: Descriptor } {
|
||||
get directiveProperties(): { [name: string]: Descriptor } {
|
||||
return this._props.props;
|
||||
}
|
||||
|
||||
get directiveInputs(): string[] {
|
||||
return Object.keys(this._props.inputs || {});
|
||||
}
|
||||
|
||||
get directiveOutputs(): string[] {
|
||||
return Object.keys(this._props.outputs || {});
|
||||
}
|
||||
|
||||
getExpandedProperties(): NestedProp[] {
|
||||
return getExpandedDirectiveProperties(this._dataSource.data);
|
||||
}
|
||||
|
|
@ -91,7 +105,10 @@ export class DirectivePropertyResolver {
|
|||
|
||||
private _getChildren(prop: Property): Property[] | undefined {
|
||||
const descriptor = prop.descriptor;
|
||||
if (descriptor.type === PropType.Object && !(descriptor.value instanceof Observable)) {
|
||||
if (
|
||||
(descriptor.type === PropType.Object || descriptor.type === PropType.Array) &&
|
||||
!(descriptor.value instanceof Observable)
|
||||
) {
|
||||
return Object.keys(descriptor.value || {}).map(name => {
|
||||
return {
|
||||
name,
|
||||
|
|
@ -99,14 +116,6 @@ export class DirectivePropertyResolver {
|
|||
parent: prop,
|
||||
};
|
||||
});
|
||||
} else if (descriptor.type === PropType.Array && !(descriptor.value instanceof Observable)) {
|
||||
return (descriptor.value || []).map((el: Descriptor, idx: number) => {
|
||||
return {
|
||||
name: idx.toString(),
|
||||
descriptor: el,
|
||||
parent: prop,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
console.error('Unexpected data type', descriptor, 'in property', prop);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,20 @@ import { map } from 'rxjs/operators';
|
|||
import { DefaultIterableDiffer } from '@angular/core';
|
||||
import { diff } from '../../diffing';
|
||||
import { FlatNode, Property } from './element-property-resolver';
|
||||
import { PropertyViewFilterOptions } from './directive-property-resolver';
|
||||
|
||||
const expandable = (prop: Descriptor, messageBus?: MessageBus<Events>) => {
|
||||
if (!prop) {
|
||||
return false;
|
||||
}
|
||||
if (!prop.value && !messageBus) {
|
||||
return false;
|
||||
}
|
||||
if (!prop.expandable) {
|
||||
return false;
|
||||
}
|
||||
return !(prop.type !== PropType.Object && prop.type !== PropType.Array);
|
||||
};
|
||||
|
||||
const trackBy = (_: number, item: FlatNode) => {
|
||||
return `#${item.prop.name}#${item.level}`;
|
||||
|
|
@ -18,6 +32,8 @@ export class PropertyDataSource extends DataSource<FlatNode> {
|
|||
private _expandedData = new BehaviorSubject<FlatNode[]>([]);
|
||||
private _differ = new DefaultIterableDiffer(trackBy);
|
||||
|
||||
private readonly _originalData: FlatNode[];
|
||||
|
||||
constructor(
|
||||
props: { [prop: string]: Descriptor },
|
||||
private _treeFlattener: MatTreeFlattener<Property, FlatNode>,
|
||||
|
|
@ -28,7 +44,8 @@ export class PropertyDataSource extends DataSource<FlatNode> {
|
|||
private _onReceivedNestedProperties: () => void
|
||||
) {
|
||||
super();
|
||||
this._data.next(this._treeFlattener.flattenNodes(this._arrayify(props)));
|
||||
this._originalData = this._treeFlattener.flattenNodes(this._arrayify(props));
|
||||
this._data.next(this._originalData);
|
||||
}
|
||||
|
||||
get data(): FlatNode[] {
|
||||
|
|
@ -110,25 +127,23 @@ export class PropertyDataSource extends DataSource<FlatNode> {
|
|||
});
|
||||
}
|
||||
|
||||
private _getChildren(prop: Property): Property[] {
|
||||
const descriptor = prop.descriptor;
|
||||
if (descriptor.type === PropType.Object && !(descriptor.value instanceof Observable)) {
|
||||
return Object.keys(descriptor.value || {}).map(name => {
|
||||
return {
|
||||
name,
|
||||
descriptor: descriptor.value ? descriptor.value[name] : null,
|
||||
parent: prop,
|
||||
};
|
||||
});
|
||||
} else if (descriptor.type === PropType.Array && !(descriptor.value instanceof Observable)) {
|
||||
return (descriptor.value || []).map((el: Descriptor, idx: number) => {
|
||||
return {
|
||||
name: idx.toString(),
|
||||
descriptor: el,
|
||||
parent: prop,
|
||||
};
|
||||
});
|
||||
filterDataSource(filter: string[] | null): void {
|
||||
if (filter === null) {
|
||||
return;
|
||||
}
|
||||
throw new Error('Unexpected data type');
|
||||
let pushFlag = false;
|
||||
const filteredData: FlatNode[] = [];
|
||||
|
||||
// tslint:disable-next-line:prefer-for-of
|
||||
for (let i = 0; i < this._originalData.length; i++) {
|
||||
const node = this._originalData[i];
|
||||
if (node.level === 0) {
|
||||
pushFlag = filter.includes(node.prop.name);
|
||||
}
|
||||
if (pushFlag) {
|
||||
filteredData.push(node);
|
||||
}
|
||||
}
|
||||
this._data.next(filteredData);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||
import { PropertyDataSource } from '../../../../property-resolver/property-data-source';
|
||||
import { FlatNode } from '../../../../property-resolver/element-property-resolver';
|
||||
|
|
@ -9,13 +9,22 @@ import { DirectivePropertyResolver } from '../../../../property-resolver/directi
|
|||
templateUrl: './property-view-body.component.html',
|
||||
styleUrls: ['./property-view-body.component.css'],
|
||||
})
|
||||
export class PropertyViewBodyComponent {
|
||||
export class PropertyViewBodyComponent implements OnChanges {
|
||||
@Input() dataSource: PropertyDataSource;
|
||||
@Input() treeControl: FlatTreeControl<FlatNode>;
|
||||
@Input() controller: DirectivePropertyResolver;
|
||||
@Input() filterList: string[] | null = null;
|
||||
|
||||
hasChild = (_: number, node: FlatNode): boolean => node.expandable;
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.filterTreeNodes();
|
||||
}
|
||||
|
||||
filterTreeNodes(): void {
|
||||
this.dataSource.filterDataSource(this.filterList);
|
||||
}
|
||||
|
||||
toggle(node: FlatNode): void {
|
||||
if (this.treeControl.isExpanded(node)) {
|
||||
this.treeControl.collapse(node);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
:host {
|
||||
width: 325px;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
height: calc(100% - 24px);
|
||||
}
|
||||
:host mat-tree {
|
||||
display: table;
|
||||
}
|
||||
.name {
|
||||
margin-left: -9px;
|
||||
}
|
||||
.non-expandable {
|
||||
margin-left: 13px;
|
||||
}
|
||||
.property-list {
|
||||
margin: 5px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.property-list mat-tree-node .name {
|
||||
color: #b82519;
|
||||
}
|
||||
.property-list mat-tree-node.disabled .name,
|
||||
.disabled {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-family: monospace;
|
||||
font-size: 7px;
|
||||
color: #6e6e6e;
|
||||
}
|
||||
|
||||
:host /deep/ .mat-tree-node .mat-icon {
|
||||
font-size: 12px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
:host /deep/ .mat-tree-node {
|
||||
min-height: 20px !important;
|
||||
cursor: default;
|
||||
font-family: Menlo, monospace;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
:host /deep/ .mat-tree-node,
|
||||
.mat-nested-tree-node {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<mat-tree *ngIf="_treeControl" class="property-list" [dataSource]="_dataSource" [treeControl]="_treeControl">
|
||||
<mat-tree-node matTreeNodePaddingIndent="14" *matTreeNodeDef="let node" matTreeNodePadding>
|
||||
<span class="name non-expandable"> {{ node.prop.name }} </span>:
|
||||
<ng-container *ngIf="!node.prop.descriptor.editable; else editable">
|
||||
<span class="value">
|
||||
{{ node.prop.descriptor.preview }}
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-template #editable>
|
||||
<ng-property-editor
|
||||
[key]="node.prop.name"
|
||||
[initialValue]="node.prop.descriptor.value || node.prop.descriptor.preview"
|
||||
(updateValue)="handleUpdate(node, $event)"
|
||||
>
|
||||
</ng-property-editor>
|
||||
</ng-template>
|
||||
</mat-tree-node>
|
||||
|
||||
<mat-tree-node matTreeNodePaddingIndent="14" *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
|
||||
LOL
|
||||
<div (click)="toggle(node)">
|
||||
<span class="arrow">
|
||||
{{ _treeControl.isExpanded(node) ? '▼' : '►' }}
|
||||
</span>
|
||||
|
||||
<span class="name"> {{ node.prop.name }} </span>:
|
||||
<span class="value">
|
||||
{{ _treeControl.isExpanded(node) ? '' : node.prop.descriptor.preview }}
|
||||
</span>
|
||||
</div>
|
||||
</mat-tree-node>
|
||||
</mat-tree>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { PropertyDataSource } from '../../../../../property-resolver/property-data-source';
|
||||
import { FlatNode } from '../../../../../property-resolver/element-property-resolver';
|
||||
import { UpdateEvent } from '../property-view-body.component';
|
||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-property-view-tree',
|
||||
templateUrl: './property-view-tree.component.html',
|
||||
styleUrls: ['./property-view-tree.component.css'],
|
||||
})
|
||||
export class PropertyViewTreeComponent {
|
||||
@Input() set dataSource(dataSource: PropertyDataSource) {
|
||||
this._dataSource = dataSource;
|
||||
this._treeControl = dataSource.getTreeControl();
|
||||
}
|
||||
@Output() updateValue = new EventEmitter<UpdateEvent>();
|
||||
|
||||
_dataSource: PropertyDataSource;
|
||||
_treeControl: FlatTreeControl<FlatNode>;
|
||||
|
||||
hasChild = (_: number, node: FlatNode): boolean => {
|
||||
debugger;
|
||||
return node.expandable;
|
||||
};
|
||||
|
||||
toggle(node: FlatNode): void {
|
||||
if (this._treeControl.isExpanded(node)) {
|
||||
this._treeControl.collapse(node);
|
||||
return;
|
||||
}
|
||||
this.expand(node);
|
||||
}
|
||||
|
||||
expand(node: FlatNode): void {
|
||||
const { prop } = node;
|
||||
if (!prop.descriptor.expandable) {
|
||||
return;
|
||||
}
|
||||
this._treeControl.expand(node);
|
||||
}
|
||||
|
||||
handleUpdate(node: FlatNode, updatedValue: any): void {
|
||||
this.updateValue.emit({
|
||||
node,
|
||||
updatedValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.explorer-panel header {
|
||||
header {
|
||||
background-color: #eee;
|
||||
padding-left: 9px;
|
||||
border-top: 1px solid #ccc;
|
||||
|
|
@ -22,3 +22,7 @@
|
|||
line-height: 15px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
:host /deep/ .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
|
||||
line-height: 25px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
<header>
|
||||
Properties of {{ directive }}
|
||||
<span> Properties of {{ directive }} </span>
|
||||
<mat-button-toggle-group [multiple]="true" (change)="filterProperties($event)">
|
||||
<mat-button-toggle [value]="cmpFilterOptions.INPUTS" [checked]="true">
|
||||
Inputs
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle [value]="cmpFilterOptions.OUTPUTS" [checked]="true">
|
||||
Outputs
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle [value]="cmpFilterOptions.STATE" [checked]="true">
|
||||
State
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
<button mat-icon-button (click)="copyPropData.emit(directive)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { MatButtonToggleChange } from '@angular/material/button-toggle';
|
||||
import { PropertyViewFilterOptions } from '../../../../property-resolver/directive-property-resolver';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-property-view-header',
|
||||
|
|
@ -7,5 +9,12 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|||
})
|
||||
export class PropertyViewHeaderComponent {
|
||||
@Input() directive: string;
|
||||
@Output() filter = new EventEmitter<PropertyViewFilterOptions[]>();
|
||||
@Output() copyPropData = new EventEmitter<string>();
|
||||
|
||||
cmpFilterOptions = PropertyViewFilterOptions;
|
||||
|
||||
filterProperties(event: MatButtonToggleChange): void {
|
||||
this.filter.emit(event.value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
<ng-property-view-header [directive]="directive" (copyPropData)="copyPropData.emit($event)"></ng-property-view-header>
|
||||
<ng-property-view-header
|
||||
[directive]="directive"
|
||||
(copyPropData)="copyPropData.emit($event)"
|
||||
(filter)="filter($event)"
|
||||
></ng-property-view-header>
|
||||
<ng-property-view-body
|
||||
[controller]="controller"
|
||||
[dataSource]="dataSource"
|
||||
[filterList]="allowedList"
|
||||
[treeControl]="treeControl"
|
||||
></ng-property-view-body>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { DirectivePropertyResolver } from '../../../property-resolver/directive-property-resolver';
|
||||
import { FlatNode, PropertyDataSource } from '../../../property-resolver/property-data-source';
|
||||
import {
|
||||
DirectivePropertyResolver,
|
||||
PropertyViewFilterOptions,
|
||||
} from '../../../property-resolver/directive-property-resolver';
|
||||
import { PropertyDataSource } from '../../../property-resolver/property-data-source';
|
||||
import { TreeControl } from '@angular/cdk/tree';
|
||||
import { ElementPropertyResolver } from '../../../property-resolver/element-property-resolver';
|
||||
import { ElementPropertyResolver, FlatNode } from '../../../property-resolver/element-property-resolver';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-property-view',
|
||||
|
|
@ -13,17 +16,56 @@ export class PropertyViewComponent {
|
|||
@Input() directive: string;
|
||||
@Output() copyPropData = new EventEmitter<string>();
|
||||
|
||||
currentFilter: PropertyViewFilterOptions[] = [
|
||||
PropertyViewFilterOptions.INPUTS,
|
||||
PropertyViewFilterOptions.OUTPUTS,
|
||||
PropertyViewFilterOptions.STATE,
|
||||
];
|
||||
allowedList: string[] | null = null;
|
||||
|
||||
constructor(private _nestedProps: ElementPropertyResolver) {}
|
||||
|
||||
get controller(): DirectivePropertyResolver {
|
||||
get controller(): DirectivePropertyResolver | undefined {
|
||||
return this._nestedProps.getDirectiveController(this.directive);
|
||||
}
|
||||
|
||||
get dataSource(): PropertyDataSource {
|
||||
get dataSource(): PropertyDataSource | void {
|
||||
if (!this.controller) {
|
||||
return;
|
||||
}
|
||||
return this.controller.getDirectiveControls().dataSource;
|
||||
}
|
||||
|
||||
get treeControl(): TreeControl<FlatNode> {
|
||||
get treeControl(): TreeControl<FlatNode> | void {
|
||||
if (!this.controller) {
|
||||
return;
|
||||
}
|
||||
return this.controller.getDirectiveControls().treeControl;
|
||||
}
|
||||
|
||||
filter(evt: PropertyViewFilterOptions[]): void {
|
||||
this.currentFilter = evt;
|
||||
this.computeAllowedList();
|
||||
}
|
||||
|
||||
computeAllowedList(): void {
|
||||
if (!this.controller || !this.controller.directiveProperties) {
|
||||
return;
|
||||
}
|
||||
const inputList = this.controller.directiveInputs;
|
||||
const outputList = this.controller.directiveOutputs;
|
||||
const stateList = Object.keys(this.controller.directiveProperties).filter(
|
||||
prop => !inputList.includes(prop) && !outputList.includes(prop)
|
||||
);
|
||||
const propList = {
|
||||
[PropertyViewFilterOptions.INPUTS]: inputList,
|
||||
[PropertyViewFilterOptions.OUTPUTS]: outputList,
|
||||
[PropertyViewFilterOptions.STATE]: stateList,
|
||||
};
|
||||
|
||||
this.allowedList = [].concat.apply(
|
||||
[],
|
||||
this.currentFilter.map(filterOption => propList[filterOption])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,18 @@ import { MatTreeModule } from '@angular/material/tree';
|
|||
import { PropertyEditorModule } from './property-view-body/property-editor/property-editor.module';
|
||||
import { PropertyViewHeaderComponent } from './property-view-header/property-view-header.component';
|
||||
import { PropertyViewComponent } from './property-view.component';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { PropertyViewTreeComponent } from './property-view-body/property-view-tree/property-view-tree.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PropertyViewBodyComponent, PropertyViewHeaderComponent, PropertyViewComponent],
|
||||
imports: [MatTreeModule, CommonModule, PropertyEditorModule],
|
||||
declarations: [
|
||||
PropertyViewBodyComponent,
|
||||
PropertyViewHeaderComponent,
|
||||
PropertyViewComponent,
|
||||
PropertyViewTreeComponent,
|
||||
],
|
||||
imports: [MatTreeModule, CommonModule, PropertyEditorModule, MatButtonModule, MatButtonToggleModule],
|
||||
exports: [PropertyViewBodyComponent, PropertyViewHeaderComponent, PropertyViewComponent],
|
||||
})
|
||||
export class PropertyViewModule {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { IndexedNode } from '../directive-forest/index-forest';
|
||||
import { Descriptor, DirectivesProperties, Events, MessageBus } from 'protocol';
|
||||
import { PropertyTabBodyComponent } from './property-tab-body/property-tab-body.component';
|
||||
|
||||
@Component({
|
||||
|
|
|
|||
Loading…
Reference in a new issue