feat(editor): chagne the component id reference properties' values when changing the component id

add the `isComponentId` into spec options to declare a property is whether refer to a component id

ISSUES CLOSED: #106
This commit is contained in:
MrWindlike 2022-06-13 14:55:13 +08:00
parent d85ed7dc53
commit b6b7083e15
10 changed files with 187 additions and 36 deletions

View File

@ -3,6 +3,7 @@ import { FieldModel } from '../../src/AppModel/FieldModel';
import { ComponentId } from '../../src/AppModel/IAppModel';
import { registry } from '../services';
import { ChangeIdMockSchema } from './mock';
import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared';
describe('Field test', () => {
it('parse static property', () => {
@ -82,6 +83,7 @@ describe('Field test', () => {
const appModel = new AppModel(ChangeIdMockSchema, registry);
const input = appModel.getComponentById('input' as ComponentId);
const text = appModel.getComponentById('text' as ComponentId);
const button = appModel.getComponentById('button' as ComponentId);
input.changeId('input1' as ComponentId);
@ -92,5 +94,27 @@ describe('Field test', () => {
format: 'plain',
},
});
expect(
button.traits.find(trait => trait.type === `${CORE_VERSION}/${CoreTraitName.Event}`)
.properties.rawValue
).toEqual({
handlers: [
{
type: 'onClick',
componentId: 'input1',
method: {
name: 'setInputValue',
parameters: {
value: 'Hello',
},
},
disabled: false,
wait: {
type: 'delay',
time: 0,
},
},
],
});
});
});

View File

@ -187,6 +187,63 @@ export const EventHandlerMockSchema: ComponentSchema[] = [
];
export const ChangeIdMockSchema: ComponentSchema[] = [
{
id: 'stack',
type: 'core/v1/stack',
properties: {
spacing: 12,
direction: 'horizontal',
align: 'auto',
wrap: '',
justify: '',
},
traits: [],
},
{
id: 'button',
type: 'chakra_ui/v1/button',
properties: {
text: {
raw: 'text',
format: 'plain',
},
isLoading: false,
colorScheme: 'blue',
},
traits: [
{
type: 'core/v1/event',
properties: {
handlers: [
{
type: 'onClick',
componentId: 'input',
method: {
name: 'setInputValue',
parameters: {
value: 'Hello',
},
},
disabled: false,
wait: {
type: 'delay',
time: 0,
},
},
],
},
},
{
type: 'core/v1/slot',
properties: {
container: {
id: 'stack',
slot: 'content',
},
},
},
],
},
{
id: 'text',
type: 'core/v1/text',
@ -196,7 +253,31 @@ export const ChangeIdMockSchema: ComponentSchema[] = [
format: 'plain',
},
},
traits: [],
traits: [
{
type: 'core/v1/style',
properties: {
styles: [
{
styleSlot: 'content',
style: '',
cssProperties: {
padding: '8px 0px 9px 0px ',
},
},
],
},
},
{
type: 'core/v1/slot',
properties: {
container: {
id: 'stack',
slot: 'content',
},
},
},
],
},
{
id: 'input',
@ -210,6 +291,16 @@ export const ChangeIdMockSchema: ComponentSchema[] = [
isRequired: false,
defaultValue: '',
},
traits: [],
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'stack',
slot: 'content',
},
},
},
],
},
];

View File

@ -58,7 +58,12 @@ export class ComponentModel implements IComponentModel {
this.genStateExample();
this.parentId = this._slotTrait?.rawProperties.container.id;
this.parentSlot = this._slotTrait?.rawProperties.container.slot;
this.properties = new FieldModel(schema.properties, this.appModel, this);
this.properties = new FieldModel(
schema.properties,
this.spec.spec.properties,
this.appModel,
this
);
}
get slots() {
@ -207,25 +212,23 @@ export class ComponentModel implements IComponentModel {
changeId(newId: ComponentId) {
const oldId = this.id;
const isIdExist = !!this.appModel.getComponentById(newId);
if (isIdExist) {
throw Error(`Id ${newId} already exist`);
}
this.id = newId;
for (const slot in this.children) {
const slotChildren = this.children[slot as SlotName];
slotChildren.forEach(child => {
child.parentId = newId;
const slotTrait = child.traits.find(t => t.type === SlotTraitType);
if (slotTrait) {
slotTrait.properties.update({ container: { id: newId, slot } });
slotTrait._isDirty = true;
}
child._isDirty = true;
});
}
this._isDirty = true;
this.appModel.changeComponentMapId(oldId, newId);
this.appModel.emitter.emit('idChange', { oldId, newId });
return this;
}

View File

@ -1,4 +1,4 @@
import { parseExpression, expChunkToString } from '@sunmao-ui/shared';
import { parseExpression, expChunkToString, SpecOptions } from '@sunmao-ui/shared';
import * as acorn from 'acorn';
import * as acornLoose from 'acorn-loose';
import { simple as simpleWalk } from 'acorn-walk';
@ -8,11 +8,13 @@ import {
ComponentId,
IAppModel,
IComponentModel,
ITraitModel,
IFieldModel,
ModuleId,
RefInfo,
} from './IAppModel';
import escodegen from 'escodegen';
import { JSONSchema7 } from 'json-schema';
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
@ -24,8 +26,10 @@ export class FieldModel implements IFieldModel {
constructor(
value: unknown,
public spec?: JSONSchema7 & SpecOptions,
public appModel?: IAppModel,
public componentModel?: IComponentModel
public componentModel?: IComponentModel,
public traitModel?: ITraitModel
) {
this.update(value);
this.appModel?.emitter.on('idChange', this.onReferenceIdChange.bind(this));
@ -63,7 +67,15 @@ export class FieldModel implements IFieldModel {
(oldValue as FieldModel).updateValue(value[key], false);
newValue = oldValue;
} else {
newValue = new FieldModel(value[key], this.appModel, this.componentModel);
newValue = new FieldModel(
value[key],
(this.spec?.properties?.[key] || this.spec?.items) as
| (JSONSchema7 & SpecOptions)
| undefined,
this.appModel,
this.componentModel,
this.traitModel
);
}
if (isArray(result)) {
@ -170,32 +182,40 @@ export class FieldModel implements IFieldModel {
}
onReferenceIdChange({ oldId, newId }: { oldId: ComponentId; newId: ComponentId }) {
if (!(this.refs[oldId] && this.componentModel)) {
if (!this.componentModel) {
return;
}
const exps = parseExpression(this.value as string);
const newExps = exps.map(exp => {
const node = this.nodes[exp.toString()];
if (node) {
const ref = this.refs[oldId];
ref.nodes.forEach(refNode => {
refNode.name = newId;
});
this.refs[newId] = ref;
delete this.refs[oldId];
return [escodegen.generate(node)];
if (this.spec?.isComponentId && this.value === oldId) {
if (this.traitModel) {
this.traitModel._isDirty = true;
}
this.componentModel._isDirty = true;
this.update(newId);
} else if (this.refs[oldId]) {
const exps = parseExpression(this.value as string);
const newExps = exps.map(exp => {
const node = this.nodes[exp.toString()];
return exp;
});
const value = expChunkToString(newExps);
if (node) {
const ref = this.refs[oldId];
this.componentModel._isDirty = true;
this.update(value);
ref.nodes.forEach(refNode => {
refNode.name = newId;
});
this.refs[newId] = ref;
delete this.refs[oldId];
return [escodegen.generate(node)];
}
return exp;
});
const value = expChunkToString(newExps);
this.componentModel._isDirty = true;
this.update(value);
}
}
}

View File

@ -4,8 +4,10 @@ import {
MethodSchema,
RuntimeTrait,
} from '@sunmao-ui/core';
import { SpecOptions } from '@sunmao-ui/shared';
import { Emitter } from 'mitt';
import { Node } from 'acorn';
import { JSONSchema7 } from 'json-schema';
export type ComponentId = string & {
kind: 'componentId';
@ -133,6 +135,7 @@ export interface IFieldModel {
// value: any;
appModel?: IAppModel;
componentModel?: IComponentModel;
spec?: JSONSchema7 & SpecOptions;
isDynamic: boolean;
rawValue: any;
update: (value: unknown) => void;

View File

@ -33,7 +33,13 @@ export class TraitModel implements ITraitModel {
this.id = `${this.parent.id}_trait${traitIdCount++}` as TraitId;
this.spec = this.registry.getTraitByType(this.type);
this.properties = new FieldModel(trait.properties, this.appModel, this.parent);
this.properties = new FieldModel(
trait.properties,
this.spec.spec.properties,
this.appModel,
this.parent,
this
);
}
get rawProperties() {

View File

@ -136,7 +136,8 @@ export class SchemaValidator implements ISchemaValidator {
.addKeyword('category')
.addKeyword('widgetOptions')
.addKeyword('conditions')
.addKeyword('name');
.addKeyword('name')
.addKeyword('isComponentId');
this.validatorMap = {
components: {},

View File

@ -4,7 +4,7 @@ import { implementRuntimeTrait } from '../../utils/buildKit';
const ContainerPropertySpec = Type.Object(
{
id: Type.String(),
id: Type.String({ isComponentId: true }),
slot: Type.String(),
},
// don't show this property in the editor

View File

@ -4,6 +4,7 @@ import { CORE_VERSION, CoreWidgetName } from '../constants/core';
const BaseEventSpecObject = {
componentId: Type.String({
title: 'Component ID',
isComponentId: true,
}),
method: Type.Object(
{

View File

@ -11,4 +11,6 @@ export type SpecOptions<WidgetOptions = Record<string, any>> = {
name?: string;
// conditional render
conditions?: Condition[];
// is a reference of component id
isComponentId?: boolean;
};