Ant Design 如何在表单中嵌入表格

2,404 阅读4分钟

我们在构建表单时, 有时需要处理数组嵌套对象类型的数据结构, Table 组件非常适合此类数据的展示,如下图中的备货信息。用户可以在允许范围内添加、删除备货信息,且每一条备货信息都有一些必填项,那么,在使用 Ant Design 时,如何在表单中内嵌表格,实现这种效果呢?

complex-form.png

接下来,我们用两种方式实现一个内嵌表格的表单,表单数据结构如下 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>
      );
    },
  },
];

总结

两种实现表单嵌套表格的方法:

  1. 第一种通过给 Form.Item 设置 [field, index, property] 格式的 namePath 实现
  2. 第二种方式借助 Form.List, 操作数据更加方便,内嵌 Form.Item 的 name 也更加简洁

如果你还有其他实现方式的话,欢迎评论区交流。

本文中的源码我已放到 GitHub仓库 ,如果你觉得此文对你有帮助的话,求赞求star 🌟。如果你在工作中有其他关于表单的问题,欢迎提出来交流一下,我会在这个仓库里继续更新 Ant Design 表单相关的问题。