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 组件等,可以满足大多数场景的表单实现需求。
-
扩展性强:具有非常强的扩展性,支持自定义各种类型的表单控件,用户可以根据实际需要进行定制,非常灵活。
-
易于使用:容易上手,可以通过表单设计器可视化拖拽的方式快速生成表单。
三、如何使用
- 安装依赖
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}
/>
);
}
- 通过可视化表单生成器 Schemabuilder ,用户可以拖拖拽拽快速生成 JsonSchema。
4. 通过 Playground 可以进行在线体验,并查看其中丰富的表单示例。
四、高级特性
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.setSchemaByPath
和 form.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" }
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 已经完美支持。
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 进行良好的配合。
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
中的表单项即可。
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 组件进行自定义。
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'
}
五、写在最后
作为一款开箱即用的表单解决方案,FormRender 可以大幅提高中后台系统中的表单开发效率和灵活性,让你可以快速创建各种类型的表单,并省略从头编写表单组件的繁琐步骤。我们将一直坚持这个初衷,并不断推进协议配置方面的创新和提升,努力提供更加完善的表单开发体验。
接下来我们将推出适配 2.0 协议的表单设计器,该设计器将支持自定义二次开发功能,并支持在移动端场景下拖拽生成 schema 协议。这将极大地提高表单设计和开发的便捷性。
最后感谢大家的支持与信任,我们欢迎在使用 FormRender 的过程中继续提出宝贵意见,帮助我们不断创造更具有价值的产品。