formily2.x 在react中使用JSON Schema模式创建复杂表单

业务场景中的表单开发,比较难实现的有两种:表单联动和可编辑表格。在antd中有shouldUpdate可以被动模型处理表单联动,有useWatch可以主动模型处理表单联动(联动模型概念);proComponents中EditableProTable组件可以快速实现可编辑表格。这两个方案是我没接触formily2.x之前经常使用的。

在使用了formily2.x后,这两个方案被抛弃了。第一是因为前面的两种方案都是以antd组件为基础实现的,且可编辑表格必须在react生态中使用,而formily2.x可以在vue或react或原生js中使用,并且官方已经支持antd、element、next、vant等UI框架,没有官方支持的UI框架也可以自己写一些桥接层去使用组件,使用方便。它专注于解决表单的方案,阿里团队开源,好用且坚挺,出来已经有3、4年了,它统共有三种开发方式,纯Jsx模式, Markup Schema模式和JSON Schema模式(三种模式介绍),下面的示例使用JSON Schema模式和antd5组件且主要使用被动模式,formilyjs中相关文档:Schema字段文档antd5组件文档

schema常用字段名:

字段名 含义
type 类型
properties 属性描述
x-decorator 表单字段 UI 包装器组件(类似于antd中的FormItem,在使用中也可直接使用formily/antd-v5的 FormItem组件)
x-decorator-props 表单字段 UI 包装器组件上的属性,实际开发中没啥用,x-visible、x-hidden、x-dispaly、x-visible、title、required已经代替FormItem组件上的属性
x-validator 字段校验器
title 表单字段名标题
x-value 初始值
x-visible 控制表单字段显隐,隐藏时提交表单时不存在
x-hidden 控制表单字段显隐,隐藏时提交表单时还是存在
x-dispaly 没啥用,上面两个够用了
required 透出标题的必填标识
x-component 表单字段 UI 组件(createSchemaField引入哪个组件库就可以在内部使用)
x-component-props 表单字段 UI 组件上的属性(UI组件上有的属性的都可以用)
x-reactions 表单字段联动协议
$self 当前字段自身实例
$deps 只能在 x-reactions 中的表达式使用,与 x-reactions 定义的 dependencies 对应,能获取到依赖项相应的值
表单联动

通过dependencies做依赖收集,依赖更改时fulfill中条件满足则执行fulfill内schema中操作

js 复制代码
import { FormButtonGroup, FormItem, Input, Submit } from '@formily/antd-v5';
import { createForm } from '@formily/core';
import { FormProvider, ISchema, createSchemaField } from '@formily/react';
import React from 'react';

const SchemaField = createSchemaField({
  components: {
    Input,
    FormItem,
  }, //components组件注入后可以在schema中使用
});

const form = createForm(); //创建表单实例

const schema: ISchema = {
  type: 'object',
  properties: { //在下方注入当前需要的字段
    input: {
      type: 'string',
      title: '输入框',
      'x-decorator': 'FormItem',
      'x-component': 'Input',
      required: true,
      'x-component-props': {
        style: {
          width: 240,
        },
      },
    },
    textarea: {
      type: 'string',
      title: '文本框',
      required: true,
      'x-reactions': {
        dependencies: ['input'], //联动依赖项的字段
        fulfill: { //fulfill有三种模式,实际上直接用schema即可,但有依赖项的字段更改,schema会被fulfill中schema替换
          schema: {
            'x-visible': "{{$deps[0] === '123'}}", //$deps为dependencies数组的字段值
            //很多时候表单联动也只会跟其他表单项值的更改有关,直接使用这个是很方便的
            'x-component-props.style.color':
              "{{$self.value === '123' ? 'red' : 'blue'}}", //$self为本字段的实例,可以根据值做当前字段自身的修改
          },
        },
      },
      'x-decorator': 'FormItem',
      'x-component': 'Input.TextArea',
      'x-component-props': {
        style: {
          width: 400,
        },
      },
    },
  },
};

const Demo: React.FC = () => {
  return (
    <FormProvider form={form}>
      <SchemaField schema={schema} />
      <FormButtonGroup>
        <Submit onSubmit={console.log}>提交</Submit>
      </FormButtonGroup>
    </FormProvider>
  );
};

export default Demo;
自定义组件

类似于antd中form组件中编写自定义组件的操作,打印props可以看见onChange和value,通过onChange可以给value赋予新值实现自定义组件

js 复制代码
import { FormButtonGroup, FormItem, Input, Submit } from '@formily/antd-v5';
import { createForm } from '@formily/core';
import { FormProvider, ISchema, createSchemaField } from '@formily/react';
import { Input as InputOfAntd } from 'antd';
import React from 'react';

const IncreaseOrDecrease = (props) => {
console.log(props,'props')
  const changeNum = (type) => {
    if (type === 'add') {
      return props.onChange(Number(props.value) + 1);
    }
    props.onChange(Number(props.value) - 1);
  };

  return (
    <div
      style={{
        display: 'flex',
      }}
    >
      <div
        onClick={() => {
          changeNum('add');
        }}
      >
        +
      </div>
      <InputOfAntd value={props.value} style={{ width: 400 }}></InputOfAntd>
      <div
        onClick={() => {
          changeNum('reduce');
        }}
      >
        -
      </div>
    </div>
  );
};

const SchemaField = createSchemaField({
  components: {
    Input,
    FormItem,
    IncreaseOrDecrease, //注入自定义组件
  },
});

const form = createForm();

const schema: ISchema = {
  type: 'object',
  properties: {
    input: {
      type: 'number',
      title: '增减',
      required: true,
      'x-value': 0, //初始值
      'x-decorator': 'FormItem',
      'x-component': 'IncreaseOrDecrease', //引入自定义组件
    },
  },
};

const Demo: React.FC = () => {
  return (
    <FormProvider form={form}>
      <SchemaField schema={schema} />
      <FormButtonGroup>
        <Submit onSubmit={console.log}>提交</Submit>
      </FormButtonGroup>
    </FormProvider>
  );
};

export default Demo;
异步数据源

复杂案例: 复杂的省市联动案例异步联动数据源

x-reactions中写函数获取异步数据源,scope可以注入其他函数给当前schema中使用,$self可以给当前字段使用

js 复制代码
import { FormButtonGroup, FormItem, Select, Submit } from '@formily/antd-v5';
import { Field, createForm } from '@formily/core';
import { FormProvider, ISchema, createSchemaField } from '@formily/react';
import { action } from '@formily/reactive';
import React from 'react';

const SchemaField = createSchemaField({
  components: {
    Select,
    FormItem,
  },
});

const loadData = async (field: Field) => {
  const value = field.value;
  return fetch(
    `https://api.oioweb.cn/api/search/taobao_suggest?keyword=${value}`,
  )
    .then((response) => response.json())
    .then((res: any) => {
      return res?.result.map((item) => {
        return {
          label: item[0],
          value: item[1],
        };
      });
    })
    .catch(() => []);
};

const useAsyncDataSource = (service: typeof loadData) => (field: Field) => { //field即为当前字段实例
  field.loading = true; //加上加载态
  service(field).then(
  //需要使用action.bound在异步更新数据后二次更新field实例(https://reactive.formilyjs.org/zh-CN/api/action)
    action.bound?.((data) => {
      field.dataSource = data;
      field.loading = false;
    }),
  );
};

const onSearchValue = (searchValue, $self) => {
  $self.value = searchValue; //给当前字段的值赋值
};

const form = createForm();

const schema: ISchema = {
  type: 'object',
  properties: {
    select: {
      type: 'string',
      title: '异步选择框',
      'x-decorator': 'FormItem',
      'x-component': 'Select',
      'x-component-props': {
        showSearch: true,
        filterOption: false,
        onSearch: '{{(value)=>onSearchValue(value, $self)}}', //$self可以在字段描述中任意处使用
        style: {
          width: 120,
        },
      },

      'x-reactions': ['{{useAsyncDataSource(loadData)}}'], //field即为当前字段实例
    },
  },
};

const Demo: React.FC = () => {
  return (
    <FormProvider form={form}>
      <SchemaField
        schema={schema}
        scope={{ useAsyncDataSource, loadData, onSearchValue }} //注入方法
      />
      <FormButtonGroup>
        <Submit onSubmit={console.log}>Submit</Submit>
      </FormButtonGroup>
    </FormProvider>
  );
};

export default Demo;
可编辑表格

ArrayTable组件有ArrayTable.Column空节点作为表格列展示,ArrayTable.SortHandle提供拖拽能力,ArrayTable.Index提供当前行序号,ArrayTable.Addition新增一行表格,ArrayTable.Remove删除一行表格,ArrayTable.MoveDown下移一行表格,ArrayTable.MoveUp上移一行表格

js 复制代码
import {
  ArrayTable,
  FormButtonGroup,
  FormItem,
  Input,
  Submit,
  Switch,
} from '@formily/antd-v5';
import { createForm } from '@formily/core';
import { FormProvider, createSchemaField } from '@formily/react';

const SchemaField = createSchemaField({
  components: {
    FormItem,
    Switch,
    Input,
    ArrayTable,
  },
});

const form = createForm();

const schema = {
  type: 'object',
  properties: {
    array: {
      type: 'array',
      'x-decorator': 'FormItem',
      'x-component': 'ArrayTable',
      'x-component-props': {
        pagination: { pageSize: 10 },
        scroll: { x: '100%' },
      },
      items: {
        type: 'object',
        properties: {
          column1: {
            type: 'void', //空节点(不会在表单中留下)
            'x-component': 'ArrayTable.Column', //表格列
            'x-component-props': { width: 50, title: 'Sort', align: 'center' },

            properties: {
              sort: {
                type: 'void', //空节点(不会在表单中留下)
                'x-component': 'ArrayTable.SortHandle', //提供表格可拖拽的能力
              },
            },
          },
          column2: {
            type: 'void', 
            'x-component': 'ArrayTable.Column', //表格列
            'x-component-props': { width: 80, title: 'Index', align: 'center' },
            properties: {
              index: {
                type: 'void', /空节点(不会在表单中留下)
                'x-component': 'ArrayTable.Index', //表格序号
              },
            },
          },
          column3: {
            type: 'void',
            'x-component': 'ArrayTable.Column',
            'x-component-props': { width: 100, title: '显隐->A2' },
            properties: {
              a1: {
                type: 'boolean',
                'x-decorator': 'FormItem',
                'x-component': 'Switch',
              },
            },
          },
          column4: {
            type: 'void',
            'x-component': 'ArrayTable.Column',
            'x-component-props': { width: 200, title: 'A2' },
            properties: {
              a2: { 
                type: 'string',
                'x-decorator': 'FormItem',
                'x-component': 'Input',
                required: true,
                'x-reactions': [
                  {
                    dependencies: ['.a1'], //当前显隐通过a1值控制
                    when: '{{$deps[0]}}',
                    fulfill: {
                      schema: {
                        'x-visible': false,
                      },
                    },
                    otherwise: {
                      schema: {
                        'x-visible': true,
                      },
                    },
                  },
                ],
              },
            },
          },
          column5: {
            type: 'void',
            'x-component': 'ArrayTable.Column',
            'x-component-props': { width: 200, title: 'A3' },
            properties: {
              a3: { //当前可编辑表格中a3字段
                type: 'string',
                required: true,
                'x-decorator': 'FormItem',
                'x-component': 'Input',
              },
            },
          },
          column6: {
            type: 'void',
            'x-component': 'ArrayTable.Column',
            'x-component-props': {
              title: 'Operations',
              dataIndex: 'operations',
              width: 200,
              fixed: 'right',
            },
            properties: {
              item: {
                type: 'void',
                'x-component': 'FormItem',
                properties: {
                  remove: {
                    type: 'void', // 空节点,只操作不影响表单值
                    'x-component': 'ArrayTable.Remove', //删除
                  },
                  moveDown: {
                    type: 'void',
                    'x-component': 'ArrayTable.MoveDown', //上移
                  },
                  moveUp: {
                    type: 'void',
                    'x-component': 'ArrayTable.MoveUp', //下移
                  },
                },
              },
            },
          },
        },
      },
      properties: {
        add: {
          type: 'void',
          'x-component': 'ArrayTable.Addition', //下方的新增可编辑内容按钮
          title: '添加条目',
        },
      },
    },
  },
};

export default () => {
  return (
    <FormProvider form={form}>
      <SchemaField schema={schema} />
      <FormButtonGroup>
        <Submit onSubmit={console.log}>提交</Submit>
      </FormButtonGroup>
    </FormProvider>
  );
};
相关推荐
m0_748240251 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar1 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人1 小时前
前端知识补充—CSS
前端·css
GISer_Jing2 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245522 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v2 小时前
webpack最基础的配置
前端·webpack·node.js
pubuzhixing2 小时前
开源白板新方案:Plait 同时支持 Angular 和 React 啦!
前端·开源·github
2401_857600952 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_857600952 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL2 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js