From 72a7d83f02fabb39936035313628d19aa0088795 Mon Sep 17 00:00:00 2001 From: kangwei Date: Sun, 8 Mar 2020 14:03:23 +0800 Subject: [PATCH] enhance settings --- packages/demo/src/app.ts | 5 +- .../designer/src/designer/component-type.ts | 4 + .../src/designer/document/node/props/prop.ts | 2 +- packages/designer/src/utils/clone-deep.ts | 2 +- packages/designer/tsconfig.json | 2 +- packages/plugin-setters/number-setter.tsx | 3 + packages/plugin-setters/package.json | 5 + packages/plugin-settings-pane/src/main.ts | 45 +++++-- .../plugin-settings-pane/src/settings-tab.tsx | 110 ++++++++++++------ packages/utils/has-own-property.ts | 4 + .../{designer/src => }/utils/is-function.ts | 0 .../{designer/src => }/utils/is-object.ts | 0 .../src => }/utils/is-plain-object.ts | 0 packages/utils/package.json | 5 + packages/utils/shallow-equal.ts | 27 +++++ 15 files changed, 169 insertions(+), 45 deletions(-) create mode 100644 packages/plugin-setters/number-setter.tsx create mode 100644 packages/plugin-setters/package.json create mode 100644 packages/utils/has-own-property.ts rename packages/{designer/src => }/utils/is-function.ts (100%) rename packages/{designer/src => }/utils/is-object.ts (100%) rename packages/{designer/src => }/utils/is-plain-object.ts (100%) create mode 100644 packages/utils/package.json create mode 100644 packages/utils/shallow-equal.ts diff --git a/packages/demo/src/app.ts b/packages/demo/src/app.ts index b0b828549..9c6f9eea7 100644 --- a/packages/demo/src/app.ts +++ b/packages/demo/src/app.ts @@ -1,5 +1,6 @@ import { ViewController } from '@ali/recore'; import DesignView from '../../designer/src'; +import NumberSetter from '../../plugin-setters/number-setter'; import SettingsPane, { registerSetter } from '../../plugin-settings-pane/src'; import { EventEmitter } from 'events'; import { Input } from '@alifd/next'; @@ -17,7 +18,9 @@ registerSetter('EventsSetter', () => { }, '这里是事件设置'); }); -registerSetter('StringSetter', Input); +registerSetter('StringSetter', { component: Input, props: { placeholder: "请输入" } }); + +registerSetter('NumberSetter', NumberSetter as any); const emitter = new EventEmitter(); diff --git a/packages/designer/src/designer/component-type.ts b/packages/designer/src/designer/component-type.ts index 52665d801..71519f2ce 100644 --- a/packages/designer/src/designer/component-type.ts +++ b/packages/designer/src/designer/component-type.ts @@ -158,6 +158,10 @@ export class ComponentType { name: 'size', title: '大小', setter: 'StringSetter' + }, { + name: 'age', + title: '年龄', + setter: 'NumberSetter' }] }, { name: '#styles', diff --git a/packages/designer/src/designer/document/node/props/prop.ts b/packages/designer/src/designer/document/node/props/prop.ts index 639e42090..7b8599b31 100644 --- a/packages/designer/src/designer/document/node/props/prop.ts +++ b/packages/designer/src/designer/document/node/props/prop.ts @@ -3,7 +3,7 @@ import { valueToSource } from '../../../../utils/value-to-source'; import { CompositeValue, isJSExpression, isJSSlot, NodeSchema, NodeData, isNodeSchema } from '../../../schema'; import PropStash from './prop-stash'; import { uniqueId } from '../../../../../../utils/unique-id'; -import { isPlainObject } from '../../../../utils/is-plain-object'; +import { isPlainObject } from '../../../../../../utils/is-plain-object'; import { hasOwnProperty } from '../../../../utils/has-own-property'; import Props from './props'; import Node from '../node'; diff --git a/packages/designer/src/utils/clone-deep.ts b/packages/designer/src/utils/clone-deep.ts index 94719418a..535b843b1 100644 --- a/packages/designer/src/utils/clone-deep.ts +++ b/packages/designer/src/utils/clone-deep.ts @@ -1,4 +1,4 @@ -import { isPlainObject } from './is-plain-object'; +import { isPlainObject } from '../../../utils/is-plain-object'; export function cloneDeep(src: any): any { const type = typeof src; diff --git a/packages/designer/tsconfig.json b/packages/designer/tsconfig.json index 8bfdb77e8..0d509c931 100644 --- a/packages/designer/tsconfig.json +++ b/packages/designer/tsconfig.json @@ -4,6 +4,6 @@ "experimentalDecorators": true }, "include": [ - "./src/", "../utils/unique-id.ts" + "./src/", "../utils/unique-id.ts", "../utils/is-plain-object.ts", "../utils/is-object.ts", "../utils/is-function.ts" ] } diff --git a/packages/plugin-setters/number-setter.tsx b/packages/plugin-setters/number-setter.tsx new file mode 100644 index 000000000..c43b2aa89 --- /dev/null +++ b/packages/plugin-setters/number-setter.tsx @@ -0,0 +1,3 @@ +import { NumberPicker } from '@alifd/next'; + +export default NumberPicker; diff --git a/packages/plugin-setters/package.json b/packages/plugin-setters/package.json new file mode 100644 index 000000000..5e02945a3 --- /dev/null +++ b/packages/plugin-setters/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@alifd/next": "^1.19.16" + } +} diff --git a/packages/plugin-settings-pane/src/main.ts b/packages/plugin-settings-pane/src/main.ts index 3ab9766d9..9c52c01b7 100644 --- a/packages/plugin-settings-pane/src/main.ts +++ b/packages/plugin-settings-pane/src/main.ts @@ -74,6 +74,8 @@ export function isCustomView(obj: any): obj is CustomView { return obj && (isValidElement(obj) || isReactComponent(obj)); } +export type DynamicProps = (field: SettingField, editor: any) => object; + export interface SetterConfig { /** * if *string* passed must be a registered Setter Name @@ -82,9 +84,7 @@ export interface SetterConfig { /** * the props pass to Setter Component */ - props?: { - [prop: string]: any; - }; + props?: object | DynamicProps; children?: any; } @@ -225,26 +225,57 @@ export class SettingField implements SettingTarget { // ====== 当前属性读写 ===== + // Todo cache!! + /** + * 判断当前属性值是否一致 + */ get isSameValue(): boolean { - // todo: + if (this.type !== 'field') { + return false; + } + const propName = this.path.join('.'); + const first = this.nodes[0].getProp(propName)!; + let l = this.nodes.length; + while (l-- > 1) { + const next = this.nodes[l].getProp(propName, false); + if (!first.isEqual(next)) { + return false; + } + } return true; } + /** + * 获取当前属性值 + */ getValue(): any { + if (this.type !== 'field') { + return null; + } return this.parent.getPropValue(this.name); } + /** + * 设置当前属性值 + */ setValue(val: any) { + if (this.type !== 'field') { + return; + } this.parent.setPropValue(this.name, val); } - // 设置属性值 + /** + * 设置子级属性值 + */ setPropValue(propName: string, value: any) { const path = this.type === 'field' ? `${this.name}.${propName}` : propName; this.parent.setPropValue(path, value); } - // 获取属性值 + /** + * 获取子级属性值 + */ getPropValue(propName: string): any { const path = this.type === 'field' ? `${this.name}.${propName}` : propName; return this.parent.getPropValue(path); @@ -418,7 +449,7 @@ export class SettingsMain implements SettingTarget { // setups this.setupComponentType(); - // todo: enhance that componentType not changed + // todo: enhance when componentType not changed do merge // clear fields this.setupItems(); diff --git a/packages/plugin-settings-pane/src/settings-tab.tsx b/packages/plugin-settings-pane/src/settings-tab.tsx index 9959a2d51..0bad74192 100644 --- a/packages/plugin-settings-pane/src/settings-tab.tsx +++ b/packages/plugin-settings-pane/src/settings-tab.tsx @@ -1,42 +1,96 @@ -import { Component, isValidElement, ReactNode, ReactElement, ComponentType as ReactComponentType } from 'react'; -import { isReactClass } from '../../utils/is-react'; +import { Component, ReactNode } from 'react'; import { createContent } from '../../utils/create-content'; -import { SettingField, CustomView, isSettingField, SettingTarget } from './main'; +import { shallowEqual } from '../../utils/shallow-equal'; +import { + SettingField, + CustomView, + isSettingField, + SettingTarget, + SetterConfig, + isCustomView, + DynamicProps, +} from './main'; import { Field, FieldGroup } from './field'; -const settersMap = new Map>(); -export function registerSetter(type: string, setter: ReactElement | ReactComponentType) { +export type RegisteredSetter = CustomView | { + component: CustomView; + props?: object; +}; + +const settersMap = new Map(); +export function registerSetter(type: string, setter: RegisteredSetter) { settersMap.set(type, setter); } -export function getSetter(type: string): ReactElement | ReactComponentType | null { +export function getSetter(type: string): RegisteredSetter | null { return settersMap.get(type) || null; } export function createSetterContent(setter: any, props: object): ReactNode { if (typeof setter === 'string') { setter = getSetter(setter); + if (!isCustomView(setter)) { + if (setter.props) { + props = { + ...setter.props, + ...props, + }; + } + setter = setter.component; + } } return createContent(setter, props); } +function isSetterConfig(obj: any): obj is SetterConfig { + return obj && typeof obj === 'object' && 'componentName' in obj && !isCustomView(obj); +} + class SettingFieldView extends Component<{ field: SettingField }> { state = { visible: false, value: null, + setterProps: {}, }; private dispose: () => void; + private setterType?: string | CustomView; constructor(props: any) { super(props); const { field } = this.props; - const { condition } = field.extraProps; + const { setter } = field; + let setterProps: object | DynamicProps = {}; + if (isSetterConfig(setter)) { + this.setterType = setter.componentName; + if (setter.props) { + setterProps = setter.props; + } + } else if (setter) { + this.setterType = setter; + } let firstRun: boolean = true; this.dispose = field.onEffect(() => { const state: any = {}; - state.visible = (field.isOne && typeof condition === 'function') ? !condition(field, field.editor) : true; + const { extraProps, editor } = field; + const { condition, defaultValue } = extraProps; + state.visible = field.isOne && typeof condition === 'function' ? !condition(field, editor) : true; if (state.visible) { - state.value = field.getValue(); + state.setterProps = { + ...(typeof setterProps === 'function' ? setterProps(field, editor) : setterProps), + }; + if (field.type === 'field') { + state.value = field.getValue(); + if (defaultValue != null && !('defaultValue' in state.setterProps)) { + state.setterProps.defaultValue = defaultValue; + } + if (!field.isSameValue) { + state.setterProps.multiValue = true; + if (!('placeholder' in props)) { + state.setterProps.placeholder = '多种值'; + } + } + // TODO: error handling + } } if (firstRun) { firstRun = false; @@ -48,7 +102,12 @@ class SettingFieldView extends Component<{ field: SettingField }> { } shouldComponentUpdate(_: any, nextState: any) { - if (nextState.value !== this.state.value || nextState.visible !== this.state.visible) { + const { state } = this; + if ( + nextState.value !== state.value || + nextState.visible !== state.visible || + !shallowEqual(state.setterProps, nextState.setterProps) + ) { return true; } return false; @@ -60,35 +119,17 @@ class SettingFieldView extends Component<{ field: SettingField }> { render() { const { field } = this.props; - const { setter, title, extraProps } = field; - const { defaultValue } = extraProps; - const { visible, value } = this.state; - // reaction point + const { visible, value, setterProps } = this.state; if (!visible) { return null; } - let setterType = setter; - let props: any = {}; - if (typeof setter === 'object' && 'componentName' in setter && !(isValidElement(setter) || isReactClass(setter))) { - setterType = (setter as any).componentName; - props = (setter as any).props; - } - if (defaultValue != null && !('defaultValue' in props)) { - props.defaultValue = defaultValue; - } - /* - if (!('placeholder' in props) && !isSameValue) { - props.placeholder = '多种值'; - } - */ - // todo: error handling return ( - - {createSetterContent(setterType, { - ...props, + + {createSetterContent(this.setterType, { + ...setterProps, key: field.id, // === injection prop: field, @@ -100,7 +141,7 @@ class SettingFieldView extends Component<{ field: SettingField }> { value, }); field.setValue(value); - } + }, })} ); @@ -120,7 +161,7 @@ class SettingGroupView extends Component<{ field: SettingField }> { let firstRun: boolean = true; this.dispose = field.onEffect(() => { const state: any = {}; - state.visible = (field.isOne && typeof condition === 'function') ? !condition(field, field.editor) : true; + state.visible = field.isOne && typeof condition === 'function' ? !condition(field, field.editor) : true; if (state.visible) { state.items = field.items.slice(); } @@ -134,6 +175,7 @@ class SettingGroupView extends Component<{ field: SettingField }> { } shouldComponentUpdate(_: any, nextState: any) { + // todo: shallowEqual ? if (nextState.items !== this.state.items || nextState.visible !== this.state.visible) { return true; } diff --git a/packages/utils/has-own-property.ts b/packages/utils/has-own-property.ts new file mode 100644 index 000000000..ea5ece914 --- /dev/null +++ b/packages/utils/has-own-property.ts @@ -0,0 +1,4 @@ +const prototypeHasOwnProperty = Object.prototype.hasOwnProperty; +export function hasOwnProperty(obj: any, key: string | number | symbol): boolean { + return obj && prototypeHasOwnProperty.call(obj, key); +} diff --git a/packages/designer/src/utils/is-function.ts b/packages/utils/is-function.ts similarity index 100% rename from packages/designer/src/utils/is-function.ts rename to packages/utils/is-function.ts diff --git a/packages/designer/src/utils/is-object.ts b/packages/utils/is-object.ts similarity index 100% rename from packages/designer/src/utils/is-object.ts rename to packages/utils/is-object.ts diff --git a/packages/designer/src/utils/is-plain-object.ts b/packages/utils/is-plain-object.ts similarity index 100% rename from packages/designer/src/utils/is-plain-object.ts rename to packages/utils/is-plain-object.ts diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 000000000..b90bb4493 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@babel/runtime": "^7.8.7" + } +} diff --git a/packages/utils/shallow-equal.ts b/packages/utils/shallow-equal.ts new file mode 100644 index 000000000..c7fdb9cb1 --- /dev/null +++ b/packages/utils/shallow-equal.ts @@ -0,0 +1,27 @@ +import { hasOwnProperty } from './has-own-property'; + +export function shallowEqual(objA: any, objB: any): boolean { + if (objA === objB) { + return true; + } + + if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { + return false; + } + + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + + if (keysA.length !== keysB.length) { + return false; + } + + // Test for A's keys different from B. + for (let i = 0; i < keysA.length; i++) { + if (!hasOwnProperty(objB, keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) { + return false; + } + } + + return true; +}