FormRender 2.0 开箱即用表单方案,助你准点下班 🎈

48,359 阅读11分钟

官方文档:xrender.fun/form-render

github:github.com/alibaba/x-r…

本文作者飞猪前端 @lhbxs,分享 FormRender 2.0 开箱即用的表单解决方案,产品体验更上一层楼

一、前言

在前端开发过程中,表单渲染是重要且繁琐的一环。为了提高开发效率并避免重复工作,飞猪推出了基于 React 的表单渲染器 FormRender。FormRender 使用 JsonSchema 协议渲染表单,是适用于中后台表单的一种通用解决方案。本文将介绍 FormRender 的基本概念、使用方式及高级特性。

二、简介

  • FormRender 通过 JsonSchema 协议渲染表单,为中后台表单业务提供开箱即用的通用解决方案。

  • FormRender 是属于飞猪 XRender 系列的一款开源库,于 2023 年 2 月完成了版本升级并发布了相应的升级公告。当前最新版本可能已经不同,建议查看官网以获得最新信息。

  • FormRender 最近推出了适用于移动端 H5 表单的解决方案,如果你对此感兴趣,可以查看官方文档以获取详细信息。

优势

  • 协议简单:协议相对简单,并在一定程度上遵循了 JsonSchema 规范,因此相对容易上手和理解。

  • 较强的配置能力:具有较强的配置能力,可以对表单联动、校验、布局以及数据处理等方面进行配置。

  • 良好的性能体验:通过对 FormRender 进行重构,底层采用 Antd Form 来实现表单的数据收集和管控,同时针对控件渲染层面进行优化处理,从而大幅提升性能,使得在使用过程中具有良好的性能体验。

  • 内置组件丰富:内置组件非常丰富,包括基础组件、嵌套卡片类组件和动态增减 List 组件等,可以满足大多数场景的表单实现需求。

  • 扩展性强:具有非常强的扩展性,支持自定义各种类型的表单控件,用户可以根据实际需要进行定制,非常灵活。

  • 易于使用:容易上手,可以通过表单设计器可视化拖拽的方式快速生成表单。

三、如何使用

  1. 安装依赖
    npm install form-render --save

2. 使用方式


import React from 'react';
import FormRender, { useForm } from 'form-render';

const schema = {
  type: 'object',
  properties: {
    input: {
      title: '输入框',
      type: 'string'
    },
    select: {
      title: '下拉框',
      type: 'string',
      props: {
        options: [
          { label: '早', value: 'a' },
          { label: '中', value: 'b' },
          { label: '晚', value: 'c' }
        ]
      }
    }
  }
};


export default () => {
  const form = useForm();

  const onFinish = (formData) => {
    console.log('formData:', formData);
  };

  return (
    <FormRender 
      form={form}
      schema={schema}
      onFinish={onFinish}
      footer={true}
    />
  );
}

1685379199013-fa9f80b7-dab1-428d-9648-6a354a626c65.png

  1. 通过可视化表单生成器 Schemabuilder ,用户可以拖拖拽拽快速生成 JsonSchema。

image.png 4. 通过 Playground 可以进行在线体验,并查看其中丰富的表单示例。

截屏2023-06-01 下午8.48.45.png

四、高级特性

1. 表单校验

FormRender 简化了表单校验配置,常规校验可以通过协议内置字段进行配置,同时也支持像 Antd Rules 的配置方式,并且可以处理更复杂的子表单联动校验配置。

内置校验

为了简化校验逻辑的配置,FormRender 内置了一些校验配置选项,包括以下字段:

  • 必填:required
  • 最大长度、最大值:max
  • 最小长度、最小值:min
  • 字符串特殊格式:format,其中 format的格式有 url、email、image、color
  • 正则表达式:pattern
const schema = {
  "type": "object",
  "displayType": "row",
  "properties": {
    "input1": {
      "title": "必填",
      "type": "string",
      "widget": "input",
      "required": true
    },
    "input2": {
      "title": "数值大小",
      "type": "number",
      "widget": "inputNumber",
      "max": 5,
      "min": 1
    },
    "input4": {
      "title": "字符串长度",
      "type": "string",
      "widget": "input",
      "max": 20,
      "min": 5
    },
    "input6": {
      "title": "url 校验",
      "type": "string",
      "widget": "urlInput",
      "format": "url"
    }
  }
}
Rules 校验

FormRender 的 Rules 校验规则可以参考 Antd Form Rules API,validator 自定义校验规则做了一点小的调整,自定义方法的返回值类型,改成布尔值或者对象格式

validator 自定义校验

const schema = {
  type: 'object',
  displayType: 'row',
  properties: {
    input2: {
      title: '自定义校验',
      type: 'string',
      rules: [
        { 
          validator: (_, value) => {
            const pattern = '^[\u4E00-\u9FA5]+$';
            const result = new RegExp(pattern).test(value);
            return result;
            // 或者是返回一个对象,用于动态设置 message 内容
            // return {
            //   status: result,
            //   message: '请输入中文!',
            // }
          }, 
          message: '请输入中文!' 
        }
      ]
    }
  }
};

validateTrigger: 表单提交时触发校验

const schema = {
  type: 'object',
  displayType: 'row',
  properties: {
    input2: {
      title: '自定义校验',
      type: 'string',
      rules: [
        { 
          validateTrigger: 'onSubmit'
          validator: (_, value) => {
            const pattern = '^[\u4E00-\u9FA5]+$';
            const result = new RegExp(pattern).test(value);
            return result;
            // 或者是返回一个对象,用于动态设置 message 内容
            // return {
            //   status: result,
            //   message: '请输入中文!',
            // }
          }, 
          message: '请输入中文!' 
        }
      ]
    }
  }
};
子表单校验

在处理非常复杂的表单场景时,有时会使用自定义组件作为子表单。在这种情况下,表单的提交行为无法触发子表单进行校验,因此需要对自定义子组件进行特殊处理。

子表单组件提供一个触发内部校验的函数,并通过 useImperativeHandle 暴露出去

import { useImperativeHandle } form 'react';

const ChildForm = (props) => {

   // 内部校验方法,异步校验请用 async、await 语法

    const validator = async () => {
        return true; // 返回 boolean 值,true 表示内部校验通过

        // 如果需在外部显示子表单错误信息可以使用对象形式返回
        // retrun { status: boolean, message: string };
    };

    useImperativeHandle(props.addons.fieldRef, () => {
        // 将校验方法暴露出去,方便外部表单提交时,触发校验
        return {
            validator
        };
    });

    return (
        ...// 表单渲染代码
    );
}

export default ChildForm;

2. 表单联动

表单联动是前端开发中常见的一种需求,也是前端开发人员经常遇到的难点之一,往往评价一个表单渲染器能力强不强,表单联动能力至关重要。FormRender 通过函数表达式依赖项关联、watch 监听 等方式尽可能的在表单联动上做的更加易用,降低表单联动的成本。

函数表达式

函数表达式为字符串格式,以双花括号 {{ ... }}为语法特征,对于简单的联动提供一种简洁的配置方式。组件所有的 Schema 协议字段都支持函数表达式。

例如:控制表单项禁用、隐藏、文案提示等交互。

{
  "type": "object",
  "properties": {
    "input": {
      "title": "输入框",
      "type": "string",
      "widget": "input",
      "hidden": "{{ formData.hidden }}",
      "disabled": "{{ formData.disabled }}",
      "placeholder": "{{ formData.placeholder || '请输入' }}"
    },
    "placeholder": {
      "title": "修改提示信息",
      "type": "string",
      "widget": "input"
    },
    "hidden": {
      "title": "隐藏",
      "type": "boolan",
      "widget": "switch"
    },
    "disabled": {
      "title": "禁用",
      "type": "boolan",
      "widget": "switch"
    }
  }
}
  • formData 表示整个表单的值,rootValue 用于 List 的场景,表示 List.Item 的值。

除此之外,FormRender 还支持更加细粒度的配置,例如使用函数表达式来控制某个选项的行为。

{
  "type": "object",
  "properties": {
    "input1": {
      "title": "当输入框的值为 2 时,下拉单选第二个选项隐藏,如果已选中并清空选中值",
      "type": "string",
      "widget": "input"
    },
    "select1": {
      "title": "下拉单选",
      "type": "string",
      "widget": "select",
      "defaultValue": "{{ (formData.input1 === '2' && formData.select1 === 'b') ? undefined : formData.select1 }}",
      "props": {
        "options": [
          {
            "label": "早",
            "value": "a"
          },
          {
            "label": "中",
            "value": "b",
            "hidden": "{{ formData.input1 === '2' }}"
          },
          {
            "label": "晚",
            "value": "c"
          }
        ]
      }
    }
  }
}
watch 监听

Antd Form 通过 onValuesChange 事件来监听字段值的更新,而 FormRender 提供了一个功能更为强大的 API 来监听值的变化,即 watch API。下面让我们深入了解其用法。

const watch = {
  // '#' 等同于 onValuesChagne
  '#': (allValues, changedValues) => { 
    console.log('表单 allValues:', allValues);
    console.log('表单 changedValues:', changedValues);
  },
  'input': value => {
    console.log('input:', value);
  },
  // 监听 对象嵌套 场景,某个表单项的值发生变化
  'obj.input': (value) => {
    console.log('input:', value);
  },
  // 监听 List 场景,某个表单项的值发生变化
  'list[].input': (value, indexList) => {
    console.log('list[].input:', value, ',indexList:', indexList);
  }
};

配合 form.setSchemaByPathform.setValueByPath就能实现更加复杂的表单联动需求。

dependencies 关联依赖项

当依赖项的值发生变化时,组件自身会触发更新和校验操作。在组件内部,可以通过 props.addons.dependValues 属性来获取依赖项的更新值。

一个经典的场景就是“密码二次确认”,代码如下

const schema = {
  type: 'object',
  displayType: 'row',
  properties: {
    input1: {
      title: '密码',
      type: 'string',
    },
    input2: {
      title: '确认密码',
      type: 'string',
      dependencies: ['input1'],
      rules: [
        { 
          validator: (_, value, { form }) => {
            if (!value || form.getValueByPath('input1') === value) {
              return true;
            }
            return false;
          }, 
          message: '你输入的两个密码不匹配' 
        }
      ]
    }
  }
};
getFieldRef

在使用自定义组件时,可以通过 form.getFieldRef('path') 方法获取到对应的组件实例。其中,path 是该组件的表单路径,当然自定义组件也要通过 props.addons.fieldRef 操作进行绑定。

import { useImperativeHandle } form 'react'
const CustomField = (props) => {

  useImperativeHandle(props.addons.fieldRef, () => {
    return {
      // 暴露内部方法
    };
  });

  return (
    ...// 组件渲染代码
  );
}

export default CustomField;

3. 数据转换

在开发过程中,经常会遇到需要将前端数据转换为符合服务端数据结构的情况。为了解决这个问题,FormRender 提供了一个名为 bind 的魔法字段。通过在表单项中指定 bind 字段,可以将该表单项的值绑定到一个自定义的数据结构中,从而方便实现前后端数据格式的转换。

场景一

表单收集到的日期数据结构是: { "date": \["2023-04-01","2023-04-23"] },而服务端要求的数据结构是: { "startDate": "2023-04-01", "endDate": "2023-04-23" }

image.png

const schema = {
  "type": "object",
  "properties": {
    "date": {
      "bind": [
        "startDate",
        "endDate"
      ],
      "title": "日期",
      "type": "range",
      "format": "date",
      "description": "bind 转换"
    },
    "date1": {
      "title": "日期",
      "type": "range",
      "format": "date",
      "description": "未进行转换"
    }
  }
}

场景二

相信用过 FormRender 1.0 版的用户应该都清楚,使用 List 组件收集到的数据结构是:{ "list": [{ "size": 1 }, {"size": 2}]},而你希望的数据结构可能是 { "list": [1, 2] }, 2.0 已经完美支持。

image.png

const schema = {
  "type": "object",
  "properties": {
    "list": {
      "type": "array",
      "widget": "simpleList",
      "items": {
        "type": "object",
        "column": 3,
        "properties": {
          "input": {
            "bind": "root",
            "title": "大小",
            "type": "number"
          }
        }
      }
    }
  }
}

更多关于 bind 字段的使用技巧,请前往官方文档进行阅读

4. 组件自定义

FormRender 提供了一些基础组件,例如 input、select 和 radio 等,但有时候这些组件并不能完全符合我们的业务需求,此时可以考虑使用自定义组件。FormRender 提供了丰富的自定义组件 API 和接口,以满足业务在表单组件上的个性化需求。

除此之外,FormRender 还支持自定义嵌套组件(object)和列表组件(list),所以真正意义上的自定义组件包括三种类型:输入控件、嵌套组件和列表组件。在使用自定义组件时,可以根据不同的类型使用相应的自定义 API 和接口对组件进行个性化调整和扩展。

输入控件自定义

在使用自定义输入控件时,只需要让该控件能够接收 value 和 onChange 两个基本属性即可。这两个属性是表单项数据和改变数据的事件处理函数,是实现表单控件与表单数据之间双向绑定的必要条件。因此,只要自定义组件能够接收这两个属性,就可以与 FormRender 进行良好的配合。

image.png

const CaptchaInput = (props: any) => {
  const { value, onChange, addons } = props;

  const sendCaptcha = (phone: string) => {
    console.log('send captcha to:', phone);
  }

  return (
    <Space>
      <Input
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder="请输入手机号"
      />
      <Button onClick={() => sendCaptcha(value)}>发送验证码</Button>
    </Space>
  );
};
import React from 'react';
import { Input, Button, Space } from 'antd';
import Form, { useForm } from 'form-render';
import CaptchaInput from './CaptchaInput';

const schema = {
  type: 'object',
  properties: {
    phone: {
      title: '自定义 Input',
      type: 'string',
      widget: 'CaptchaInput',
      props: {}
    }
  }
};

const Demo = () => {
  const form = useForm();

  return (
    <Form
      form={form}
      schema={schema}
      widgets={{ CaptchaInput }}
    />
  );
};

export default Demo;
嵌套组件自定义

在进行嵌套组件的自定义时,需要重点关注 children 属性。children 是嵌套组件的内部表单项,可以直接渲染而无需过多关注。FormRender 会将嵌套组件的 schema 字段透传到嵌套组件的 props 中,并将其打散以渲染对应的子表单项。因此,只要自定义组件能够正确接收 props 中的数据并渲染 children 中的表单项即可。

image.png

import React from 'react';
import { Card } from 'antd';

import './index.less';

const CustomCard = ({ children, title, description }) => {
  if (!title) {
    return children;
  }

  return (
    <Card
      title={
        <>
          {title}
          {description && (
            <span className='fr-header-desc'>
              {description}
            </span>
          )}
        </>
      }
    >
      {children}
    </Card>
  );
}

export default CustomCard;
import React from 'react';
import FormRender, { useForm } from 'form-render';
import CustomCard from './CustomCard';

const schema = {
  type: 'object',
  displayType: 'row',
  properties: {
    obj: {
      type: 'object',
      widget: 'CustomCard',
      title: '卡片主题',
      description: '这是一个对象类型',
      column: 3,
      properties: {
        input1: {
          title: '输入框 A',
          type: 'string'
        },
        input2: {
          title: '输入框 B',
          type: 'string'
        },
        input3: {
          title: '输入框 C',
          type: 'string'
        },
        input4: {
          title: '输入框 D',
          type: 'string'
        }
      }
    }
  }
};

export default () => {
  const form = useForm();

  return <FormRender schema={schema} form={form} widgets={{ CustomCard }/>;
};
列表组件自定义

列表组件的实现稍微复杂一点,一般都会提供扩展参数来实现,但如果过于繁琐的话,可以参照源码对应的 list 组件进行自定义。

image.png

import React, { useState, useContext } from 'react';
import { Tabs } from 'antd';
import './index.less';

const TabPane = Tabs.TabPane;

const TabList = (props) => {
  const {
    schema,
    fields,
    rootPath,
    renderCore,
    readOnly,
    delConfirmProps,
    tabName,
    hideDelete,
    hideAdd,
    addItem,
    removeItem,
    tabItemProps = {}
  } = props;

  return (
      <Tabs>
        {fields.map(({ key, name }) => {
          return (
            <TabPane>
              <div style={{ flex: 1 }}>
                {renderCore({ schema, parentPath: [name], rootPath: [...rootPath, name] })}
              </div>
            </TabPane>
          );
        })}
      </Tabs>
  );
}

export default TabList;
占位组件 (试验)

纯展示型组件,独占一行,可以用于表单模块分割使用,其他场景待开发

void2: {
    title: '其他组件',
    type: 'void', // 关键配置
    widget: 'voidTitle'
}

image.png

五、写在最后

作为一款开箱即用的表单解决方案,FormRender 可以大幅提高中后台系统中的表单开发效率和灵活性,让你可以快速创建各种类型的表单,并省略从头编写表单组件的繁琐步骤。我们将一直坚持这个初衷,并不断推进协议配置方面的创新和提升,努力提供更加完善的表单开发体验。

接下来我们将推出适配 2.0 协议的表单设计器,该设计器将支持自定义二次开发功能,并支持在移动端场景下拖拽生成 schema 协议。这将极大地提高表单设计和开发的便捷性。

最后感谢大家的支持与信任,我们欢迎在使用 FormRender 的过程中继续提出宝贵意见,帮助我们不断创造更具有价值的产品。