Antd-FormList嵌套Table实践
需求说明
-
使用Antd,实现一个Table,有两种状态,纯展示状态和可编辑状态。纯展示状态下所有内容直接取值,不可变。可编辑状态下table某些列为Input输入框,并且input中的值改变时,需要实时收集改变后的状态。Table行支持勾选,并且在可编辑状态下,点击提交表单时,只校验已勾选行的值。
-
实现方法:一种方法是通过Form嵌套Form.List嵌套Table来实现。
const Demo = () => {
return (
<Form>
<Form.List name="list">
(fields) => {
return (
<Table
size="small"
rowKey="key"
columns={columns}
dataSource={fields}
pagination={false}
rowSelection={getRowSelection()}
/>
)
}
</Form.List>
</Form>
)
}
TableProps变化
- 此时Table的dataSource为fields,并且rowKey为key,这里的key是fields数组项的key,如下图所示。
注意,fieldKey已被遗弃,不建议使用。
-
Table的columns属性中,每一项的render方法的参数此时也发生了变化,第一个参数不再是该行该列的值, 而是field的name,第二个参数不再是该行数据record对象,而是整个field对象,第三个参数则是field的key。
因为以上参数含义的变化,我们在获取某行某列的值时,不能像之前一样直接从第二个参数record中去到。以下是一个columns示例:
const columns: TableColumnType<FormListFieldData>[] = [ { title: "index", width: 50, render: (_, _field: FormListFieldData, index: number) => `${index + 1}` }, { title: "name", dataIndex: "name", render: (_, field: FormListFieldData, index) => { const { name } = form.getFieldValue([list, field.name]); return name; } }, { title: "desc", dataIndex: "desc", render: (_, field: FormListFieldData) => { const { desc } = form.getFieldValue([list, field.name]); return desc; } }, { title: "price", width: 140, render: (_, field: FormListFieldData) => { const { price, editable } = form.getFieldValue([list, field.name]); return editable ? ( <Form.Item {...field} name={[field.name, "price"]} rules={[{ required: true, message: "please input price" }]} > <InputNumber className="w-full" min={0} max={99999} precision={2} controls={false} /> </Form.Item> ) : ( price ); } }, { title: "count", width: 120, render: (_, field: FormListFieldData) => { const { count, editable } = form.getFieldValue([list, field.name]); return editable ? ( <Form.Item {...field} name={[field.name, "count"]} rules={[{ required: true, message: "please input count" }]} > <InputNumber min={0} max={99999} precision={0} controls={false} /> </Form.Item> ) : ( count ); } } ];
如上所示,每个column对象中的render属性中想要获取改行的数据只能通过
form.getFieldValue([Form.List.name, [field.name]])
table初始化赋值
-
现在table内所有值的获取与变化都通过form实例的方法来控制,比如上边说到getFieldValue。初始化可以通过一个effect执行form.setFieldValue({
{Form.List.name}
:{valueObject}
}),同时,如果需要设置选中行,也可以在这种effect中处理。以下是示例代码:/** 通过Table.rowSelection设置行可选择与默认选中行 */ const getRowSelection: GetRowSelection = () => { return { type: "checkbox", onChange: handleSelectedRowChange, getCheckboxProps: (field: FormListFieldData) => { const { hasEdited } = form.getFieldValue([list, field.name]); return { disabled: hasEdited === true }; }, selectedRowKeys: selectedKeys } useEffect(() => { form.setFieldsValue({ list: dataList }) /** 设置默认选中行 * 这里0,1其实是fields中数据项field的index。 * 如果不涉及到table行的增删时,它跟数据源dataSource的index是相对应的。 * 如果涉及到增删数据行,得着重考虑这一块的处理。 */ setSelectedKeys([0, 1]) }, [form, dataList])
表单验证
对于表单验证,有这样的需求:提交表单时,只验证已勾选行的表单,这就需要使用form.validateFields({namePath}
)了。namePath参数是一个数组,通过selectedKeys数组获取到需要执行验证的行。以下是示例代码:
const handleOperationClick = async () => {
const validList = selectedKeys.map((key) => [list, key])
try {
await form.validateFields([...validList], { recursive: true })
} catch (e) {
if (e?.errorFields?.length) {
message.error("please complete the warning area!")
}
return
}
operateSomething()
};
[namePath]
接受类似于[ ['list', 0], ['list', 1] ]的参数,得益于antd5.9的recursive参数的支持,不用传类似[ ['list', 0, 'price'], ['list', 0, 'count'], ['list', 1, 'price'], ['list', 1, 'count'], ... ]这样臃肿的namePath。
总结
使用Form.List嵌套Table的关键点在于如何维护数据源,其实就是通过form的几个示例方法setFieldValue,getFieldValue与validateFileds。还有要注意一些属性参数的含义变化等
完整示例代码
Form.List嵌套Table示例 - antd@5.9.1 (forked) - CodeSandbox
import { useEffect, useState } from "react";
import type { FC } from "react";
import {
Button,
Drawer,
Form,
InputNumber,
Table,
Typography,
message as Message
} from "antd";
import type { FormListFieldData, TableColumnType, TableProps } from "antd";
import type { TableRowSelection } from "antd/es/table/interface";
const { Title } = Typography;
const list = "list";
interface FormTableDrawerProps {
open: boolean;
onClose?: () => void;
onOperateSuccess: () => void;
detail?: any;
dataList?: any[];
}
const FormTableDrawer: FC<FormTableDrawerProps> = ({
open,
onClose,
onOperateSuccess,
detail,
dataList
}) => {
type GetRowSelection = () => TableProps<FormListFieldData>["rowSelection"];
const [selectedKeys, setSelectedKeys] = useState<(string | number)[]>([]);
const [form] = Form.useForm();
const [message, contextHolder] = Message.useMessage();
const {
instockState,
ysbOrderSn,
payTime,
cost,
providerName,
state,
id: orderId
} = detail || {};
const operateSomething = () => {
onOperateSuccess?.();
message.success("operate success");
};
/**
* 通过FormList嵌套Table时,render的参数结构会变化
* render: (_,field) field此时不是行数据record,而是数组fields的项field
*/
const columns: TableColumnType<FormListFieldData>[] = [
{
title: "index",
width: 50,
render: (_, _field: FormListFieldData, index: number) => `${index + 1}`
},
{
title: "name",
dataIndex: "name",
render: (_, field: FormListFieldData, index) => {
const { name } = form.getFieldValue([list, field.name]);
return name;
}
},
{
title: "desc",
dataIndex: "desc",
render: (_, field: FormListFieldData) => {
const { desc } = form.getFieldValue([list, field.name]);
return desc;
}
},
{
title: "price",
width: 140,
render: (_, field: FormListFieldData) => {
const { price, editable } = form.getFieldValue([list, field.name]);
return editable ? (
<Form.Item
{...field}
name={[field.name, "price"]}
rules={[{ required: true, message: "please input price" }]}
>
<InputNumber
className="w-full"
min={0}
max={99999}
precision={2}
controls={false}
/>
</Form.Item>
) : (
price
);
}
},
{
title: "count",
width: 120,
render: (_, field: FormListFieldData) => {
const { count, editable } = form.getFieldValue([list, field.name]);
return editable ? (
<Form.Item
{...field}
name={[field.name, "count"]}
rules={[{ required: true, message: "please input count" }]}
>
<InputNumber min={0} max={99999} precision={0} controls={false} />
</Form.Item>
) : (
count
);
}
}
];
const handleSelectedRowChange: TableRowSelection<any>["onChange"] = (
selectedRowKeys: number[]
) => {
setSelectedKeys(selectedRowKeys);
};
const handleOperationClick = async () => {
const validList = selectedKeys.map((key) => [list, key]);
try {
await form.validateFields([...validList], { recursive: true });
} catch (e) {
if (e?.errorFields?.length) {
message.error("please complete the warning area!");
}
return;
}
operateSomething();
};
const getRowSelection: GetRowSelection = () => {
return {
type: "checkbox",
onChange: handleSelectedRowChange,
getCheckboxProps: (field: FormListFieldData) => {
const { hasEdited } = form.getFieldValue([list, field.name]);
return {
disabled: hasEdited === true
};
},
selectedRowKeys: selectedKeys
};
};
const getFooter = () => {
return (
<div className="flex items-center justify-end">
<Button className="mr-12px" onClick={() => onClose?.()}>
cancel
</Button>
<Button
type="primary"
// loading={operateLoading}
disabled={selectedKeys.length === 0}
onClick={handleOperationClick}
>
operate
</Button>
</div>
);
};
useEffect(() => {
form.setFieldsValue({
list: dataList
});
setSelectedKeys([0, 1]);
}, [form, dataList]);
return (
<>
{contextHolder}
<Drawer
forceRender
open={open}
onClose={onClose}
width={768}
footer={getFooter()}
>
<Title level={4}>LIST</Title>
<Form form={form}>
<Form.List name={list}>
{/* FormList嵌套Table时,Table的dataSource来源于FormList的fields */}
{(fields) => {
return (
<Table
size="small"
rowKey="key"
columns={columns}
dataSource={fields}
pagination={false}
rowSelection={getRowSelection()}
/>
);
}}
</Form.List>
</Form>
</Drawer>
</>
);
};
export default FormTableDrawer;