Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Custom Widget #313

Closed
MrWindlike opened this issue Feb 28, 2022 · 8 comments
Closed

Proposal: Custom Widget #313

MrWindlike opened this issue Feb 28, 2022 · 8 comments
Assignees
Labels
editor feature New feature or request

Comments

@MrWindlike
Copy link
Contributor

MrWindlike commented Feb 28, 2022

Demand Overview

We would allow component developers to customize the form widgets for the properties. And we may support these features for the widget:

  • Registering Widgets
  • Widget Props
  • Using Other Widgets In The Custom Widget
  • Using One Widget To Configure Multiple Properties
  • Widget Options

Scheme Design

Registering Widgets

We should register the widgets like registering the components, traits, and so on. Thus we also define a spec for widgets to describe their information:

interface WidgetSpec {
  version: string;
  metadata: {
    name: string;
  }
}

Widget Props

The interface of props which would pass to widget component:

interface WidgetProps {
  // the property schema
  schema: Schema & EditorSchema;
  // the value of the property
  value: any;
  // runtime services
  readonly services: Service;
  // components in the editor
  readonly components: Component[];
  // the handler of value changed
  onChange: (value: any)=> void;
  // render the sub-property schema normally
  renderBySchema: ({schema: Schema, onChange: (value: any)=> void, value: any})=> ReactElement;
}

Using Other Widgets In The Custom Widget

If component developers want to use other widgets for sub-properties, they can define the widgets in the sub-properties spec options and use renderBySchema function in the custom widget component.

In The following, I will show you an example of implementing a custom widget for the Table component's column property.

First of all, we should define the column property's spec.

{
  "column": Type.Array(Type.Object({
    ...,
    // use the common widget by its type
    "key": Type.KeyOf(...),
    // use the another widget defined in spec options
    "handlers": Type.Object({}, { "widget": "EventHandler" }),
  }, {
    "widget": "TableColumn"
  }))
}

And then we should implement the widget component and call renderBySchema inside.

function TableColumn (props) {
  const { schema, value, onChange, renderBySchema } = props;
  
  const onValueChange = (key, value)=> {
    onChange({
      ...value,
      [key]: value
    })
  }
  
  return (
    <div>
      {/* implement custom widget here */ }
      {/* use others widgets for some properties */ }
      { renderBySchema({ 
          schema: schema.properties.key, 
          value: value.key,
          onChange: (v)=> onValueChange('key', value) 
      }) }
      { renderBySchema({
          schema: schema.properties.handlers,
          value: value.handlers,
          onChange: (v)=> onValueChange('handlers', value) 
      }) }
    </div>
  )
}

Using One Widget To Config Multiple Properties

In some situations, some properties should be configured by the same widget is better.

For example, some components would have the location properties such as left, right, top, bottom, which should be configured by a location widget.

image.png

There are two ways that come to my mind. The first one is using Type.Object to wrap the properties which should configure by one widget. And another way is adding a new field like widgetGroup into property spec options to define a virtual group for properties.

The spec examples of these two ways are following this:

Type.Object

{
  "location": Type.Object({
    "left": Type.String(),
    "right": Type.String(),
    "top": Type.String(),
    "bottom": Type.String(),
  }, {
    "widget": "Location"
  })
}

Group

{
  "left": Type.String({ "widgetGroup": "Location" }),
  "right": Type.String({ "widgetGroup": "Location" }),
  "top": Type.String({ "widgetGroup": "Location" }),
  "bottom": Type.String({ "widgetGroup": "Location" }), 
}

The Type.Object way is already implemented and it's easy to use. Although it makes the component developers need to unwrap the properties to take their values in the components, I think it's doesn't matter.

The second way needs more additional editor implementation to support it, which would break the currently onChange logic.

Thus I prefer the first way, which is to use Type.Object to define the properties group.

Widget Options

The component developers may also want to pass some additional options to the widget component. We would provide the new field widgetOptions in properties spec options.

In the above location widget example, we may want to pass a keymap to the widget component for key transformed. For example, if we have paddingLeft property instead of left property, we should transform it.

So, we can define the map in the widgetOptions to tell the custom widget how to transform the keys.

{
  "location": Type.Object({
    "paddingLeft": Type.String(),
    "paddingRight": Type.String(),
    "paddingTop": Type.String(),
    "paddingBottom": Type.String(),
  }, {
    "widget": "Location",
    "wigetOptions": {
      "map": {
        "left": "paddingLeft",
        "right" "paddingRight",
        "top": "paddingTop",
        "bottom": "paddingBottom"
      }
    }
  })
}

And then we read the widgetOptions in the widget component and implement the transformed logic.

function Location (props) {
  const { schema, value, onChange } = props;
  const { widgetOptions = {} } = schema;
  const { map = {} } = widgetOptions;
  
  const onValueChange = (key, value)=> {
     onChange({
       ...value,
       [map[key] ?? key]: value
     });
  }
  
  return (
    <div>
      <input 
        value={value[map.left ?? 'left']} 
        onChange={(v)=> { onValueChange('left', v) }} 
      />
      ...
    </div>
  )
}
@MrWindlike MrWindlike added feature New feature or request editor labels Feb 28, 2022
@MrWindlike
Copy link
Contributor Author

@Yuyz0112 @tanbowensg @xzdry PTAL

@tanbowensg
Copy link
Collaborator

  1. The registry and store in widget props can merge into services, just like component and trait.
  2. Maybe value is better thanformData?
{
  "location": Type.Object({
    "left": Type.String(),
    "right": Type.String(),
    "top": Type.String(),
    "bottom": Type.String(),
  }, {
    "widget": "Location",
    "wigetOptions": {
      "map": {
        "left": "paddingLeft",
        "right" "paddingRight",
        "top": "paddingTop",
        "bottom": "paddingBottom"
      }
    }
  })
}

In this spec, left should be converted to paddingLeft according to map.While in location, the key is still left, shouldn't it be paddingLeft?

@MrWindlike
Copy link
Contributor Author

@tanbowensg Yes, you are right. I have already changed the contents of this proposal now.

@Yuyz0112
Copy link
Contributor

Yuyz0112 commented Mar 1, 2022

@MrWindlike @tanbowensg I'm going to start with a simple proposal.

If we has a implementation like this:

import { Editor, Widget, kit } from '@sunmao-ui/editor-sdk';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

// type Widget = React.FC<WidgetProps>
const RichTextEditor: Widget = (props) => {
  const { value, onChange, schema } = props;

  return (
    <>
      <kit.button>toolbar</kit.button>
      <kit.SchemaField schema={schema.subField} />
      <ReactQuill
        value={value}
        onChange={onChange}
      />
    </>
  );
}

Editor.registerWidget({
  name: 'myRichText',
  component: RichTextEditor,
});

In this example, we can import static functions from the SDK package, and get dynamic values from the props. This may have a more flexible usage when implementing a custom widget. Since developers may want to build a widget that has the same UI theme as same as other built-in widgets, they need things like kit.button.

And I think the widget mapping API is a shortcut, which developers can implement by themself like this:

import { Editor, Widget, kit } from "@sunmao-ui/editor-sdk";

// type Widget = React.FC<WidgetProps>
const LayoutWidget: Widget = (props) => {
  const { value, onChange } = props;

  return (
    <kit.Location
      value={{
        left: value.paddingLeft,
        right: value.paddingRight,
        top: value.paddingTop,
        bottom: value.paddingBottom,
      }}
      onChange={(v) =>
        onChange({
          paddingLeft: v.left,
          paddingRight: v.right,
          paddingTop: v.top,
          paddingBottom: v.bottom,
        })
      }
    />
  );
};

Editor.registerWidget({
  name: "myLayout",
  component: LayoutWidget,
});

This helps us remove some learning curve of our customize mapping API, and keep the flexibility when developers need to do more complex transformations. So I think we can let developers do this in userland at this moment and maybe provide some helper functions in the future.

@MrWindlike
Copy link
Contributor Author

@Yuyz0112

we can import static functions from the SDK package, and get dynamic values from the props.

OK. We would support some static functions, types, UI components exported from the SDK package.

Editor.registerWidget({
  name: 'myRichText',
  component: RichTextEditor,
});

Oh, you are right. We should register the widgets by the editor instead of the Registry which is from the runtime module. But maybe we should add a version field as its namespace?

In addition, maybe we shouldn't export the Editor to widget developers to register their widget? Because it would give the great power to the developers to do anything on the Editor. Maybe the widget developer should export their widget components and we would register them in the editor is better?

And I think the widget mapping API is a shortcut

The widget mapping API is provided by the widgetOptions field which depends on how the widget developers design their widget's API.

If the other widget developers want to do more complex transformations for their properties, they can also import the widget component and use it to develop a new widget.

const { kit, value, onChange } = props;

Does it wrong in the second example? The kit may import from the SDK package instead of taking it from props.

@Yuyz0112
Copy link
Contributor

Yuyz0112 commented Mar 1, 2022

Does it wrong in the second example? The kit may import from the SDK package instead of taking it from props.

Yes, updated.

In addition, maybe we shouldn't export the Editor to widget developers to register their widget? Because it would give the great power to the developers to do anything on the Editor. Maybe the widget developer should export their widget components and we would register them in the editor is better?

Agree, it's just a simple example in my code.

The widget mapping API is provided by the widgetOptions field which depends on how the widget developers design their widget's API.

Got it, sorry for the misunderstanding.

@MrWindlike
Copy link
Contributor Author

MrWindlike commented Mar 1, 2022

OK. I'm starting to implement it and I will do these jobs:

  • Adding new @sunmao-ui/editor-sdk package
  • Adding ChakraUI components into the kit object and exporting it
  • Changing all the form widgets depend on this proposal and moving them to the @sunmao-ui/editor-sdk package

@MrWindlike MrWindlike self-assigned this Mar 1, 2022
@Yuyz0112
Copy link
Contributor

Yuyz0112 commented Mar 1, 2022

Sounds good. @tanbowensg @xzdry please confirm the latest design before @MrWindlike starts to implement it.

@MrWindlike MrWindlike mentioned this issue Mar 2, 2022
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
editor feature New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants