背景
我目前的工作是负责开发和维护公司内部管理系统,其中要涉及大量表单的建设,全部是基于 Ant Design 这一套进行开发的。
Ant Design 围绕 <Form>
元素开发的表单系统已经相当完善了,而表单模块中最灵活、有趣的一个功能就是动态表单的构建了,今天就带大家来学学它。
首先,我们先创建好一个 Ant Design 初始项目,很快的。
安装 Ant Design
使用 Vite 创建项目,安装 Ant Design React 依赖。
创建项目:
npm create vite antd-demo -- --template react
cd antd-demo
安装依赖:
# 安装 antd 包
npm install antd --save
npm install
使用 VS Code 打开:
code .
删除 src/index.css 中的内容,修改 src/App.jsx 文件内容如下:
import { Button } from 'antd';
const App = () => (
<div className="App">
<Button type="primary">Button</Button>
</div>
);
export default App;
启动项目,浏览器访问。
$ npm run dev
> antd-demo@0.0.0 dev
> vite
VITE v5.3.1 ready in 466 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
效果:
如此,我们便完成了 Ant Design 项目的搭建。
接下来,就来学习动态表单的内容。
表单基础知识
阅读官当文档《Form 表单》一节时,会看到关于“动态增减表单项”的内容讲解。
在 Ant Design 表单系统中,所有的表单内容都是包含在 <Form>
之中的。
<Form>
{/* 表单内容 */}
</Form>
一般表单内是如下三层结构。
<Form>
<Form.Item>
<Input />
</Form.Item>
</Form>
以下就是一个简单的账号密码登录案例:
import { Form, Input, Button, Checkbox } from 'antd';
const App = () => {
const onFinish = (values) => {
console.log('Success:', values);
}
return (
<div className="App">
<Form
name="basic"
initialValues={{ remember: true }}
onFinish={onFinish}
autoComplete="off"
>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input.Password />
</Form.Item>
<Form.Item
name="remember"
valuePropName="checked"
>
<Checkbox>Remember me</Checkbox>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
</div>
)
}
展示效果:
补充数据,点击“Submit”,就能在控制台看到提到的数据了!
是不是很方便呢?
当然,有时候由于业务需要,我们的 UI 需要支持动态添加。上面提前写好的组织方式就不适应了。
动态表单初印象
一旦涉及到动态表单的构建,就要用到 <Form.List>
这个组件。
<Form>
<Form.List>
{/* ... */}
</Form.List>
</Form>
<Form.List>
类似循环组件,其 children 是一个函数。
<Form>
<Form.List>
{(fields, { add, remove, move }, { errors }) => (
<>
{/* ... */}
</>
)}
</Form.List>
</Form>
这个函数接受 3 个参数:
- 第一个参数就是我们要渲染的动态数组 fields,这就是
<Form.List>
中我们要循环遍历的对象 - 第二个参数是一个对象,你能从中解构出 add、remove 和 move 三个函数,它们都是用来改变 fields 的
- 第三个参数则可以解构出错误列表 errors,它是配合 Form.List 的 rules 一起使用的
好了,有了这些储备,我们就可以写一个简单的 DEMO 了先。
import { Form, Input, Button, Space } from 'antd';
const App = () => {
const onFinish = (values) => {
console.log('Success:', values);
}
return (
<div className="App">
<Form
name="basic"
onFinish={onFinish}
autoComplete="off"
initialValues={{ options: [] }}
>
<Form.List name="options">
{(fields, { add, remove }) => (
<>
{fields.map((field, index) => (
<Space align="baseline" key={field.key}>
<Form.Item {...field} key={field.key} label={`选项 ${index + 1}`}>
<Input placeholder="选项内容" />
</Form.Item>
{fields.length > 1 ? (
<Button type="dashed" onClick={() => remove(field.name)}>
删除选项
</Button>
) : null}
<p>{JSON.stringify(field)}</p>
</Space>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()}>
增加选项
</Button>
</Form.Item>
</>
)}
</Form.List>
<Form.Item>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
</div>
)
}
这里需要说明的是。
- 动态表单
<Form.List>
我们给取名options
,还在<Form>
中做了初始化,初始化为一个空数组 - 增加项目后,至少会保留一个项目不被删除
下面,我们来看下渲染效果:
现在点击 1 次“增加选项”:
我们能在右侧看到 filed 项的数据结构:{"name":0,"key":0,"isListField":true,"fieldKey":0}。这里的 name/key 对应当前项在数组中的索引值,可是值相同但含义不同——name 用于当前值的设置,key 则用于唯一标识,确保渲染无误。
再点击 1 次“增加选项”:
增加了一个 filed 项目:{"name":1,"key":2,"isListField":true,"fieldKey":2}。
点击“提交”:
填写数据再“提交”:
以上,我们就实现了一个简易版本的动态表单删减能力。
更复杂一点的动态表单
前一个例子,每个 field 只收集一个字符串参数。更复杂一些,可以用来收集对象参数。
import { Form, Input, Button, Space } from 'antd';
const App = () => {
const onFinish = (values) => {
console.log('Success:', values);
}
return (
<div className="App">
<Form
name="basic"
onFinish={onFinish}
autoComplete="off"
initialValues={{ users: [] }}
>
<Form.List name="users">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Space align="baseline" key={key}>
<Form.Item {...restField} key={`${key}-first`} name={[name, 'first']} rules={[{ required: true, message: 'Missing first name' }]}>
<Input placeholder="First Name" />
</Form.Item>
<Form.Item {...restField} key={`${key}-last`} name={[name, 'last']} rules={[{ required: true, message: 'Missing last name' }]}>
<Input placeholder="Last Name" />
</Form.Item>
<Button type="dashed" onClick={() => remove(name)}>
-
</Button>
<p>{JSON.stringify({ key, name, ...restField })}</p>
</Space>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()}>
+
</Button>
</Form.Item>
</>
)}
</Form.List>
<Form.Item>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
</div>
)
}
展示效果:
直接点击“Submit”,就能看到校验信息!
补充完信息后,校验信息就没了,点击“Submit”,就能看到提交的数据了。
数据结构类型:{"users":[{"first":"a","last":"b"},{"first":"c","last":"d"}]}。
当然,如果你想在初始时,就先展示一个项目,可以修改 <Form>
的 initialValues
prop。
<Form
- initialValues={{ users: [] }}
+ initialValues={{ users: [{}] }}
>
效果如下:
这个小技巧还是很有帮助的,大多数情况下我们都需要这样展示。
嵌套动态表单的创建
还有一个超复杂一点动态表单创建——一个 <Form.List>
嵌套一个 <Form.List>
,跟套娃似的。
最终效果如下:
首先我们先绘制最外层的 Group 列表:
import { Form, Input, Button, Space, Card, Typography } from 'antd';
const App = () => {
const [form] = Form.useForm()
return (
<div className="App">
<Form
form={form}
autoComplete="off"
initialValues={{ groups: [{}] }}
>
<Form.List name="groups">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Card
key={key}
title={`Group ${index + 1}`}
extra={(
<Button onClick={() => remove(name)}>×</Button>
)}
>
<Form.Item {...restField} key={`${key}-group-name`} label="Name" name={[name, 'name']} rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item {...restField} key={`${key}-group-users`} label="Users" required>
{/* 嵌套 Form.List */}
</Form.Item>
</Card>
))}
<Form.Item style={{ marginTop: '1rem' }}>
<Button type="dashed" block onClick={() => add()}>
+ Add Group
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form >
</div >
)
}
效果如下:
然后再将 {/* 嵌套 Form.List */}
下方增加嵌 User 列表:
<Form.Item {...restField} key={`${key}-group-users`} label="Users" required>
{/* 嵌套 Form.List */}
<Form.List name={[name, 'users']}>
{(subFields, { add: subAdd, remove: subRemove }) => (
<>
{subFields.map((subFiled) => (
<div key={subFiled.key}>
<Space align="baseline" key={subFiled.key}>
<Form.Item {...subFiled} key={`${subFiled.key}-first`} name={[subFiled.name, 'first']} rules={[{ required: true }]}>
<Input placeholder="First Name" />
</Form.Item>
<Form.Item {...subFiled} key={`${subFiled.key}-last`} name={[subFiled.name, 'last']} rules={[{ required: true }]}>
<Input placeholder="Last Name" />
</Form.Item>
<Button onClick={() => subRemove(subFiled.name)}>
×
</Button>
</Space>
</div>
))}
<Form.Item>
<Button type="dashed" block onClick={() => subAdd()}>
+ Add User
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form.Item>
效果如下:
为了便于观察 Form 表单的值,我们再添加一个预览区域。
<Form
form={form}
autoComplete="off"
initialValues={{ groups: [{}] }}
>
<Form.List name="groups">
{/* ... */}
</Form.List>
<Form.Item noStyle shouldUpdate>
{() => (
<Typography>
<pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre>
</Typography>
)}
</Form.Item>
</Form>
效果如下:
填写一些数据,观察表单数据结构:
总结
本文带大家了解了 Antd Design <Form>
表单关于动态表单构建的内容。
主要分成 3 块内容:
- 基本动态表单构建(成员是简单类型值)
- 复杂一点的动态表单构建(成员是对象类型值)
- 更复杂一点的动态表单构建(
<Form.List>
嵌套<Form.List>
)
基本上涵盖了关于动态表单构建的所有内容。
当然,纸上得来终觉浅。大家可以实地跟随本文内容,自己手动敲写一遍,更能起到学习的目的。
希望本文对你的工作会有所帮助,后面有机会我还会更新关于 Ant Design 表单部分的其他内容。感谢你的阅读,再见。