我们在构建表单时, 有时需要处理数组嵌套对象类型的数据结构, Table
组件非常适合此类数据的展示,如下图中的备货信息。用户可以在允许范围内添加、删除备货信息,且每一条备货信息都有一些必填项,那么,在使用 Ant Design 时,如何在表单中内嵌表格,实现这种效果呢?
接下来,我们用两种方式实现一个内嵌表格的表单,表单数据结构如下 FormData
type User = {
name: string;
age: number;
address?: string;
}
type FormData = {
users: User[]
}
实现1 — 通过 Form.Item
的 namePath
第一种实现方式的核心,就是给 Form.Item 设置 [field, index, property]
格式的 namePath, 通过此种方式,Form 会自动把 field
处理成数组结构。
注意我们是如何定义 Form.Item 的:
我们只设置了 label
属性,没有设置 name
,且把表单项设为 shouldUpdate
;
Form.Item 的子元素时一个函数,函数内部通过 form.getFieldValue(LIST_NAME)
获取 users 的值用于渲染 Table
// 为了便于后续维护,使用常量保存表单项的 name
const LIST_NAME = "users";
const NestTableInForm: React.FC<OnlyTableProps> = (props) => {
const [form] = Form.useForm();
const columns: ColumnProps<User>[] = [
// ...
]
return (
<Form
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={{
[LIST_NAME]: [{ name: "hello" }, { name: "world" }],
}}>
<Form.Item label="Users" shouldUpdate>
{() => {
// 表单中获取数据
const dataSource = form.getFieldValue(LIST_NAME);
return (
<Table
size="small"
bordered
rowKey="name"
dataSource={dataSource || []}
columns={columns}
pagination={false}
/>
);
}}
</Form.Item>
{/* ... */}
</Form>
);
};
此处必须设置 Form.Item 为
shouldUpdate
, 否则表格不会渲染;
注意当shouldUpdate
为true
时,Form 的任意变化都会使该 Form.Item 重新渲染,如果表单中还有很多其他字段时,你需要把shouldUpdate
改为函数,判断是否更新当前表单项。详情见 官方文档渲染表格的数据要从表单中获取,不推荐用
useState
另外维护一份 dataSource
接下来我们需要重点看下如何定义 Table 的列 columns
。
以第一列为例,我们可以在 render 函数里获取当前行的 index, render 函数返回一个 Form.Item, 该表单项的 name
需要设为 namePath [LIST_NAME, index, "name"]
,这样表单收集到数据就会和我们定义的数据结构吻合。
添加、删除数据时,则需要自行处理数据,然后调用 form 实例的方法给 users
赋值
const columns: ColumnProps<any>[] = [
{
title: "Name",
dataIndex: "name",
render(value, records, index) {
return (
<Form.Item
name={[LIST_NAME, index, "name"]}
rules={[{ required: true, message: "required" }]}>
<Input />
</Form.Item>
);
},
},
// Age...
// Address...
{
title: "Action",
dataIndex: "action",
render(value, record, index) {
return (
<Space>
<Button type="primary" shape="circle" onClick={() => handleAdd()}>
+
</Button>
<Button danger shape="circle" onClick={() => handleDelete(index)}>
-
</Button>
</Space>
);
},
},
];
const handleAdd = () => {
form.setFieldsValue({
[LIST_NAME]: [...form.getFieldValue(LIST_NAME), {}],
});
};
const handleDelete = (index: number) => {
const list = form.getFieldValue(LIST_NAME);
list.splice(index, 1);
form.setFieldsValue({
[LIST_NAME]: [...list],
});
};
实现2 — 借助 Form.List
(推荐)
Ant Design 提供了表单字段数组化管理的方案 Form.List
,和 Table 结合也可以实现表单内嵌表格的效果。
Form.List 的 children 是一个函数,类型如下:
type Fn = (
fields: Field[],
operation: { add, remove, move },
meta: { errors }
) => React.ReactNode
type Field = {
fieldKey: number;
isListField: boolean;
key: number;
name: number;
}
接下来看下具体实现:
- 添加一个Form.Item
<Form.Item label="Users">
,未设置name, 只用于展示 label; - Form.Item 的子元素为一个 Form.List ,
<Form.List name="users">
, 表单通过该 Form.list 收集users
下的数据; - Form.List 的子函数里用
fields
映射一份数据,用于渲染 Table
主要代码如下:
const NestTableInForm: React.FC = () => {
// ...
return (
<Form
layout="vertical"
onFinish={onFinish}
initialValues={{
users: [{ name: "hello" }, { name: "world" }],
}}>
<Form.Item label="Users">
<Form.List name="users">
{(fields, operation) => {
// 映射数据,用于渲染表格
const dataSources = fields.map((field) => ({
field,
operation, // 把操作方法提供给每一行
}));
return (
<Table
size="small"
bordered
rowKey={(row) => row.field.key}
dataSource={dataSources}
columns={columns}
pagination={false}
/>
);
}}
</Form.List>
</Form.Item>
</Form>
);
};
field对象 的 key
name
fieldKey
都是 number
类型的,它们的值实际上就是该 field 在列表中的 index
。
特别注意,在 Form.List 下定义的 Form.Item, 其 name 需要从 Form.List 的 name 往后开始算起。例如一个 Form.Item 完整的 namePath 是 ['list', index, 'name']
, 如果该 Form.Item 被嵌套在 <Form.List name="list">
内部, 则其 name
属性应该设为 [index, 'name']
。所以,与第一种实现方式有所不同,我们在 columns 中定义 Form.Item 时, 需要在设置 name 时省略 Form.List 的 namePath。
此外,操作数据时,我们也可以直接使用 Form.List 提供的操作方法添加、删除数据。
定义 columns 的代码如下:
const columns: ColumnProps<{
field: FormListFieldData;
operation: FormListOperation;
}>[] = [
{
title: "Name",
dataIndex: "name",
render(value, { field }) {
return (
<Form.Item
// 省略 Form.List 的 namePath, field.name 就是当前行的 index
name={[field.name, "name"]}
rules={[{ required: true, message: "required" }]}>
<Input />
</Form.Item>
);
},
},
// Age...
// Address...
{
title: "Action",
dataIndex: "action",
render(value, { operation, field }) {
return (
<Space>
<Button
type="primary"
shape="circle"
onClick={() => operation.add()}>
+
</Button>
<Button
danger
shape="circle"
onClick={() => operation.remove(field.name)}>
-
</Button>
</Space>
);
},
},
];
总结
两种实现表单嵌套表格的方法:
- 第一种通过给 Form.Item 设置
[field, index, property]
格式的 namePath 实现 - 第二种方式借助 Form.List, 操作数据更加方便,内嵌 Form.Item 的 name 也更加简洁
如果你还有其他实现方式的话,欢迎评论区交流。
本文中的源码我已放到 GitHub仓库 ,如果你觉得此文对你有帮助的话,求赞求star 🌟。如果你在工作中有其他关于表单的问题,欢迎提出来交流一下,我会在这个仓库里继续更新 Ant Design 表单相关的问题。