diff --git a/packages/editor/__tests__/model/fieldModel.spec.ts b/packages/editor/__tests__/model/fieldModel.spec.ts index 1f66dc04..6a8ff04e 100644 --- a/packages/editor/__tests__/model/fieldModel.spec.ts +++ b/packages/editor/__tests__/model/fieldModel.spec.ts @@ -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, + }, + }, + ], + }); }); }); diff --git a/packages/editor/__tests__/model/mock.ts b/packages/editor/__tests__/model/mock.ts index 185d376e..849ce391 100644 --- a/packages/editor/__tests__/model/mock.ts +++ b/packages/editor/__tests__/model/mock.ts @@ -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', + }, + }, + }, + ], }, ]; diff --git a/packages/editor/src/AppModel/ComponentModel.ts b/packages/editor/src/AppModel/ComponentModel.ts index d34fb6e3..8060620c 100644 --- a/packages/editor/src/AppModel/ComponentModel.ts +++ b/packages/editor/src/AppModel/ComponentModel.ts @@ -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; } diff --git a/packages/editor/src/AppModel/FieldModel.ts b/packages/editor/src/AppModel/FieldModel.ts index 79129347..a022927b 100644 --- a/packages/editor/src/AppModel/FieldModel.ts +++ b/packages/editor/src/AppModel/FieldModel.ts @@ -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 extends Array ? 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); + } } } diff --git a/packages/editor/src/AppModel/IAppModel.ts b/packages/editor/src/AppModel/IAppModel.ts index eb76eeea..25e3ce47 100644 --- a/packages/editor/src/AppModel/IAppModel.ts +++ b/packages/editor/src/AppModel/IAppModel.ts @@ -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; diff --git a/packages/editor/src/AppModel/TraitModel.ts b/packages/editor/src/AppModel/TraitModel.ts index ca1beaa6..615478f0 100644 --- a/packages/editor/src/AppModel/TraitModel.ts +++ b/packages/editor/src/AppModel/TraitModel.ts @@ -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() { diff --git a/packages/editor/src/validator/SchemaValidator.ts b/packages/editor/src/validator/SchemaValidator.ts index 91fbf96b..78841dc9 100644 --- a/packages/editor/src/validator/SchemaValidator.ts +++ b/packages/editor/src/validator/SchemaValidator.ts @@ -136,7 +136,8 @@ export class SchemaValidator implements ISchemaValidator { .addKeyword('category') .addKeyword('widgetOptions') .addKeyword('conditions') - .addKeyword('name'); + .addKeyword('name') + .addKeyword('isComponentId'); this.validatorMap = { components: {}, diff --git a/packages/runtime/src/traits/core/Slot.tsx b/packages/runtime/src/traits/core/Slot.tsx index da6fc11b..fad51893 100644 --- a/packages/runtime/src/traits/core/Slot.tsx +++ b/packages/runtime/src/traits/core/Slot.tsx @@ -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 diff --git a/packages/shared/src/specs/event.ts b/packages/shared/src/specs/event.ts index e7d08d1b..56aa83ba 100644 --- a/packages/shared/src/specs/event.ts +++ b/packages/shared/src/specs/event.ts @@ -4,6 +4,7 @@ import { CORE_VERSION, CoreWidgetName } from '../constants/core'; const BaseEventSpecObject = { componentId: Type.String({ title: 'Component ID', + isComponentId: true, }), method: Type.Object( { diff --git a/packages/shared/src/types/spec.ts b/packages/shared/src/types/spec.ts index 8cc03f62..befadef3 100644 --- a/packages/shared/src/types/spec.ts +++ b/packages/shared/src/types/spec.ts @@ -11,4 +11,6 @@ export type SpecOptions> = { name?: string; // conditional render conditions?: Condition[]; + // is a reference of component id + isComponentId?: boolean; };