Antd-FormList嵌套Table实践

70

需求说明

  • 使用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,如下图所示。

formlistfields

注意,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;