mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(devtools): Add support for signals. (#53269)
The devtools now support signals. Writable signals of primitives are editable. Object Signal and other non-writable signals (like computed) are not editable. Co-authored-by: Tomasz Ducin <tomasz.ducin@gmail.com> PR Close #53269
This commit is contained in:
committed by
Dylan Hunn
parent
025f7af694
commit
9ff4893bc0
@@ -2,3 +2,10 @@
|
||||
<app-zippy [title]="getTitle()">This is my content</app-zippy>
|
||||
<app-heavy></app-heavy>
|
||||
<div #elementReference>HTMLElement</div>
|
||||
|
||||
<hr/>
|
||||
<h2>Signals</h2>
|
||||
<div>Object Signal: {{objectSignal()|json}}</div>
|
||||
<div>Object Computed: {{objectComputed()|json}}</div>
|
||||
<div>Editable Signal: {{primitiveSignal()}}</div>
|
||||
<div>Primitive computed: {{primitiveComputed()}}</div>
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, EventEmitter, inject, Injector, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core';
|
||||
import {JsonPipe} from '@angular/common';
|
||||
import {Component, computed, CUSTOM_ELEMENTS_SCHEMA, ElementRef, EventEmitter, inject, Injector, Input, Output, signal, ViewChild, ViewEncapsulation} from '@angular/core';
|
||||
import {createCustomElement} from '@angular/elements';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
import {initializeMessageBus} from 'ng-devtools-backend';
|
||||
@@ -22,7 +23,7 @@ import {ZippyComponent} from './zippy.component';
|
||||
styleUrls: ['./demo-app.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: true,
|
||||
imports: [HeavyComponent, RouterOutlet],
|
||||
imports: [HeavyComponent, RouterOutlet, JsonPipe],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class DemoAppComponent {
|
||||
@@ -35,6 +36,15 @@ export class DemoAppComponent {
|
||||
@Output() outputOne = new EventEmitter();
|
||||
@Output('output_two') outputTwo = new EventEmitter();
|
||||
|
||||
primitiveSignal = signal(123);
|
||||
primitiveComputed = computed(() => this.primitiveSignal() ** 2);
|
||||
objectSignal = signal({name: 'John', age: 40});
|
||||
objectComputed = computed(() => {
|
||||
const original = this.objectSignal();
|
||||
return {...original, age: original.age + 1};
|
||||
});
|
||||
test = [signal(3)];
|
||||
|
||||
constructor() {
|
||||
const el = createCustomElement(ZippyComponent, {injector: inject(Injector)});
|
||||
customElements.define('app-zippy', el as any);
|
||||
|
||||
@@ -124,6 +124,7 @@ ts_library(
|
||||
srcs = ["component-tree.ts"],
|
||||
deps = [
|
||||
":interfaces",
|
||||
":utils",
|
||||
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest",
|
||||
"//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api",
|
||||
"//devtools/projects/ng-devtools-backend/src/lib/state-serializer",
|
||||
|
||||
@@ -19,7 +19,7 @@ import {ComponentTreeNode} from './interfaces';
|
||||
import {ngDebugDependencyInjectionApiIsSupported} from './ng-debug-api/ng-debug-api';
|
||||
import {setConsoleReference} from './set-console-reference';
|
||||
import {serializeDirectiveState} from './state-serializer/state-serializer';
|
||||
import {runOutsideAngular} from './utils';
|
||||
import {hasDiDebugAPIs, runOutsideAngular} from './utils';
|
||||
|
||||
export const subscribeToClientEvents = (messageBus: MessageBus<Events>): void => {
|
||||
messageBus.on('shutdown', shutdownCallback(messageBus));
|
||||
|
||||
@@ -23,6 +23,7 @@ enum ChangeDetectionStrategy {
|
||||
import {ComponentTreeNode, DirectiveInstanceType, ComponentInstanceType} from './interfaces';
|
||||
|
||||
import type {ClassProvider, ExistingProvider, FactoryProvider, InjectOptions, InjectionToken, Injector, Type, ValueProvider, ɵComponentDebugMetadata as ComponentDebugMetadata, ɵProviderRecord as ProviderRecord} from '@angular/core';
|
||||
import {isSignal} from './utils';
|
||||
|
||||
export const injectorToId = new WeakMap<Injector|HTMLElement, string>();
|
||||
export const nodeInjectorToResolutionPath = new WeakMap<HTMLElement, SerializedInjector[]>();
|
||||
@@ -537,10 +538,19 @@ const mutateComponentOrDirective = (updatedStateData: UpdatedStateData, compOrDi
|
||||
parentObjectOfValueToUpdate = parentObjectOfValueToUpdate[key];
|
||||
});
|
||||
|
||||
if (isSignal(parentObjectOfValueToUpdate)) {
|
||||
// we don't support updating nested objects in signals yet
|
||||
return;
|
||||
}
|
||||
|
||||
// When we try to set a property which only has a getter
|
||||
// the line below could throw an error.
|
||||
try {
|
||||
parentObjectOfValueToUpdate[valueKey] = updatedStateData.newValue;
|
||||
if (isSignal(parentObjectOfValueToUpdate[valueKey])) {
|
||||
parentObjectOfValueToUpdate[valueKey].set(updatedStateData.newValue);
|
||||
} else {
|
||||
parentObjectOfValueToUpdate[valueKey] = updatedStateData.newValue;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {unwrapSignal} from '../utils';
|
||||
|
||||
// Intentionally do not include all the prototype (Except for getters and setters)
|
||||
// because it contains inherited methods (hasOwnProperty, etc.). Also ignore symbols
|
||||
// because it is tricky to pass a path to a symbol.
|
||||
@@ -16,6 +18,7 @@ export function getKeys(obj: {}): string[] {
|
||||
if (!obj) {
|
||||
return [];
|
||||
}
|
||||
obj = unwrapSignal(obj);
|
||||
const properties = Object.getOwnPropertyNames(obj);
|
||||
|
||||
const prototypeMembers = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(obj));
|
||||
@@ -39,6 +42,6 @@ export function getKeys(obj: {}): string[] {
|
||||
* @param propName The string representation of the target property name
|
||||
* @returns The Descriptor object of the property
|
||||
*/
|
||||
export const getDescriptor = (instance: any, propName: string) =>
|
||||
export const getDescriptor = (instance: any, propName: string): PropertyDescriptor|undefined =>
|
||||
Object.getOwnPropertyDescriptor(instance, propName) ||
|
||||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), propName);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import {PropType} from 'protocol';
|
||||
|
||||
import {isSignal} from '../utils';
|
||||
|
||||
const commonTypes = {
|
||||
boolean: PropType.Boolean,
|
||||
bigint: PropType.BigInt,
|
||||
@@ -24,6 +26,10 @@ const commonTypes = {
|
||||
* @see `devtools/projects/protocol`
|
||||
*/
|
||||
export const getPropType = (prop: unknown): PropType => {
|
||||
if (isSignal(prop)) {
|
||||
prop = prop();
|
||||
}
|
||||
|
||||
if (prop === undefined) {
|
||||
return PropType.Undefined;
|
||||
}
|
||||
|
||||
+110
-98
@@ -6,7 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Descriptor, NestedProp, PropType} from 'protocol';
|
||||
import {ContainerType, Descriptor, NestedProp, PropType} from 'protocol';
|
||||
|
||||
import {isSignal, unwrapSignal} from '../utils';
|
||||
|
||||
import {getDescriptor, getKeys} from './object-utils';
|
||||
|
||||
@@ -18,11 +20,13 @@ type NestedType = PropType.Array|PropType.Object;
|
||||
export interface CompositeType {
|
||||
type: Extract<PropType, NestedType>;
|
||||
prop: any;
|
||||
containerType: ContainerType;
|
||||
}
|
||||
|
||||
export interface TerminalType {
|
||||
type: Exclude<PropType, NestedType>;
|
||||
prop: any;
|
||||
containerType: ContainerType;
|
||||
}
|
||||
|
||||
export type PropertyData = TerminalType|CompositeType;
|
||||
@@ -36,23 +40,15 @@ interface LevelOptions {
|
||||
level?: number;
|
||||
}
|
||||
|
||||
const serializable: {[key in PropType]: boolean} = {
|
||||
[PropType.Boolean]: true,
|
||||
[PropType.String]: true,
|
||||
[PropType.Null]: true,
|
||||
[PropType.Number]: true,
|
||||
[PropType.Object]: true,
|
||||
[PropType.Undefined]: true,
|
||||
[PropType.Unknown]: true,
|
||||
[PropType.Array]: false,
|
||||
[PropType.Set]: false,
|
||||
[PropType.Map]: false,
|
||||
[PropType.BigInt]: false,
|
||||
[PropType.Function]: false,
|
||||
[PropType.HTMLNode]: false,
|
||||
[PropType.Symbol]: false,
|
||||
[PropType.Date]: false,
|
||||
};
|
||||
const serializable: Set<PropType> = new Set([
|
||||
PropType.Boolean,
|
||||
PropType.String,
|
||||
PropType.Null,
|
||||
PropType.Number,
|
||||
PropType.Object,
|
||||
PropType.Undefined,
|
||||
PropType.Unknown,
|
||||
]);
|
||||
|
||||
const typeToDescriptorPreview: Formatter<string> = {
|
||||
[PropType.Array]: (prop: Array<unknown>) => `Array(${prop.length})`,
|
||||
@@ -78,6 +74,10 @@ const typeToDescriptorPreview: Formatter<string> = {
|
||||
};
|
||||
|
||||
type Key = string|number;
|
||||
type NestedSerializerFn =
|
||||
(instance: any, propName: string|number, nodes: NestedProp[], isReadonly: boolean,
|
||||
currentLevel: number, level?: number) => Descriptor;
|
||||
|
||||
const ignoreList: Set<Key> = new Set([METADATA_PROPERTY_NAME, '__ngSimpleChanges__']);
|
||||
|
||||
const shallowPropTypeToTreeMetaData:
|
||||
@@ -137,8 +137,12 @@ const shallowPropTypeToTreeMetaData:
|
||||
};
|
||||
|
||||
const isEditable =
|
||||
(descriptor: any, propName: string|number, propData: TerminalType,
|
||||
(descriptor: PropertyDescriptor|undefined, propName: string|number, propData: TerminalType,
|
||||
isGetterOrSetter: boolean) => {
|
||||
if (propData.containerType === 'ReadonlySignal') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof propName === 'symbol') {
|
||||
return false;
|
||||
}
|
||||
@@ -158,130 +162,138 @@ const isGetterOrSetter = (descriptor: any): boolean =>
|
||||
(descriptor?.set || descriptor?.get) && !('value' in descriptor);
|
||||
|
||||
const getPreview = (propData: TerminalType|CompositeType, isGetterOrSetter: boolean) => {
|
||||
if (propData.containerType === 'ReadonlySignal') {
|
||||
return `Readonly Signal(${typeToDescriptorPreview[propData.type](propData.prop())})`;
|
||||
} else if (propData.containerType === 'WritableSignal') {
|
||||
return `Signal(${typeToDescriptorPreview[propData.type](propData.prop())})`;
|
||||
}
|
||||
return !isGetterOrSetter ? typeToDescriptorPreview[propData.type](propData.prop) :
|
||||
typeToDescriptorPreview[PropType.Function]({name: ''});
|
||||
};
|
||||
|
||||
export const createShallowSerializedDescriptor =
|
||||
(instance: any, propName: string|number, propData: TerminalType): Descriptor => {
|
||||
const {type} = propData;
|
||||
export function createShallowSerializedDescriptor(
|
||||
instance: any, propName: string|number, propData: TerminalType,
|
||||
isReadonly: boolean): Descriptor {
|
||||
const {type, containerType} = propData;
|
||||
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
|
||||
const shallowSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
expandable: shallowPropTypeToTreeMetaData[type].expandable,
|
||||
editable: isEditable(descriptor, propName, propData, getterOrSetter),
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
};
|
||||
const shallowSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
expandable: shallowPropTypeToTreeMetaData[type].expandable,
|
||||
editable: isEditable(descriptor, propName, propData, getterOrSetter) && !isReadonly,
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
containerType,
|
||||
};
|
||||
|
||||
if (propData.prop !== undefined && serializable[type]) {
|
||||
shallowSerializedDescriptor.value = propData.prop;
|
||||
}
|
||||
if (propData.prop !== undefined && serializable.has(type)) {
|
||||
shallowSerializedDescriptor.value = unwrapSignal(propData.prop);
|
||||
}
|
||||
|
||||
return shallowSerializedDescriptor;
|
||||
};
|
||||
return shallowSerializedDescriptor;
|
||||
}
|
||||
|
||||
export const createLevelSerializedDescriptor =
|
||||
(instance: {}, propName: string|number, propData: CompositeType, levelOptions: LevelOptions,
|
||||
continuation: (instance: any, propName: string|number, level?: number, max?: number) => void):
|
||||
Descriptor => {
|
||||
const {type, prop} = propData;
|
||||
export function createLevelSerializedDescriptor(
|
||||
instance: {}, propName: string|number, propData: CompositeType, levelOptions: LevelOptions,
|
||||
continuation: (
|
||||
instance: any, propName: string|number, isReadonly: boolean, level?: number,
|
||||
max?: number) => Descriptor): Descriptor {
|
||||
const {type, prop, containerType} = propData;
|
||||
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
|
||||
const levelSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
editable: false,
|
||||
expandable: !getterOrSetter && getKeys(prop).length > 0,
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
};
|
||||
const levelSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
editable: false,
|
||||
expandable: !getterOrSetter && getKeys(prop).length > 0,
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
containerType,
|
||||
};
|
||||
|
||||
if (levelOptions.level !== undefined && levelOptions.currentLevel < levelOptions.level) {
|
||||
const value = getLevelDescriptorValue(propData, levelOptions, continuation);
|
||||
if (value !== undefined) {
|
||||
levelSerializedDescriptor.value = value;
|
||||
}
|
||||
}
|
||||
if (levelOptions.level !== undefined && levelOptions.currentLevel < levelOptions.level) {
|
||||
const value = getLevelDescriptorValue(propData, levelOptions, continuation);
|
||||
if (value !== undefined) {
|
||||
levelSerializedDescriptor.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
return levelSerializedDescriptor;
|
||||
};
|
||||
return levelSerializedDescriptor;
|
||||
}
|
||||
|
||||
export const createNestedSerializedDescriptor =
|
||||
(instance: {}, propName: string|number, propData: CompositeType, levelOptions: LevelOptions,
|
||||
nodes: NestedProp[],
|
||||
nestedSerializer: (
|
||||
instance: any, propName: string|number, nodes: NestedProp[], currentLevel: number,
|
||||
level?: number) => void): Descriptor => {
|
||||
const {type, prop} = propData;
|
||||
export function createNestedSerializedDescriptor(
|
||||
instance: {}, propName: string|number, propData: CompositeType, levelOptions: LevelOptions,
|
||||
nodes: NestedProp[], nestedSerializer: NestedSerializerFn): Descriptor {
|
||||
const {type, prop, containerType} = propData;
|
||||
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
|
||||
const nestedSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
editable: false,
|
||||
expandable: !getterOrSetter && getKeys(prop).length > 0,
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
};
|
||||
const nestedSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
editable: false,
|
||||
expandable: !getterOrSetter && getKeys(prop).length > 0,
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
containerType,
|
||||
};
|
||||
|
||||
if (nodes?.length) {
|
||||
const value = getNestedDescriptorValue(propData, levelOptions, nodes, nestedSerializer);
|
||||
if (value !== undefined) {
|
||||
nestedSerializedDescriptor.value = value;
|
||||
}
|
||||
}
|
||||
return nestedSerializedDescriptor;
|
||||
};
|
||||
if (nodes?.length) {
|
||||
const value = getNestedDescriptorValue(propData, levelOptions, nodes, nestedSerializer);
|
||||
if (value !== undefined) {
|
||||
nestedSerializedDescriptor.value = value;
|
||||
}
|
||||
}
|
||||
return nestedSerializedDescriptor;
|
||||
}
|
||||
|
||||
function getNestedDescriptorValue(
|
||||
propData: CompositeType, levelOptions: LevelOptions, nodes: NestedProp[],
|
||||
nestedSerializer: (
|
||||
instance: any, propName: string|number, nodes: NestedProp[], currentLevel: number,
|
||||
level?: number) => void) {
|
||||
nestedSerializer: NestedSerializerFn) {
|
||||
const {type, prop} = propData;
|
||||
const {currentLevel} = levelOptions;
|
||||
const value = unwrapSignal(prop);
|
||||
|
||||
switch (type) {
|
||||
case PropType.Array:
|
||||
return nodes.map(
|
||||
(nestedProp) =>
|
||||
nestedSerializer(prop, nestedProp.name, nestedProp.children, currentLevel + 1));
|
||||
(nestedProp) => nestedSerializer(
|
||||
value, nestedProp.name, nestedProp.children, false, currentLevel + 1));
|
||||
case PropType.Object:
|
||||
return nodes.reduce((accumulator, nestedProp) => {
|
||||
if (prop.hasOwnProperty(nestedProp.name) && !ignoreList.has(nestedProp.name)) {
|
||||
accumulator[nestedProp.name] =
|
||||
nestedSerializer(prop, nestedProp.name, nestedProp.children, currentLevel + 1);
|
||||
accumulator[nestedProp.name] = nestedSerializer(
|
||||
value, nestedProp.name, nestedProp.children, false, currentLevel + 1);
|
||||
}
|
||||
return accumulator;
|
||||
}, {} as Record<string, void>);
|
||||
}, {} as Record<string, Descriptor>);
|
||||
}
|
||||
}
|
||||
|
||||
function getLevelDescriptorValue(
|
||||
propData: CompositeType, levelOptions: LevelOptions,
|
||||
continuation: (instance: any, propName: string|number, level?: number, max?: number) => void) {
|
||||
continuation: (
|
||||
instance: any, propName: string|number, isReadonly: boolean, level?: number,
|
||||
max?: number) => Descriptor) {
|
||||
const {type, prop} = propData;
|
||||
const {currentLevel, level} = levelOptions;
|
||||
|
||||
const value = unwrapSignal(prop);
|
||||
const isReadonly = isSignal(prop);
|
||||
switch (type) {
|
||||
case PropType.Array:
|
||||
return prop.map((_: any, idx: number) => continuation(prop, idx, currentLevel + 1, level));
|
||||
return prop.map(
|
||||
(_: any, idx: number) => continuation(value, idx, isReadonly, currentLevel + 1, level));
|
||||
case PropType.Object:
|
||||
return getKeys(prop).reduce((accumulator, propName) => {
|
||||
if (!ignoreList.has(propName)) {
|
||||
accumulator[propName] = continuation(prop, propName, currentLevel + 1, level);
|
||||
accumulator[propName] =
|
||||
continuation(value, propName, isReadonly, currentLevel + 1, level);
|
||||
}
|
||||
return accumulator;
|
||||
}, {} as Record<string, void>);
|
||||
}, {} as Record<string, Descriptor>);
|
||||
}
|
||||
}
|
||||
|
||||
const truncate = (str: string, max = 20): string => {
|
||||
if (str.length > max) {
|
||||
return str.substring(0, max) + '...';
|
||||
}
|
||||
return str;
|
||||
};
|
||||
function truncate(str: string, max = 20): string {
|
||||
return str.length > max ? str.substring(0, max) + '...' : str;
|
||||
}
|
||||
|
||||
+28
-1
@@ -93,6 +93,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null
|
||||
},
|
||||
nested: {
|
||||
type: PropType.Object,
|
||||
@@ -105,8 +106,10 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
expandable: true,
|
||||
editable: false,
|
||||
preview: 'Array(4)',
|
||||
containerType: null
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -120,6 +123,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null
|
||||
},
|
||||
nested: {
|
||||
type: PropType.Object,
|
||||
@@ -151,21 +155,26 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
},
|
||||
],
|
||||
containerType: null
|
||||
},
|
||||
{
|
||||
type: PropType.Set,
|
||||
editable: false,
|
||||
expandable: false,
|
||||
preview: 'Set(2)',
|
||||
|
||||
containerType: null
|
||||
},
|
||||
],
|
||||
containerType: null
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -193,8 +202,10 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
expandable: true,
|
||||
preview: 'Array(3)',
|
||||
containerType: null
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -233,8 +244,10 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
expandable: true,
|
||||
preview: '{...}',
|
||||
containerType: null
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -265,8 +278,10 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
expandable: true,
|
||||
preview: '{...}',
|
||||
containerType: null
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -292,6 +307,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
preview: '(...)',
|
||||
value: 42,
|
||||
containerType: null
|
||||
},
|
||||
bar: {
|
||||
type: PropType.Number,
|
||||
@@ -299,6 +315,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
preview: '(...)',
|
||||
value: 42,
|
||||
containerType: null
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -375,6 +392,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null
|
||||
},
|
||||
bar: {
|
||||
type: PropType.Number,
|
||||
@@ -382,8 +400,10 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '2',
|
||||
value: 2,
|
||||
containerType: null
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
},
|
||||
foo: {
|
||||
type: PropType.Number,
|
||||
@@ -391,10 +411,13 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
preview: '(...)',
|
||||
value: 42,
|
||||
containerType: null
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
},
|
||||
},
|
||||
containerType: null
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -414,12 +437,14 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
preview: '(...)',
|
||||
containerType: null
|
||||
},
|
||||
bar: {
|
||||
type: PropType.Object,
|
||||
editable: false,
|
||||
expandable: false,
|
||||
preview: '(...)',
|
||||
containerType: null,
|
||||
value: {
|
||||
foo: {
|
||||
type: PropType.Number,
|
||||
@@ -427,6 +452,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -441,6 +467,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
preview: 'undefined',
|
||||
containerType: null
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+75
-54
@@ -6,7 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Descriptor, NestedProp, PropType} from 'protocol';
|
||||
import {ContainerType, Descriptor, NestedProp, PropType} from 'protocol';
|
||||
import type {Signal} from '@angular/core';
|
||||
|
||||
import {isSignal, unwrapSignal} from '../utils';
|
||||
|
||||
import {getKeys} from './object-utils';
|
||||
import {getPropType} from './prop-type';
|
||||
@@ -19,44 +22,47 @@ const ignoreList = new Set([METADATA_PROPERTY_NAME, '__ngSimpleChanges__']);
|
||||
|
||||
const MAX_LEVEL = 1;
|
||||
|
||||
const nestedSerializer =
|
||||
(instance: any, propName: string|number, nodes: NestedProp[], currentLevel = 0,
|
||||
level = MAX_LEVEL): Descriptor => {
|
||||
const serializableInstance = instance[propName];
|
||||
const propData:
|
||||
PropertyData = {prop: serializableInstance, type: getPropType(serializableInstance)};
|
||||
function nestedSerializer(
|
||||
instance: any, propName: string|number, nodes: NestedProp[], isReadonly: boolean,
|
||||
currentLevel = 0, level = MAX_LEVEL): Descriptor {
|
||||
instance = unwrapSignal(instance);
|
||||
const serializableInstance = instance[propName];
|
||||
const propData: PropertyData = {
|
||||
prop: serializableInstance,
|
||||
type: getPropType(serializableInstance),
|
||||
containerType: getContainerType(serializableInstance)
|
||||
};
|
||||
|
||||
if (currentLevel < level) {
|
||||
return levelSerializer(
|
||||
instance, propName, currentLevel, level, nestedSerializerContinuation(nodes, level));
|
||||
}
|
||||
if (currentLevel < level) {
|
||||
const continuation =
|
||||
(instance: any, propName: string|number, isReadonly: boolean, nestedLevel?: number,
|
||||
_?: number) => {
|
||||
const nodeChildren = nodes.find((v) => v.name === propName)?.children ?? [];
|
||||
return nestedSerializer(instance, propName, nodeChildren, isReadonly, nestedLevel, level);
|
||||
};
|
||||
|
||||
switch (propData.type) {
|
||||
case PropType.Array:
|
||||
case PropType.Object:
|
||||
return createNestedSerializedDescriptor(
|
||||
instance, propName, propData, {level, currentLevel}, nodes, nestedSerializer);
|
||||
default:
|
||||
return createShallowSerializedDescriptor(instance, propName, propData);
|
||||
}
|
||||
};
|
||||
return levelSerializer(instance, propName, isReadonly, currentLevel, level, continuation);
|
||||
}
|
||||
|
||||
const nestedSerializerContinuation = (nodes: NestedProp[], level: number) =>
|
||||
(instance: any, propName: string|number, nestedLevel?: number, _?: number) => {
|
||||
const idx = nodes.findIndex((v) => v.name === propName);
|
||||
if (idx < 0) {
|
||||
// The property is not specified in the query.
|
||||
return nestedSerializer(instance, propName, [], nestedLevel, level);
|
||||
}
|
||||
return nestedSerializer(instance, propName, nodes[idx].children, nestedLevel, level);
|
||||
};
|
||||
switch (propData.type) {
|
||||
case PropType.Array:
|
||||
case PropType.Object:
|
||||
return createNestedSerializedDescriptor(
|
||||
instance, propName, propData, {level, currentLevel}, nodes, nestedSerializer);
|
||||
default:
|
||||
return createShallowSerializedDescriptor(instance, propName, propData, isReadonly);
|
||||
}
|
||||
}
|
||||
|
||||
function levelSerializer(
|
||||
instance: any, propName: string|number, currentLevel = 0, level = MAX_LEVEL,
|
||||
continuation = levelSerializer): Descriptor {
|
||||
instance: any, propName: string|number, isReadonly: boolean, currentLevel = 0,
|
||||
level = MAX_LEVEL, continuation = levelSerializer): Descriptor {
|
||||
const serializableInstance = instance[propName];
|
||||
const propData:
|
||||
PropertyData = {prop: serializableInstance, type: getPropType(serializableInstance)};
|
||||
const propData: PropertyData = {
|
||||
prop: serializableInstance,
|
||||
type: getPropType(serializableInstance),
|
||||
containerType: getContainerType(serializableInstance)
|
||||
};
|
||||
|
||||
switch (propData.type) {
|
||||
case PropType.Array:
|
||||
@@ -64,35 +70,50 @@ function levelSerializer(
|
||||
return createLevelSerializedDescriptor(
|
||||
instance, propName, propData, {level, currentLevel}, continuation);
|
||||
default:
|
||||
return createShallowSerializedDescriptor(instance, propName, propData);
|
||||
return createShallowSerializedDescriptor(instance, propName, propData, isReadonly);
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeDirectiveState(
|
||||
instance: object, levels = MAX_LEVEL): {[key: string]: Descriptor} {
|
||||
export function serializeDirectiveState(instance: object): Record<string, Descriptor> {
|
||||
const result: Record<string, Descriptor> = {};
|
||||
getKeys(instance).forEach((prop) => {
|
||||
const value = unwrapSignal(instance);
|
||||
const isReadonly = isSignal(instance);
|
||||
getKeys(value).forEach((prop) => {
|
||||
if (typeof prop === 'string' && ignoreList.has(prop)) {
|
||||
return;
|
||||
}
|
||||
result[prop] = levelSerializer(instance, prop, 0, 0);
|
||||
result[prop] = levelSerializer(value, prop, isReadonly, 0, 0);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export const deeplySerializeSelectedProperties =
|
||||
(instance: any, props: NestedProp[]): {[name: string]: Descriptor} => {
|
||||
const result: Record<string, Descriptor> = {};
|
||||
getKeys(instance).forEach((prop) => {
|
||||
if (ignoreList.has(prop)) {
|
||||
return;
|
||||
}
|
||||
const idx = props.findIndex((v) => v.name === prop);
|
||||
if (idx < 0) {
|
||||
result[prop] = levelSerializer(instance, prop);
|
||||
} else {
|
||||
result[prop] = nestedSerializer(instance, prop, props[idx].children);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
export function deeplySerializeSelectedProperties(
|
||||
instance: object, props: NestedProp[]): Record<string, Descriptor> {
|
||||
const result: Record<string, Descriptor> = {};
|
||||
const isReadonly = isSignal(instance);
|
||||
getKeys(instance).forEach((prop) => {
|
||||
if (ignoreList.has(prop)) {
|
||||
return;
|
||||
}
|
||||
const childrenProps = props.find((v) => v.name === prop)?.children;
|
||||
if (!childrenProps) {
|
||||
result[prop] = levelSerializer(instance, prop, isReadonly);
|
||||
} else {
|
||||
result[prop] = nestedSerializer(instance, prop, childrenProps, isReadonly);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function getContainerType(instance: unknown): ContainerType {
|
||||
if (isSignal(instance)) {
|
||||
return isWritableSignal(instance) ? 'WritableSignal' : 'ReadonlySignal';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isWritableSignal(s: any): boolean {
|
||||
return typeof s['set'] === 'function';
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export const ngDebug = () => (window as any).ng;
|
||||
|
||||
export const runOutsideAngular = (f: () => any): void => {
|
||||
const w = window as any;
|
||||
if (!w.Zone || !w.Zone.current) {
|
||||
@@ -34,3 +36,37 @@ export const isCustomElement = (node: Node) => {
|
||||
const tagName = node.tagName.toLowerCase();
|
||||
return !!customElements.get(tagName);
|
||||
};
|
||||
|
||||
export function hasDiDebugAPIs(): boolean {
|
||||
if (!ngDebugApiIsSupported('ɵgetInjectorResolutionPath')) {
|
||||
return false;
|
||||
}
|
||||
if (!ngDebugApiIsSupported('ɵgetDependenciesFromInjectable')) {
|
||||
return false;
|
||||
}
|
||||
if (!ngDebugApiIsSupported('ɵgetInjectorProviders')) {
|
||||
return false;
|
||||
}
|
||||
if (!ngDebugApiIsSupported('ɵgetInjectorMetadata')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function ngDebugApiIsSupported(api: string): boolean {
|
||||
const ng = ngDebug();
|
||||
return typeof ng[api] === 'function';
|
||||
}
|
||||
|
||||
|
||||
export function isSignal(prop: unknown): prop is() => unknown {
|
||||
if (!ngDebugApiIsSupported('isSignal')) {
|
||||
return false;
|
||||
}
|
||||
return (window as any).ng.isSignal(prop);
|
||||
}
|
||||
|
||||
export function unwrapSignal(s: any): any {
|
||||
return isSignal(s) ? s() : s;
|
||||
}
|
||||
|
||||
+8
@@ -18,12 +18,14 @@ describe('arrayify', () => {
|
||||
expandable: true,
|
||||
preview: '',
|
||||
type: PropType.Array,
|
||||
containerType: null,
|
||||
},
|
||||
bar: {
|
||||
editable: true,
|
||||
expandable: true,
|
||||
preview: '',
|
||||
type: PropType.Array,
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
expect(arr).toEqual([
|
||||
@@ -34,6 +36,7 @@ describe('arrayify', () => {
|
||||
expandable: true,
|
||||
preview: '',
|
||||
type: PropType.Array,
|
||||
containerType: null,
|
||||
},
|
||||
parent: null,
|
||||
},
|
||||
@@ -44,6 +47,7 @@ describe('arrayify', () => {
|
||||
expandable: true,
|
||||
preview: '',
|
||||
type: PropType.Array,
|
||||
containerType: null,
|
||||
},
|
||||
parent: null,
|
||||
},
|
||||
@@ -56,12 +60,14 @@ describe('arrayify', () => {
|
||||
expandable: true,
|
||||
preview: '',
|
||||
type: PropType.Array,
|
||||
containerType: null,
|
||||
},
|
||||
2: {
|
||||
editable: true,
|
||||
expandable: true,
|
||||
preview: '',
|
||||
type: PropType.Array,
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
expect(arr).toEqual([
|
||||
@@ -72,6 +78,7 @@ describe('arrayify', () => {
|
||||
expandable: true,
|
||||
preview: '',
|
||||
type: PropType.Array,
|
||||
containerType: null,
|
||||
},
|
||||
parent: null,
|
||||
},
|
||||
@@ -82,6 +89,7 @@ describe('arrayify', () => {
|
||||
expandable: true,
|
||||
preview: '',
|
||||
type: PropType.Array,
|
||||
containerType: null,
|
||||
},
|
||||
parent: null,
|
||||
},
|
||||
|
||||
+5
@@ -33,6 +33,7 @@ const properties: Properties = {
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
i: {
|
||||
editable: false,
|
||||
@@ -55,6 +56,7 @@ const properties: Properties = {
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
p: {
|
||||
editable: false,
|
||||
@@ -77,6 +79,7 @@ const properties: Properties = {
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
i_1: {
|
||||
editable: true,
|
||||
@@ -84,12 +87,14 @@ const properties: Properties = {
|
||||
preview: 'input i1',
|
||||
type: PropType.String,
|
||||
value: 'input i1',
|
||||
containerType: null,
|
||||
},
|
||||
o_1: {
|
||||
editable: false,
|
||||
expandable: true,
|
||||
preview: '',
|
||||
type: PropType.Object,
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
|
||||
+2
@@ -54,6 +54,7 @@ const fooNestedProperties: Properties = {
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -66,6 +67,7 @@ const barNestedProps: Properties = {
|
||||
preview: 'undefined',
|
||||
type: PropType.Undefined,
|
||||
value: undefined,
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+3
@@ -26,6 +26,7 @@ describe('PropertyDataSource', () => {
|
||||
preview: '42',
|
||||
type: PropType.Number,
|
||||
value: 42,
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
getTreeFlattener(), flatTreeControl, {element: [1, 2, 3]}, null as any);
|
||||
@@ -37,6 +38,7 @@ describe('PropertyDataSource', () => {
|
||||
preview: '43',
|
||||
type: PropType.Number,
|
||||
value: 43,
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -51,6 +53,7 @@ describe('PropertyDataSource', () => {
|
||||
preview: '43',
|
||||
type: PropType.Number,
|
||||
value: 43,
|
||||
containerType: null,
|
||||
},
|
||||
name: 'foo',
|
||||
parent: null,
|
||||
|
||||
+8
-3
@@ -3,9 +3,14 @@
|
||||
@switch (currentPropertyState) {
|
||||
@case (readState) {
|
||||
<span>
|
||||
<span class="editable">
|
||||
{{ valueToSubmit }}
|
||||
</span>
|
||||
@switch(containerType) {
|
||||
@case('WritableSignal') {
|
||||
<span class="editable">Signal({{valueToSubmit}})</span>
|
||||
}
|
||||
@default {
|
||||
<span class="editable">{{ valueToSubmit }}</span>
|
||||
}
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@case (writeState) {
|
||||
|
||||
+3
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import {AfterViewChecked, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnInit, Output,} from '@angular/core';
|
||||
import {ContainerType} from 'protocol';
|
||||
|
||||
type EditorType = string|number|boolean;
|
||||
type EditorResult = EditorType|Array<EditorType>;
|
||||
@@ -32,6 +33,8 @@ const parseValue = (value: EditorResult): EditorResult => {
|
||||
export class PropertyEditorComponent implements AfterViewChecked, OnInit {
|
||||
@Input({required: true}) key!: string;
|
||||
@Input({required: true}) initialValue!: EditorResult;
|
||||
@Input() containerType: ContainerType = null;
|
||||
|
||||
@Output() updateValue = new EventEmitter<EditorResult>();
|
||||
|
||||
readState = PropertyEditorState.Read;
|
||||
|
||||
+3
-2
@@ -1,13 +1,14 @@
|
||||
@if (treeControl) {
|
||||
<mat-tree 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>:
|
||||
<span class="name non-expandable">{{ node.prop.name }}</span>:
|
||||
@if (!node.prop.descriptor.editable) {
|
||||
<ng-property-preview (inspect)="inspect.emit(node)" [node]="node"></ng-property-preview>
|
||||
} @else {
|
||||
<ng-property-editor
|
||||
[key]="node.prop.name"
|
||||
[initialValue]="node.prop.descriptor.value || node.prop.descriptor.preview"
|
||||
[containerType]="node.prop.descriptor.containerType"
|
||||
(updateValue)="handleUpdate(node, $event)"
|
||||
/>
|
||||
}
|
||||
@@ -18,7 +19,7 @@
|
||||
{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
|
||||
</mat-icon>
|
||||
|
||||
<span class="name"> {{ node.prop.name }} </span>:
|
||||
<span class="name">{{ node.prop.name }}</span>:
|
||||
<span class="value">
|
||||
{{ treeControl.isExpanded(node) ? '' : node.prop.descriptor.preview }}
|
||||
</span>
|
||||
|
||||
@@ -55,6 +55,8 @@ export interface InjectedService {
|
||||
providedIn: Injector;
|
||||
}
|
||||
|
||||
export type ContainerType = 'WritableSignal'|'ReadonlySignal'|null;
|
||||
|
||||
export enum PropType {
|
||||
Number,
|
||||
String,
|
||||
@@ -79,6 +81,7 @@ export interface Descriptor {
|
||||
editable: boolean;
|
||||
type: PropType;
|
||||
preview: string;
|
||||
containerType: ContainerType;
|
||||
}
|
||||
|
||||
export interface DirectivesProperties {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, ElementRef, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core';
|
||||
import {Component, computed, ElementRef, EventEmitter, Input, Output, signal, ViewChild, ViewEncapsulation} from '@angular/core';
|
||||
|
||||
import {ZippyComponent} from './zippy.component';
|
||||
|
||||
@@ -26,6 +26,14 @@ export class DemoAppComponent {
|
||||
@Output() outputOne = new EventEmitter();
|
||||
@Output('output_two') outputTwo = new EventEmitter();
|
||||
|
||||
primitiveSignal = signal(123);
|
||||
primitiveComputed = computed(() => this.primitiveSignal() ** 2);
|
||||
objectSignal = signal({name: 'John', age: 40});
|
||||
objectComputed = computed(() => {
|
||||
const original = this.objectSignal();
|
||||
return {...original, age: original.age + 1};
|
||||
});
|
||||
|
||||
getTitle(): '► Click to expand'|'▼ Click to collapse' {
|
||||
if (!this.zippy || !this.zippy.visible) {
|
||||
return '► Click to expand';
|
||||
|
||||
@@ -9,6 +9,7 @@ import {assertDefined} from '../../util/assert';
|
||||
import {global} from '../../util/global';
|
||||
import {setupFrameworkInjectorProfiler} from '../debug/framework_injector_profiler';
|
||||
import {setProfiler} from '../profiler';
|
||||
import {isSignal} from '../reactivity/api';
|
||||
|
||||
import {applyChanges} from './change_detection_utils';
|
||||
import {getComponent, getContext, getDirectiveMetadata, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from './discovery_utils';
|
||||
@@ -55,6 +56,7 @@ const globalUtilsFunctions = {
|
||||
'getRootComponents': getRootComponents,
|
||||
'getDirectives': getDirectives,
|
||||
'applyChanges': applyChanges,
|
||||
'isSignal': isSignal,
|
||||
};
|
||||
type GlobalUtilsFunctions = keyof typeof globalUtilsFunctions;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user