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>
  );
};
相关推荐
passerby60612 分钟前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 分钟前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅34 分钟前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 小时前
jwt介绍
前端
爱敲代码的小鱼1 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc