Skip to content
This repository has been archived by the owner on May 25, 2021. It is now read-only.

Commit

Permalink
Display real type information in the State view instead of always "Ob…
Browse files Browse the repository at this point in the history
…ject" (#776)

* Display real type information in the State view instead of always "Object"

* Remove MD5 reference (was used for path hashing)
Object metadata cannot be based on paths, has to be based on reference, because the same object can appear at multiple paths
  - Luckily our serializer retains object references across serializations, so this can work for us

* Separate object metadata and component metadata into separate maps. We
cannot store component metadata keyed off values, because oftentimes
those values are scalars that are not unique keys that can be used in a
map. So now we store the component instance as the map key and then
retain a list of all properties and their metadata.
  • Loading branch information
clbond authored Nov 11, 2016
1 parent a3cbec6 commit f8a5cf4
Show file tree
Hide file tree
Showing 13 changed files with 131 additions and 73 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@
"es6-promise": "^3.1.2",
"es6-shim": "^0.35.0",
"file-loader": "^0.8.5",
"md5": "^2.2.1",
"msgpack-lite": "^0.1.20",
"object-assign": "4.0.1",
"postcss-cssnext": "^2.5.2",
Expand Down
1 change: 1 addition & 0 deletions src/frontend/components/component-info/component-info.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ <h4 class="primary-color col pt1 pb2 m0">
<template [ngSwitchCase]="ComponentLoadState.Received">
<bt-render-state
[id]="node.id"
[componentMetadata]="componentMetadata"
[metadata]="metadata"
[level]="0"
[path]="path"
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/components/component-info/component-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {ComponentLoadState} from '../../state';
import {UserActions} from '../../actions/user-actions/user-actions';

import {
ComponentMetadata,
Metadata,
MutableTree,
Node,
Expand All @@ -27,6 +28,7 @@ export class ComponentInfo {
@Input() private tree: MutableTree;
@Input() private state;
@Input() private metadata: Metadata;
@Input() private componentMetadata: ComponentMetadata;
@Input() private loadingState: ComponentLoadState;

@Output() private selectNode = new EventEmitter<Node>();
Expand Down
1 change: 1 addition & 0 deletions src/frontend/components/info-panel/info-panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
[tree]="tree"
[loadingState]="loadingState"
[state]="state"
[componentMetadata]="componentMetadata"
[metadata]="metadata"
[hidden]="selectedTab !== StateTab.Properties"
[ngClass]="{flex: selectedTab === StateTab.Properties}"
Expand Down
7 changes: 7 additions & 0 deletions src/frontend/components/info-panel/info-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {ComponentLoadState} from '../../state';
import {StateTab} from '../../state';
import {UserActions} from '../../actions/user-actions/user-actions';
import {
ComponentMetadata,
InstanceWithMetadata,
Metadata,
Node,
Expand Down Expand Up @@ -57,6 +58,12 @@ export class InfoPanel {
: new Map<string, [ObjectType, any]>();
}

private get componentMetadata(): ComponentMetadata {
return this.instanceValue
? this.instanceValue.componentMetadata
: new Map<string, [string, ObjectType, any]>();
}

private onSelectedTabChanged(tab: StateTab) {
this.selectedTab = tab;
}
Expand Down
13 changes: 7 additions & 6 deletions src/frontend/components/render-state/render-state.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@
transparent: !expandable(k)
}"></div>
<span class="primary-color">
<span *ngIf="isObjectType(k, ObjectType.Input)" class="decorator">
<span *ngIf="isComponentObjectType(k, ObjectType.Input)" class="decorator">
@Input(<span class="info-value" *ngIf="getAlias(k)">'{{getAlias(k)}}'</span>)
</span>
<span *ngIf="isObjectType(k, ObjectType.Output)" class="decorator">
<span *ngIf="isComponentObjectType(k, ObjectType.Output)" class="decorator">
@Output(<span class="info-value" *ngIf="getAlias(k)">'{{getAlias(k)}}'</span>)
</span>
<span *ngIf="isObjectType(k, ObjectType.ViewChild)" class="decorator">
<span *ngIf="isComponentObjectType(k, ObjectType.ViewChild)" class="decorator">
@ViewChild({{getSelector(k)}})
</span>
<span *ngIf="isObjectType(k, ObjectType.ViewChildren)" class="decorator">
<span *ngIf="isComponentObjectType(k, ObjectType.ViewChildren)" class="decorator">
@ViewChildren({{getSelector(k)}})
</span>
<span *ngIf="isObjectType(k, ObjectType.ContentChild)" class="decorator">
<span *ngIf="isComponentObjectType(k, ObjectType.ContentChild)" class="decorator">
@ContentChild({{getSelector(k)}})
</span>
<span *ngIf="isObjectType(k, ObjectType.ContentChildren)" class="decorator">
<span *ngIf="isComponentObjectType(k, ObjectType.ContentChildren)" class="decorator">
@ContentChildren({{getSelector(k)}})
</span>
</span>
Expand Down Expand Up @@ -61,6 +61,7 @@
[id]="id"
[level]="level + 1"
[path]="path.concat([k])"
[componentMetadata]="componentMetadata"
[metadata]="metadata"
[state]="state[k]">
</bt-render-state>
Expand Down
42 changes: 27 additions & 15 deletions src/frontend/components/render-state/render-state.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import md5 = require('md5');

import {
ChangeDetectionStrategy,
Component,
Expand All @@ -17,12 +15,15 @@ import {

import {getPropertyPath} from '../../../backend/utils';

import {functionName} from '../../../utils';

import {
ComponentPropertyState,
ExpandState,
} from '../../state';

import {
ComponentMetadata,
Metadata,
Path,
ObjectType,
Expand All @@ -43,6 +44,7 @@ export enum EmitState {
})
export class RenderState {
@Input() id: string;
@Input() componentMetadata: ComponentMetadata;
@Input() metadata: Metadata;
@Input() level: number;
@Input() path: Path;
Expand Down Expand Up @@ -88,13 +90,17 @@ export class RenderState {
return `Array[${object.length}]`;
}
else if (object != null) {
if (Object.keys(object).length === 0) {
return '{}';
const constructor = functionName(object.constructor) || typeof object;

if (/object/i.test(constructor)) {
if (Object.keys(object).length === 0) {
return '{}'; // special case to denote an empty object
}
}
return 'Object';
}

if (object === null) {
return constructor;
}
else if (object === null) {
return 'null';
}
else if (object === undefined) {
Expand All @@ -118,29 +124,35 @@ export class RenderState {
}
}

private getMetadata(key: string): [ObjectType, any] {
return this.metadata.get(md5(serializePath(this.path.concat([key]))));
private getComponentMetadata(key: string): [ObjectType, any] {
const properties = this.componentMetadata.get(this.state);
if (properties) {
const matchingProperty = properties.find(p => p[0] === key);
if (matchingProperty) {
return [matchingProperty[1], matchingProperty[2]];
}
}
return null;
}

private isEmittable(key: string): boolean {
const metadata = this.getMetadata(key);
const metadata = this.metadata.get(this.state[key]);
if (metadata) {
return (metadata[0] & ObjectType.EventEmitter) !== 0
|| (metadata[0] & ObjectType.Subject) !== 0;
}
return false;
}

private isObjectType(key: string, type: ObjectType): boolean {
const metadata = this.getMetadata(key);
private isComponentObjectType(key: string, type: ObjectType): boolean {
const metadata = this.getComponentMetadata(key);
if (metadata) {
return (metadata[0] & type) !== 0;
}
return false;
}

private getAlias(key: string): string {
const metadata = this.getMetadata(key);
const metadata = this.getComponentMetadata(key);
if (metadata) {
const additionalProperties = metadata[1];
if (additionalProperties) {
Expand All @@ -150,7 +162,7 @@ export class RenderState {
}

private getSelector(key: string): string {
const metadata = this.getMetadata(key);
const metadata = this.getComponentMetadata(key);
if (metadata) {
const additionalProperties = metadata[1];
if (additionalProperties) {
Expand Down
10 changes: 8 additions & 2 deletions src/frontend/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,13 +257,19 @@ class App {
const m = MessageFactory.selectComponent(node, node.isComponent);

if (node.isComponent) {

const promise = this.directConnection.handleImmediate(m)
.then(response => {
if (typeof beforeLoad === 'function') {
beforeLoad();
}
return response;

const {instance, metadata, componentMetadata} = response;

return {
instance,
metadata: new Map(metadata),
componentMetadata: new Map(componentMetadata),
};
});

this.componentState.wait(node, promise);
Expand Down
66 changes: 32 additions & 34 deletions src/tree/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import md5 = require('md5');

import {
AsyncSubject,
BehaviorSubject,
Expand All @@ -9,12 +7,6 @@ import {
Subject,
} from 'rxjs';

import {
Path,
serializePath,
deserializePath,
} from './path';

import {Node} from './node';

import {
Expand Down Expand Up @@ -42,53 +34,56 @@ export enum ObjectType {
ContentChildren = 0x100,
}

export type Metadata = Map<string, [ObjectType, any]>;
export type Metadata = Map<any, [ObjectType, any]>;

export type ComponentMetadata = Map<any, [string, ObjectType, any]>;

export interface InstanceWithMetadata {
instance: any;
metadata: Metadata;
componentMetadata: ComponentMetadata;
}

export const instanceWithMetadata = (node: Node, instance): InstanceWithMetadata => {
// It is imperative that the metadata and the instance value itself travel together
// through the serializer, otherwise we are going to have to serialize the entire
// object structure twice, once for the instance and once for the metadata. But if
// we put them together as part of the same object, the serializer will be smart
// enough not to duplicate objects. If someone breaks apart the instance and the
// metadata into two objects, a lot of code that depends on reference equality is
// going to get broken! So do not change this!
export const instanceWithMetadata = (node: Node, instance) => {
if (node == null || instance == null) {
return null;
}

const metadata = new Map<string, [ObjectType, any]>();
const objectMetadata = new Map<any, [ObjectType, any]>();

const nodePath = deserializePath(node.id);
const components = new Map<any, [[string, ObjectType, any]]>();

const serialize = (path: Path): string => md5(serializePath(path));

recurse(nodePath, instance,
(path: Path, obj) => {
let type = objectType(obj);

const update = (p: Path, flag: ObjectType, additionalProps) => {
const serializedPath = serialize(p);

const existing = metadata.get(serializedPath);
recurse(instance,
obj => {
const update = (key: string, flag: ObjectType, additionalProps) => {
const existing = components.get(obj);
if (existing) {
existing[0] |= flag;
Object.assign(existing, additionalProps);
existing.push([key, flag, additionalProps]);
}
else {
metadata.set(serializedPath, [flag, additionalProps]);
components.set(obj, [[key, flag, additionalProps]]);
}
};

const component = componentMetadata(obj);
if (component) {
for (const input of componentInputs(component, obj)) {
update(path.concat([input.propertyKey]), ObjectType.Input, {alias: input.bindingPropertyName});
update(input.propertyKey, ObjectType.Input, {alias: input.bindingPropertyName});
}
for (const output of componentOutputs(component, obj)) {
update(path.concat([output.propertyKey]), ObjectType.Output, {alias: output.bindingPropertyName});
update(output.propertyKey, ObjectType.Output, {alias: output.bindingPropertyName});
}

const addQuery = (decoratorType: string, objectType: ObjectType) => {
for (const vc of componentQueryChildren(decoratorType, component, obj)) {
update(path.concat([vc.propertyKey]), objectType, {selector: vc.selector});
update(vc.propertyKey, objectType, {selector: vc.selector});
}
};

Expand All @@ -98,20 +93,23 @@ export const instanceWithMetadata = (node: Node, instance): InstanceWithMetadata
addQuery('@ContentChildren', ObjectType.ContentChildren);
}

const type = objectType(obj);
if (type !== 0) {
const serializedPath = serialize(path);

const existing = metadata.get(serializedPath);
const existing = objectMetadata.get(obj);
if (existing) {
metadata.set(serializedPath, [existing[0] | type, existing[1]]);
objectMetadata.set(obj, [existing[0] | type, existing[1]]);
}
else {
metadata.set(serializedPath, [type, null]);
objectMetadata.set(obj, [type, null]);
}
}
});

return {instance, metadata};
return {
instance,
metadata: Array.from(<any> objectMetadata),
componentMetadata: Array.from(<any> components),
};
};

const objectType = (object): ObjectType => {
Expand Down
25 changes: 16 additions & 9 deletions src/utils/circular-recurse.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import {Path} from '../tree';

export type Apply = (path: Path, value) => void;
import {isScalar} from './scalar';

export const recurse = (initialPath: Path, object, exec: Apply) => {
export type Apply = (value) => void;

// Recursive traversal of an object tree, but will not traverse circular references or DOM elements
export const recurse = (object, apply: Apply) => {
const visited = new Set();

const apply = (path: Path, value, fn: Apply) => {
if (value == null || visited.has(value)) {
const visit = value => {
if (value == null || isScalar(value) || /Element/.test(Object.prototype.toString.call(value))) {
return;
}

if (visited.has(value)) { // circular loop
return;
}

visited.add(value);

fn(path, value);
apply(value);

if (Array.isArray(value) || value instanceof Set) {
(<any>value).forEach((v, k) => apply(path.concat([k]), v, fn));
(<any>value).forEach((v, k) => visit(v));
}
else if (value instanceof Map) {
value.forEach((v, k) => apply(path.concat([k]), v, fn));
value.forEach((v, k) => visit(v));
}
else {
Object.keys(value).forEach(k => apply(path.concat([k]), value[k], fn));
Object.keys(value).forEach(k => visit(value[k]));
}
};

apply(initialPath, object, exec);
visit(object);
};
Loading

0 comments on commit f8a5cf4

Please sign in to comment.