order: 2
toc: content
mobile: false
group:
title: 高级用法
order: 1
自定义组件
在实际的开发中,可能会遇到如下的应用场景:
- 我需要写一个异步加载的搜索输入框(普适性不高/难以用 schema 描述的组件)
- 我们团队使用 xxx ui,与 antd 不搭,希望能适配一套 xxx ui 组件的 FormRender(欢迎 Pull Request)
- 我需要在表单内部写一个 excel 上传按钮(完全定制化的需求)
FormRender 内置的控件可能不能满足功能上的需要,这时就需要自定义组件 widget 的支持
使用自定义组件前,也许已经有内置组件支持。具体见 内置组件
:::info 如果是新增一个常用组件,建议给 FormRender 维护的同学来提 Pull Request 或 Issue 并说明你的使用场景,这样可以更好扩展其生态,FormRender 的社区以及提供了部分 常用自定义组件。 :::
什么是 Widget
widget 只是一个普通的 React 组件,它会接收到 FormRender 传递给它的一些 props。开发者可以根据这些 props 完成控件的受控、联动、校验等操作。
比方说,我想在一个常规输入框的后面放一个按钮用于发送验证码。FormRender 的内置组件不能满足需求,那么我可以写一个如下的自定义组件:
const CaptchaInput = (props: any) => {
const { value, onChange } = props;
console.log('widget props:', 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>
);
};
使用 Widget
首先在 <FormRender /> 中注册 widget。
import CaptchaInput from 'my/widgets';
<FormRender widgets={{ CaptchaInput }} />
之后在 Schema 中指定 item 的 widget 属性为刚刚注册的 widget。
const schema = {
type: 'object',
properties: {
phone: {
title: '网址输入自定义组件',
type: 'string',
// 指定为刚刚注册的 widget
widget: 'CaptchaInput',
}
}
};
完整代码如下:
Widget 接收到的 props
默认情况下 Widget 会接收到如下的 props:
id
- 类型:
string - 描述:当前 item 在表单中的唯一 key,一般用不到
schema
- 类型:
Schema - 描述:当前 item 的 schema
value
- 类型:
any - 描述:当前 item 的值,用于 widget 的受控
onChange
- 类型:
(value: any) => void - 描述:当前 item 的值变化时的回调用于 widget 的受控
disabled
- 类型:
boolean - 描述:当前 item 是否为禁用状态,如果没有单独为这个 item 指定,那么就继承全局的
disabled属性
readOnly
- 类型:
boolean - 描述:当前 item 是否为只读状态,如果没有单独为这个 item 指定,那么就继承全局的
readOnly属性
addons
addons 包含了全部的表单实例方法,详见 FormInstance,这里不再赘述。除此之外 addons 还包含了如下一些额外属性。
addons.globalProps
- 类型:
Record<string, any> - 描述:全局属性
addons.dataIndex
- 类型:
string[] - 描述:是自上到下所有经过的数组的 index 按顺序存放的一个数组类型。例如当前的
dataPath为a.b[2].c[0].d,那么这时的dataIndex就为[2,0]。
:::info
如果不是在 Form List 中,那么 dataIndex 始终为一个空数据 []。
:::
addons.dataPath
- 类型:
string - 描述:目前数据所在的 path,例如
a.b[2].c[0].d,详见 Path 书写
addons.schemaPath
- 类型:
string - 描述:当前 item 的 schema 在整体中的路径
addons.dependValues
- 类型:
any[] - 描述:当自定义组件对应的 schema 使用到 dependencies 字段时,在此获得 dependencies 对应的表单项的实时的值
others
实际上任何写入当前 schema 的 props 中的属性都会透传给 widget,所以你不必把所有自定义的属性都写在 widget 内部,更好的方式是通过 schema 控制,以得到更好的复用性。
:::error
在编写 props 时请避开 value、onChange、addons 等字段防止 FormRender 注入的 props 被覆盖。
:::
const shcema = {
type: 'object',
properties: {
name: {
title: '姓名',
type: 'string',
widget: 'MyInput',
// props 中的属性都会透传给自定义组件 MyInput
props: {
addonAfter: 'name',
allowClear: true,
foo: 'xxx',
}
}
}
}
使用表达式联动
表达式是 FormRender 实现简单联动的一个方式。上面提到 widget 会接收到 props 中的属性,这其中也包括使用表达式的字段。FormRender 会先表达式根据表单状态表达式进行转换,然后传递给 widget,并且实时的更新这个属性。我们可以利用这一点很方便的实现一些表单联动。
formData 关键字当前全部的表单状态,对于 Form List 的场景,使用 rootValue 关键字来表示当前 List Item 的数据。详见 表单联动
const shcema = {
type: 'object',
properties: {
age: {
title: '年龄',
type: 'string',
},
name: {
title: '姓名',
type: 'string',
widget: 'MyInput',
props: {
// 当 age 字段更新时,自定义组件 MyInput 会接收到最新的 age 属性
age: '{{ formData.age }}'
}
},
}
}
完整示例如下:
使用 dependencies 联动
除了使用表达式联动,widget 还可以使用 dependencies 属性进行联动。首先在 schema 中定义好 dependencies,比如:
const shcema = {
type: 'object',
properties: {
age: {
title: '年龄',
type: 'string',
},
name: {
title: '姓名',
type: 'string',
widget: 'MyInput',
// 指定依赖的字段
dependencies: ['age']
},
}
}
之后在 widget 的 props.addons.dependValues 中可以拿到依赖的值。
const MyInput = (props) => {
const { addons } = props;
console.log('dependValues:', addons.dependValues);
// dependValues: ['xxxx']
return (
// ...
)
}
与上面同样的例子,使用 dependencies 的代码如下:
:::info
dependencies 除了触自动更新之外,还能触发校验,详见 表单联动
:::
其他 Widget
除了输入控件可以自定义 widget 之外,Form Render 还提供了自定义一个表单项其他部分的能力。
readOnlyWidget
只读模式下,默认会渲染内置的 html 组件,但有时 html 组件并不能满足一个自定义组件在只读模式下需要的展示,此时可使用readOnlyWidget字段来指定只读模式下的展示。
const schema = {
type: 'object',
properties: {
string: {
title: 'ReadOnly widget',
type: 'string',
widget: 'SiteInput',
readOnlyWidget: 'ReadOnlySiteInput',
},
},
};
如果你打算在一个自定义组件里通过 readOnly 参数判断条件展示,既是说,site 组件已经写了只读和非只读情况下的渲染
const SiteInput = ({ readOnly, value, ...rest }: WidgetProps) => {
if (readOnly) return <a href={`https://${value}.com`}>{`https://${value || ''}.com`}</a>;
return (
<Input addonBefore="https://" addonAfter=".com" value={value} {...rest} />
);
};
此时可以指定 readOnlyWidget 和 widget 为同一个组件:
const schema = {
type: 'object',
properties: {
string: {
title: 'ReadOnly widget',
type: 'string',
widget: 'SiteInput',
readOnlyWidget: 'SiteInput',
},
},
};
完整代码如下:
labelWidget
使用 labelWidget 自定义 label 组件,此时 widget 接收到的 props 只有 schema。
descWidget
使用 descWidget 自定义 description 组件,此时 widget 接收到的 props 只有 schema。
统一管理 Widget
同一个项目下不同的 form 里,使用到的自定义组件可能大致相同,但也有可能互相不同,我们建议是中心化一个 Form 组件,并一次性将所有需要的自定义组件注入其中。在项目的各处引入对应组件:
// /Component/FormRender.js
import Form from 'form-render';
import Cascade from './Cascade';
import Percentage from './Percentage';
import MyCheckBox from './MyCheckBox';
import ExcelUploader from './ExcelUploader';
export default props => (
<Form
widgets={{
Percentage,
Cascade,
MyCheckBox,
ExcelUploader
}}
{...props}
/>
);
然后在每个 form 页面统一引入使用
import { useForm } from 'form-render';
import FormRender from './Component/FormRender';
const Demo = props => {
const form = useForm();
return <FormRender form={form} onFinish={() => {}} />;
};
使用 TS
使用 WidgetProps 获得自定义组件 props 的类型支持。
import { FC } from 'react';
import type { WidgetProps } from 'form-render';
const MyWidget: FC<WidgetProps> = (props) => {
const { value, onChange, addons } = props;
return (
<div>My Widget</div>
)
}
export default MyWidget;