阅读 366

formily的几个核心概念

1、JOSN Schema描述

formily 是阿里推出的一套动态化表单的解决方案,用于解决传统模式下写动态表带时代码冗余大、性能低、可维护性差的问题,formily 表单采用标准的 JSON Schema 进性描述,可简单的理解为规范化的 JSON 用于描述 form 表单,比如,下面几个字段规范的定义:

type: 字段的数据类型,可以是简单或者复杂数据类型; properties:对象属性,通俗用于对象嵌套描述; x-rules: 字段校验属性,Array类型,支持通用的必填、正则校验、函数校验以及错误信息提示; x-component:字段组件属性,可注入对于的表单组件,相当于FormItem,比如Input、Select等,也可以是CustomComponent,通过渲染层注入组件即可; x-component-props:用于x-component中指定的组件的属性,相当于FormItem的属性。

(因文章篇幅有限,这里仅列举部分伪代码,详细代码见文章底部链接)

一个简单的 Formily 表单可写成如下:

// jsx
<SchemaForm
  labelCol={24}
  wrapperCol={24}
  components={{
    Input,
    Select,
    CheckboxGroup: Checkbox.Group,
    RadioGroup: Radio.Group,
    RangePicker: DatePicker.RangePicker,
    Upload
  }}
  schema={simpleSchema}
  onSubmit={(values) => {
    console.log(values);
  }}
>
  <FormButtonGroup offset={0}>
    <Submit>查询</Submit>
    <Reset>重置</Reset>
  </FormButtonGroup>
</SchemaForm>
复制代码

Schema 文件描述:

// schema
export const simpleSchema = {
  type: "object",
  properties: {
    input: {
      type: "string",
      title: "输入",
      required: true,
      "x-component": "Input",
      "x-component-props": {
        placeholder: "请输入"
      }
    },
    select: {
      type: "number",
      title: "下拉选",
      required: true,
      "x-component-props": {
        placeholder: "请选择"
      },
      enum: [
        { label: "选项一", value: 1 },
        { label: "选项二", value: 2 }
      ],
      "x-component": "select"
    },
    radio: {
      title: "单选",
      "x-component": "RadioGroup",
      enum: ["1", "2", "3", "4"]
    },
    checkbox: {
      title: "复选",
      "x-component": "CheckboxGroup",
      enum: [
        { label: "One", value: 1 },
        { label: "Two", value: 2 },
        { label: "Three", value: 3 }
      ]
    },
    dateRange: {
      type: "object",
      title: "时间范围",
      required: true,
      properties: {
        "[start,end]": {
          // title: "RangePicker",
          "x-component": "RangePicker",
          "x-component-props": {
            placeholder: ["开始时间", "结束时间"]
          }
        }
      }
    },
    upload: {
      type: "array",
      title: "图片",
      "x-component-props": {
        listType: "picture-card",
        action: "https://www.mocky.io/v2/5cc8019d300000980a055e76"
      },
      "x-component": "upload",
      description: "仅支持图片类数据上传"
    }
  }
};
复制代码

2、表单的生命周期/状态

在formily中,一切的联动操作都源自生命周期函数,可分为表单的生命周期函数和表单字段的操作,在表单的 effects 中实现对于的逻辑操作:

// jsx
<SchemaForm
  labelCol={24}
  wrapperCol={24}
  components={{
    Input,
    Select,
  }}
  schema={basicSchema}
  actions={basicAction}
  effects={($, { setFieldState }) => {
    $("onFieldValueChange", "classType").subscribe((parentState) => {
      setFieldState("currentToggle", (state) => {
        state.visible = parentState.value;
      });
      setFieldState("currentStatus", (state) => {
        state.value = parentState.value ? "显示" : "隐藏";
      });
    });
  }}
  onSubmit={(values) => {
    console.log(values);
  }}
>
  <FormButtonGroup offset={0}>
    <Submit>查询</Submit>
    <Reset>重置</Reset>
  </FormButtonGroup>
</SchemaForm>
复制代码

Schema 文件描述:

// schmema
export const basicSchema = {
  type: "object",
  properties: {
    classType: {
      type: "number",
      enum: [
        {
          label: "显示",
          value: 1
        },
        {
          label: "隐藏",
          value: 0
        }
      ],
      title: "联动①",
      required: true,
      default: 1,
      "x-component": "select",
      description: "利用生命周期做联动"
    },
    currentToggle: {
      type: "string",
      title: "联动①组件",
      required: true,
      "x-component": "Input"
    },
    currentStatus: {
      type: "string",
      title: "联动①状态",
      required: true,
      "x-component": "Input"
    },
  }
};
复制代码

如上述案例所示,我们在 SchemaForm 的 effects 中通过订阅生命周期函数来监听字段状态的变化,从而达到表单联动的效果;以上是一种写法,触发生命周期还有另一种写法,通过解构出 FormEffectHooks 对象:

// jsx
import { FormEffectHooks, createFormActions } from '@formily/next'
const { onFieldValueChange$, onFormInit$ } = FormEffectHooks
const { setFieldState, getFieldState } = createFormActions()

// 表单初始化完成后,执行将字段aa的值修改为123
onFormInit$().subscribe(() => {
  setFieldValue('aa', 123)
})

// 当字段bb的值发生变化后,修改字段cc的显示隐藏状态
onFieldValueChange$('bb').subscribe( fieldState => {
  // fieldState为bb的当前状态值
  setFieldState('cc', state => {
    // state为cc的当前状态值,根据字段bb的值是否为123来决定cc的隐藏属性。
    state.visible = fieldState.value === 123
  })
})
复制代码

表单的生命周期函数有很多种,详见官方文档,一些常用的生命周期函数如下:

常量名 常量值 描述 Hook 返回值
ON_FORM_SUBMIT onFormSubmit 表单提交时触发 onFormSubmit$ FormState
ON_FORM_RESET onFormReset 表单重置时触发 onFormReset$ FormState
ON_FIELD_CHANGE onFieldChange 字段状态发生变化时触发 onFieldChange$ FieldState
ON_FIELD_INPUT_CHANGE onFieldInputChange 字段输入事件触发时触发 onFieldInputChange$ FieldState
ON_FIELD_VALUE_CHANGE onFieldValueChange 字段值变化时触发 onFieldValueChange$ FieldState

3、表单操作actions/effects

// jsx
// 片段一
$("onFieldValueChange", "classType").subscribe((parentState) => {
  setFieldState("currentToggle", (state) => {
    state.visible = parentState.value;
  });
});
// 片段二
onFieldValueChange$('bb').subscribe( fieldState => {
  setFieldState('cc', state => {
    state.visible = fieldState.value === 123
  })
})
复制代码

上面只演示了两段伪代码片段,详情功能可参考上一小节表单的生命周期的代码,我们只需要知道:所有的联动操作都需要在effects实现,而操作Form API都通过actions来控制,详细接口参考文档链接

4、表单的路径系统

表单的路径系统相当于CSS中的选择器,可以通过路径系统来匹配需要操作的字段;这里的匹配方式大概可分为两种,一种是通配符匹配,另一种是target目标匹配。

4.1 通配符匹配

// 通配符匹配 匹配array字段下任意字段之后是aa的字段(array -> 任意 -> aa)
onFieldValueChange$('array.*.aa').subscribe((parentState) => {
  // ...
})
// 通配符匹配 当aa字段值改变后匹配所有的bb、cc、dd字段
onFieldValueChange$('aa').subscribe(({ name, value }) => {
  setFieldState('*(bb,cc,dd)', state => {
    state.visible = value
  })
})
复制代码

4.2 target目标匹配

target 相邻查找

  • prevPath.[].fieldName代表当前行字段
  • prevPath.[+].fieldName代表下一行字段
  • prevPath.[-].fieldName代表上一行字段
  • prevPath.[+2].fieldName代表下下一行字段
  • prevPath.[-2].fieldName代表上上一行字段

一次可以继续往下递增或者递减

target 向前路径查找

  • .path.a.b代表基于当前字段路径往后计算
  • ..path.a.b代表往前计算相对路径
  • ...path.a.b代表继续往前计算相对路径

以此类推

5、x-linkages属性简单联动

上面说到,一切的联动都源自生命周期,而 x-linkages 用于在协议层描述简单联动,注意,这个只是简单联动,它无法描述异步联动,也无法描述联动过程中的各种复杂数据处理,以下是一个简单的联动案例。

<SchemaForm
  labelCol={24}
  wrapperCol={24}
  components={components}
  schema={basicSchema}
  actions={basicAction}
  onSubmit={(values) => {
    console.log(values);
  }}
>
  <FormButtonGroup offset={0}>
    <Submit>查询</Submit>
    <Reset>重置</Reset>
  </FormButtonGroup>
</SchemaForm>
复制代码

Schema描述文件

export const basicSchema = {
  type: "object",
  properties: {
    linkTwo: {
      type: "number",
      enum: [
        {
          label: "联动②显示",
          value: 1
        },
        {
          label: "联动②隐藏",
          value: 0
        },
        {
          label: "联动②控制schema的title字段",
          value: 3
        }
      ],
      title: "联动②",
      required: true,
      default: 1,
      "x-component": "select",
      "x-linkages": [
        {
          type: "value:visible",
          target: "linkTwoEle",
          condition: "{{ $value === 1 || $value === 3 }}"
        },
        {
          type: "value:schema",
          target: "linkTwoEle",
          condition: "{{ $value === 3 }}", //当值为3时发生联动
          schema: {
            title: "这是联动的标题"
          }
        }
      ],
      description:
        "利用 x-linkages 属性做联动,这个只是简单联动,它无法描述异步联动,也无法描述联动过程中的各种复杂数据处理。"
    },
    linkTwoEle: {
      type: "object",
      title: "联动②组件",
      required: true,
      properties: {
        "[start,end]": {
          // title: "RangePicker",
          "x-component": "RangePicker",
          "x-component-props": {
            placeholder: ["开始时间", "结束时间"]
          }
        }
      }
    },
  }
};
复制代码

这里 link 联动的 Type 类型主要有三种:

  • value:state,由值变化控制指定字段的状态
  • value:visible,由值变化控制指定字段显示隐藏
  • 相当于 value:state 的一种特例情况,即 state.visible
  • value:schema,由值变化控制指定字段的 schema. 相当于 value:state 的一种特例情况,即 state.props

condition 为link联动触发的条件,target 为上小节描述的target目标匹配。

6、表单的扩展机制(自定义生命周期、自定义扩展状态、自定义校验规则、自定义组件)

6.1 自定义生命周期: 自定义事件派发

自定义事件大概可分为两种:一是通过 createFormActions 全局派发事件,二是在 effects 逻辑联动中通过 notify 来派发事件。

const extendAction = createFormActions();
// 全局自定义事件
extendAction.dispatch("customEvent1", { value: 666, text: "全局的payload" });

const myFormily = () => (
  <SchemaForm
    labelCol={24}
    wrapperCol={24}
    components={components}
    schema={extendSchema}
    actions={extendAction}
    effects={($, { notify }) => {
      // effect派发自定义事件
      $("onFieldValueChange", "a2").subscribe((parentState) => {
        notify("customEvent2", parentState);
      });
    }}
    onSubmit={(values) => {
      console.log(values);
    }}
  >
    <FormButtonGroup offset={0}>
      <Submit>查询</Submit>
      <Reset>重置</Reset>
    </FormButtonGroup>
  </SchemaForm>
);

export default myFormily;
复制代码

派发事件之后,需要在自定义组件内通过 useFormEffects 监听自定义事件

// 组件内通过 useFormEffects 监听自定义事件
useFormEffects(($, { notify, setFieldState, getFieldState }) => {
  $("customEvent1").subscribe((payload) => {
    console.log(payload);
  });
  $("customEvent2").subscribe((payload) => {
    console.log(payload);
  });
});
复制代码

6.2 自定义扩展状态:自定义formily组件状态

表单的自定义状态在自定义组件中使用的比较多,类似于 react Hook 的形式,仅有两种形式:

import { useFieldState, useFormState } from '@formily/next';

//为当前组件对应的字段中添加自定义的状态字段extendState1和extendState2.
const [state, setFieldState] = useFieldState({
  extendState1: 'something',
  extendState2: 'something'
})
//为当前组件所在的表单中添加自定义的状态字段extendState1和extendState2.
const [formState, setFormState] = useFormState({
  extendState1: 'something',
  extendState2: 'something'
})
复制代码

自定义状态字段和系统提供的状态字段一致,自定义状态改变会也触发 onFieldChange 或 onFormChange 事件。

6.3 自定义组件: 自定义字段组件和虚拟布局组件

Formily 可以通过自定义组件来满足更加复杂的业务需求,通过给组件添加 isFieldComponent 属性即可,一个简单的字段组件如下:

import React from "react";
import { useFormEffects, useFieldState } from "@formily/antd";

const CustomFieldComponent = (props) => {
  const { value, schema, className, editable, path, mutators, form } = props;

  //获取”x-component-props"的属性值
  const componentProps = schema.getExtendsComponentProps() || {};

  return (
    <div>
      <h3 style={{ fontSize: 14 }}>这是自定义字段组件描述</h3>
      <input
        value={value || ""}
        onChange={(e) => mutators.change(e.target.value)}
      />
    </div>
  );
};

CustomFieldComponent.isFieldComponent = true;

export default CustomFieldComponent;
复制代码

当然,Formily 也提供 registerVirtualBox 方法自定义虚拟组件,主要用于表单布局方面:

// 注册virtual组件 一般用于布局
registerVirtualBox("CustomLayout", ({ children, schema }) => {
  return (
    <div style={{ border: "1px solid red", padding: 10 }}>
      {children}
      {schema["x-component-props"]["say"]}
    </div>
  );
});
复制代码

组件定义好后,可以通过局部注册、全局注册、全局批量扩展三种方式注入到Formily表单系统中

//局部实例注册
<SchemaForm components={{ CustomComponent, CustomFieldComponent }}/>

//全局注册
registerFormField('CustomComponent2', connect()(CustomComponent))

//全局批量扩展
registerFormFields({ CustomComponent3: connect()(CustomComponent) })
复制代码

6.4 自定义校验:自定义x-rules校验、自定义函数验证

在校验中,Formily也提供两种校验方式,一种是直接在schema中定义 x-rules 校验,另一种是通过自定义校验函数来校验,后一种方式常用于校验函数复用。

import {
  registerValidationRules
} from "@formily/antd";

// 自定义函数校验
registerValidationRules({
  customRule2: (value) => {
    return value === "123" ? "不能等于123" : "";
  }
});
复制代码

Schema描述文件

export const extendSchema = {
  type: "object",
  properties: {
    a1: {
      type: "number",
      title: "x-rules校验",
      required: true,
      "x-component": "input",
      "x-rules": {
        validator: (value) => {
          return value === "123" ? "不能等于123" : "";
        }
      }
    },
    a3: {
      type: "string",
      title: "自定义函数校验",
      required: true,
      "x-component": "Input",
      "x-rules": {
        customRule2: true
      }
    },
  }
};
复制代码

github案例详见 github仓库

在线演示案例详见 codesandbox案例

文章分类
前端
文章标签