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

Meson Form 功能整理 #159

Open
tiye opened this issue Sep 8, 2020 · 0 comments
Open

Meson Form 功能整理 #159

tiye opened this issue Sep 8, 2020 · 0 comments

Comments

@tiye
Copy link
Collaborator

tiye commented Sep 8, 2020

Meson Form 的目标是提供一个 JSON 化配置的表单组件. 通过配置提供基本的表单布局和校验功能.
此外, 提供一些方便使用的工具, 比如 Hooks API, Modal 集成, 注册类型等等.
按照设计, Meson Form 基于 TypeScript, 尽量多提供类型提示方便开发使用.

本文整理的功能, 大部分可以在 Demo 页面找到和试用:
http://fe.jimu.io/meson-form/

基础功能

主要分为 Label 和布局, 基础控件类型, 校验逻辑.

控件方面提供若干类型:

  • custom 自定义 render 方案
  • custom-multiple 同时对多个字段自定义 render 的方案
  • registered 外部注册的控件渲染器
  • input 输入控件
  • textarea 多行输入控件
  • number 数字控件
  • select antd 选择控件
  • dropdown-select 无 antd 的选择控件
  • tree-select antd 树形选择控件
  • dropdown-tree 无 antd 的树形选择控件
  • switch antd Switch 控件
  • radio 单选控件
  • date-picker 日期选择控件
  • decorative 纯粹装饰性的方案
  • nested 嵌套项
  • group 合并为组进行控制

其中 decorative nested group 主要是布局相关的. 其他是输入控件.
自定义渲染一般就是用 custom 类型, 然后同 render: () => ... 进行渲染和控制.

定义表单主要以数组的形式进行:

let formItems: IMesonFieldItem[] = [
  {
    type: 'input',
    name: "name",
    label: "名字",
    required: true,
  },
  {
    type: 'select',
    name: "city",
    options: selectItems,
    label: "城市",
  },
];

基于 TypeScript 的类型, type 字段指定之后会激活类型提示,
不同的类型包含的参数稍有不同. 有几个参数算是比较常用的:

  • required, 字段是否必填, 为 true 时会在 Label 上有 * 提示, 并激活校验.
  • shouldHide: (...) => ... 返回 true 时隐藏当前项, 并不进行校验
  • onlyShow: (...) => ..., 仅在返回 true 时显示当前项, 并参与校验
  • className, style, 等

使用的时候可以用 Hooks API 的方式引用:

let fieldsPlugin = useMesonFields({
  initialValue: {},
  items: formItems,
  onSubmit: (form) => {
    console.log('After validation:', form);
  },
});

// ReactNode
fieldsPlugin.ui

// 校验提交
fieldsPlugin.checkAndSubmit()

// 重置表单状态
fieldsPlugin.resetForm(data)

用 Hooks API 的写法, 得到的只是表单项的部分, 需要自己提供按钮并绑定操作.

或者以组件形式调用. 这时组件内置提交按钮, 不过这种写法不适合自定义 footer 的情况,

<MesonForm
  initialValue={form}
  items={formItems}
  onSubmit={(form) => {
    setForm(form);
  }}
/>;

此外对于整体的 Modal/Drawer 样式的表单, 也可以直接整个调用:

<MesonFormModal
  centerTitle
  title={"DEMO form in modal"}
  visible={formVisible}
  onClose={() => {
    setFormVisible(false);
  }}
  items={formItems}
  initialValue={form}
  labelClassName={styleLabel}
  onSubmit={(form) => {
    setFormVisible(false);
    setForm(form);
  }}
/>

校验

校验部分, 目前的设计比较简单, 直接通过函数进行判断,
函数返回 undefined 表示没有错误, 返回 string 表示错误提示:

{
  type: 'input',
  name: "phone",
  label: "手机号",
  fullWidth: true,
  validator: (x: string, item, form) => {
    if (x == null) {
      return "empty";
    }
    if (x.length < 5) {
      return "Phone number too short, at least 5";
    }

    return null;
  },
}

校验逻辑, 一般在控件的 blur 事件时对于单个项触发.
另外在 .checkAndSubmit(...) 的时候会对所有的表单项进行校验.
如果校验通过, 会触发 onSubmit, 否则, 就不会触发.

除了 validator, 还有一个 validateRules 属性可以用于指定校验规则,
比如要校验一个数字, 不能为空, 大于 10:

  validateRules: {
    type: "number",
    failText: "不可以为空",
    next: [
      { type: "max", n: 10, failText: '最大不超过 10' }
    ]
  }

具体到校验逻辑要参考 ruled-validator 模块的文档:
https://github.com/jimengio/ruled-validator/blob/master/README.md

自定义渲染

经常会出现内置组件不足以满足需求的情况, 一般就是用 custom 类型进行渲染.
自定义的组件, 需要对 onChangeonCheck 进行适当的响应:

let formItems: IMesonFieldItem[] = [
  {
    type: 'custom',
    name: "x",
    label: "自定义",
    render: (value, onChange, form, onCheck) => {
      return (
        <div className={row}>
          <div>
            Custom input
            <Input
              onChange={(event) => {
                onChange(event.target.value);
              }}
              placeholder={"Custom field"}
              onBlur={() => {
                onCheck(value);
              }}
            />
          </div>
        </div>
      );
    },
  },
];

扩展功能

数据关联操作

偶尔会有需求, A 字段修改时, 需要对 B 字段也进行修改.
这时候可以借助表单项的 onChange 方法来进行处理,
其中可以访问到 form 的数据, 以及对 form 内部数据进行一些修改:

  {
    type: 'select',
    name: "kind",
    label: "种类",
    options: candidates,
    required: true,
    onChange: (v: string, modifyForm: FuncMesonModifyForm<IHome>, formInternals) => {
      console.log("frozen form", formInternals.formData);

      if (v === "local") {
        formInternals.updateForm((form) => {
          form.place = "上海市";
        });
        formInternals.updateErrors((errors) => {
          errors.place = null;
        });
      } else {
        formInternals.updateForm((form) => {
          form.place = "";
        });
      }
    },
  },

注意, 关联操作直接修改数据, 并不会触发校验, 需要的话还得手动处理 .updateErrors(...)

Label 的配置

有些情况需要隐藏 Label, 可以通过 label: "" 显示空的 Label,
或者直接通过 hideLabel: true 将整个 Label 区域进行隐藏,

{
  type: 'input',
  name: "hideLabel1",
  label: "hideLabel(true)",
  hideLabel: true,
},

另外通过 labelClassName 属性, 也可以在一定程度对 Label 样式进行自定义.

异步校验

通过 asyncValidator 可以支持简单的异步校验.

{
  type: "input",
  name: "familyName",
  label: "姓",
  required: true,
  asyncValidator: async (x, item, form) => {
    await new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, 1000);
    });
    if (x?.match(/d/)) {
      return "should not have digits in name";
    }
  },
},

注意, 点击提交按钮时, 没有实现异步校验, 这时候就依赖后端进行处理了.

Input 后缀支持

对于 inputnumber 类型, 支持在控件之后强行插入节点:

  {
    type: "input",
    name: "name",
    label: "名字",
    required: true,
    suffixNode: <span>TODO</span>,
  },
  {
    type: "number",
    name: "count",
    label: "数量",
    required: true,
    suffixNode: <span>TODO</span>,
  },

服务端校验

onSubmit 方法带着一个 onServerErrors 参数, 是一个函数.
这个函数的作用是将服务端提供的校验结果, 设置到表单对应的表单项位置,
参数大致就是 Record<string, string> 的类型, 注意字段要跟表单项对应:

<MesonForm
  // ...
  onSubmit={(form: T, onServerErrors: (x: IMesonErrors<T>) => void) => {
    props.onSubmit(form, onServerErrors);
    onClose();
  }}
/>

这个功能是需要服务端进行配合的, 报错要精确到字段才行.

多个字段自定义渲染

有时候 custom 类型对应的控件可能涉及到操作多个数据,
这时可以用 custom-multiple 类型, 这是对应到多个字段的,
注意校验函数这时候也要对应做一下处理, 对应这几个字段:

  {
    type: 'custom-multiple',
    names: ["a", "b"],
    label: "自定义",
    required: true,
    validateMultiple: (form) => {
      return {
        a: form.a ? null : "a is required",
        b: form.b ? null : "b is required",
      };
    },
    renderMultiple: (form, modifyForm, checkForm) => {
      return (
        // ...
      );
    },
  },

这个例子比较长, 完整例子要看文档了.
http://fe.jimu.io/meson-form/#/custom-multiple
这个写法开放的权限是挺大的, 需要自己控制避免内部逻辑出现冗余.

注册渲染器

Meson Form 提供了一个业务扩展功能, 允许项目中注册组件, 用于快速调用:

registerMesonFormRenderer("my-input", (value, onChange, onCheck, form, options: { placeholder: string }, fieldItem) => {
  return (
    <div className={rowMiddle}>
      my global input
      <Space width={8} />
      <Input
        value={value}
        style={{ width: 200 }}
        placeholder={options.placeholder}
        onChange={(event) => {
          onChange(event.target.value);
        }}
        onBlur={() => {
          onCheck(value);
        }}
      />
      <Space width={8} />
      with texts
    </div>
  );
});

注册之后, 当表单需要渲染这个控件, 就可以通过 renderType renderOptions 传入参数调用:

let formItems: IMesonFieldItem[] = [
  {
    type: "registered",
    name: "name",
    label: "名字",
    required: true,
    renderType: "my-input",
    renderOptions: {
      placeholder: "name field",
    },
  },
];

快速触发校验, 快速触发提交

特殊场景, 需要在每次输入时触发校验, 可以开启 checkOnChange 选项,

{
  type: 'input',
  name: "phone",
  label: "手机号",
  fullWidth: true,
  checkOnChange: true,
  validator: (x: string, item, form) => {
    if (x == null) {
      return "empty";
    }
    if (x.length < 5) {
      return "Phone number too short, at least 5";
    }

    return null;
  },
}

特殊场景, 需要在每次完成一项输入时触发校验, 可以开启 submitOnEdit 选项,

<MesonForm
  items={formItems}
  initialValue={form}
  onSubmit={(form) => {
    setForm(form);
  }}
  submitOnEdit
  renderFooter={() => null}
/>

横向布局

某些较为复杂的表单中, 部分表单项会合并在一行横向进行排列,
这时可以通过 group 类型来标记部分在一行内的表单项,
然后通过 itemWidth 开控制子元素的宽度, 进行三列或者四列的布局:

  {
    type: "group",
    horizontal: true,
    itemWidth: 200,
    children: [
      {
        type: "input",
        label: "1-1",
        name: "1-1",
      },
      {
        type: "input",
        label: "1-2",
        name: "1-2",
      },
      {
        type: "input",
        label: "1-3",
        name: "1-3",
      },
      {
        type: "input",
        label: "1-4",
        name: "1-4",
      },
    ],
  },

不完整功能和替代方案, 以及改进方向

整体的异步校验

前面说的异步校验, 在 onSubmit 时不进行处理的问题,
目前的考虑是, 异步校验行为较为复杂, 而且业务逻辑不确定, 赞不做深入的处理,
需要业务方使用 onServerErrors 自己进行处理, 具体也看后端提供怎样的支持.

组件主题修改

目前组件通过 emotion 控制样式, 外部定制样式主要靠暴露的 className 插口来进行.
对于组件整体各处的样式, 需要通过 attachMesonFormThemeVariables(...) 进行.
这个方案比较可控, 但是如果后续出现场景要深度定制组件样式, 需要到时进行方案修改.

Filter Form 和 inline 模式

目前组件提供的 From 主要是纵向进行布局的,
对于业务可能出现的横向布局的 Form, 有提供 MesonInlineForm 做尝试,
但是 InlineForm 收到局限比较多, 目前很少用到, 样式也比较难定制:

<MesonInlineForm
  initialValue={form}
  items={formItems}
  onSubmit={(form) => {
    setForm(form);
  }}
  submitOnEdit={true}
/>

另一种较为常见的 inline 的情形是过来配合表格使用的过滤表单,
不过这个过滤表单一般是不进行校验的, 所以另外提供了一个接口:

let filterForm = useFilterForm({
  items: filterFormItems,
  onItemChange: (result) => {
    console.log(result)
  },
});

完全自定义 UI

特殊的情况, 比如样式跟内置组件都对不上, 只是需要校验方案的话,
Meson Form 提供 useMesonCore API, 这个只暴露数据和校验相关的功能,

let { formAny, errors, onCheckSubmit, checkItem, updateItem, updateForm, updateErrors } = useMesonCore({
  initialValue: {},
  items: formItems,
  onSubmit: (form) => {
    // console.log(form)
  },
});

拿到 formAny onCheckSubmit 之后, 可以自己渲染和绑定 UI.
这应该是非常深度自定义的一个功能, 应该很少用到, 除非是极端情况.
这个作为内部 API, 后续可能看情况做调整.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant