File size: 10,204 Bytes
c9d0005 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 | ---
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 (
<Space>
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="请输入手机号"
/>
<Button onClick={() => sendCaptcha(value)}>发送验证码</Button>
</Space>
);
};
```
## 使用 Widget
首先在 `<FormRender />` 中注册 widget。
```js
import CaptchaInput from 'my/widgets';
<FormRender widgets={{ CaptchaInput }} />
```
之后在 Schema 中指定 item 的 widget 属性为刚刚注册的 widget。
```js
const schema = {
type: 'object',
properties: {
phone: {
title: '网址输入自定义组件',
type: 'string',
// 指定为刚刚注册的 widget
widget: 'CaptchaInput',
}
}
};
```
完整代码如下:
<code src="./demo/widget/basic.tsx"></code>
## 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<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 书写](/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 }}'
}
},
}
}
```
完整示例如下:
<code src="./demo/widget/linkage.tsx"></code>
## 使用 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` 的代码如下:
<code src="./demo/widget/depend-linkage.tsx"></code>
:::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 <a href={`https://${value}.com`}>{`https://${value || ''}.com`}</a>;
return (
<Input addonBefore="https://" addonAfter=".com" value={value} {...rest} />
);
};
```
此时可以指定 `readOnlyWidget` 和 `widget` 为同一个组件:
```js
const schema = {
type: 'object',
properties: {
string: {
title: 'ReadOnly widget',
type: 'string',
widget: 'SiteInput',
readOnlyWidget: 'SiteInput',
},
},
};
```
完整代码如下:
<code src="./demo/widget/readonly-widget.tsx"></code>
### labelWidget
使用 `labelWidget` 自定义 label 组件,此时 widget 接收到的 props 只有 `schema`。
<code src="./demo/widget/label-widget.tsx"></code>
### descWidget
使用 `descWidget` 自定义 description 组件,此时 widget 接收到的 props 只有 `schema`。
<code src="./demo/widget/desc-widget.tsx"></code>
## 统一管理 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
widgets={{
Percentage,
Cascade,
MyCheckBox,
ExcelUploader
}}
{...props}
/>
);
```
然后在每个 form 页面统一引入使用
```js
import { useForm } from 'form-render';
import FormRender from './Component/FormRender';
const Demo = props => {
const form = useForm();
return <FormRender form={form} onFinish={() => {}} />;
};
```
## 使用 TS
使用 `WidgetProps` 获得自定义组件 props 的类型支持。
```ts
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;
```
|