--- order: 2 toc: content mobile: false group: title: 高级用法 order: 1 --- # 自定义组件 在实际的开发中,可能会遇到如下的应用场景: - 我需要写一个异步加载的搜索输入框(普适性不高/难以用 schema 描述的组件) - 我们团队使用 xxx ui,与 antd 不搭,希望能适配一套 xxx ui 组件的 FormRender(欢迎 Pull Request) - 我需要在表单内部写一个 excel 上传按钮(完全定制化的需求) FormRender 内置的控件可能不能满足功能上的需要,这时就需要自定义组件 widget 的支持 使用自定义组件前,也许已经有内置组件支持。具体见 [内置组件](/form-render/display-row) :::info 如果是新增一个常用组件,建议给 FormRender 维护的同学来提 Pull Request 或 Issue 并说明你的使用场景,这样可以更好扩展其生态,FormRender 的社区以及提供了部分 [常用自定义组件](https://github.com/alibaba/x-render/tree/master/widgets)。 ::: ## 什么是 Widget widget 只是一个普通的 React 组件,它会接收到 FormRender 传递给它的一些 props。开发者可以根据这些 props 完成控件的受控、联动、校验等操作。 比方说,我想在一个常规输入框的后面放一个按钮用于发送验证码。FormRender 的内置组件不能满足需求,那么我可以写一个如下的自定义组件: ```js const CaptchaInput = (props: any) => { const { value, onChange } = props; console.log('widget props:', props); const sendCaptcha = (phone: string) => { console.log('send captcha to:', phone); } return ( onChange(e.target.value)} placeholder="请输入手机号" /> ); }; ``` ## 使用 Widget 首先在 `` 中注册 widget。 ```js import CaptchaInput from 'my/widgets'; ``` 之后在 Schema 中指定 item 的 widget 属性为刚刚注册的 widget。 ```js 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](/form-render/api-props#forminstance),这里不再赘述。除此之外 addons 还包含了如下一些额外属性。 #### addons.globalProps - 类型:`Record` - 描述:全局属性 #### 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 书写](/form-render/advanced-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 被覆盖。 ::: ```js 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 的数据。详见 [表单联动](/form-render/advanced-linkage) ```js 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`,比如: ```js const shcema = { type: 'object', properties: { age: { title: '年龄', type: 'string', }, name: { title: '姓名', type: 'string', widget: 'MyInput', // 指定依赖的字段 dependencies: ['age'] }, } } ``` 之后在 widget 的 `props.addons.dependValues` 中可以拿到依赖的值。 ```js const MyInput = (props) => { const { addons } = props; console.log('dependValues:', addons.dependValues); // dependValues: ['xxxx'] return ( // ... ) } ``` 与上面同样的例子,使用 `dependencies` 的代码如下: :::info `dependencies` 除了触自动更新之外,还能触发校验,详见 [表单联动](/form-render/advanced-linkage#dependencies-依赖字段) ::: ## 其他 Widget 除了输入控件可以自定义 widget 之外,Form Render 还提供了自定义一个表单项其他部分的能力。 ### readOnlyWidget 只读模式下,默认会渲染内置的 html 组件,但有时 html 组件并不能满足一个自定义组件在只读模式下需要的展示,此时可使用`readOnlyWidget`字段来指定只读模式下的展示。 ```js const schema = { type: 'object', properties: { string: { title: 'ReadOnly widget', type: 'string', widget: 'SiteInput', readOnlyWidget: 'ReadOnlySiteInput', }, }, }; ``` 如果你打算在一个自定义组件里通过 readOnly 参数判断条件展示,既是说,site 组件已经写了只读和非只读情况下的渲染 ```js const SiteInput = ({ readOnly, value, ...rest }: WidgetProps) => { if (readOnly) return {`https://${value || ''}.com`}; return ( ); }; ``` 此时可以指定 `readOnlyWidget` 和 `widget` 为同一个组件: ```js 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 组件,并一次性将所有需要的自定义组件注入其中。在项目的各处引入对应组件: ```js // /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 页面统一引入使用 ```js import { useForm } from 'form-render'; import FormRender from './Component/FormRender'; const Demo = props => { const form = useForm(); return {}} />; }; ``` ## 使用 TS 使用 `WidgetProps` 获得自定义组件 props 的类型支持。 ```ts import { FC } from 'react'; import type { WidgetProps } from 'form-render'; const MyWidget: FC = (props) => { const { value, onChange, addons } = props; return (
My Widget
) } export default MyWidget; ```