阅读 1256

复杂表单使用Form.List案例

好久不见,小编携文来扰!🔨

在最近研发的项目中,小编遇到一个比(you)较(dian)复(bian)杂(tai)的表单需求。讲真不难,就是构造数据和数据处理有点麻烦。起初画好页面后,以为万事大吉,直接掉接口塞数据就成。直到看到后端模型定义好的传参格式,一时间竟有点懵,最后只能推翻重画...

整理这篇文章的初衷,是感觉其实项目中会经常遇到这类复杂的表单,有必要记录一下,便于以后遇到类似的需求写起来有个参考。

使用组件库: ant design

需求描述: 批量选取Table中的商家,并为各个商家配置一个或多个车型和车辆营运类型。

图示:

image.png

image.png

大致流程: 选取商家确定后,将选好的商家遍历注入到表单中,并按相关需求整理好数据提交。

正文

1.首先看下接口模型定义的格式(如下):

carWithCompany: [{        // object []
  travelCompanyId: '',    // string
  carModelList: [{        // object []
    carModel: '',         // string
    operationType: []     // string[]
  }]
}]
复制代码
思考:

最初,小编刚开始没有拿接口定义的格式时,直接用map遍历选取的商家,再内嵌一个Form.List来实现内部选取的车型&车辆营运类型。但是看后端接口模型定义好的格式后,发现这么处理数据会很麻烦。后来索性直接用Form.List内嵌Form.List来实现。但是遇到一个问题:最外层的Form.List是不需要添加按钮去添加条数的,它的数量是通过已选取的商家数量去控制。

机智的小编去请教了前端大佬,大佬反手就是一个demo丢过来(👍)。看了后发现想起来useRef是个好东西(平常比较少用)。

2.给Table造点数据

const [data, setData] = useState([
  {
    id: "1",
    name: "商家1",
    alias: "简称1",
    type: "自营"
  },
  {
    id: "2",
    name: "商家2",
    alias: "简称2",
    type: "自营"
  },
  {
    id: "3",
    name: "商家3",
    alias: "简称3",
    type: "自营"
  }
]);
复制代码

3.接下来是处理Modal弹窗中多选的数据,并遍历到Form.List中,这里需要重点关注下useRef的使用(多选选取的条数需要替换原本通过使用Form.List的添加方法手动添加的条数)。具体核心代码如下:

// 每次选中条数变通过useRef出发Form.List方法更新
useEffect(() => {
  const arr = company.selectedRows;
  arr?.map(() => {
    return addRef.current();
  });
}, [company.selectedRows]);

// 处理firstFields,传入相应选中数据
addRef.current = add;
firstFields.map((field, index) => {
  return (field.record = company.selectedRows[index]);
});
let result = firstFields?.filter((item) => item.record);
复制代码

页面代码如下:

import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import {Form, Input, Button, Space, Modal, Table, message, Select} from "antd";
import { DeleteOutlined, CloseOutlined } from "@ant-design/icons";

const Demo = () => {
  const [form] = Form.useForm();
  const addRef = useRef(null);
  const [visible, setVisible] = useState(false);
  const [company, setCompany] = useState(
    {
      selectedRows: [],
      tmpRows: []
    },
    []
  );
  const [data, setData] = useState([
    {
      id: "1",
      name: "商家1",
      alias: "简称1",
      type: "自营"
    },
    {
      id: "2",
      name: "商家2",
      alias: "简称2",
      type: "自营"
    },
    {
      id: "3",
      name: "商家3",
      alias: "简称3",
      type: "自营"
    }
  ]);

  const columns = [
    {
      title: "商家名称",
      dataIndex: "name"
    },
    {
      title: "商家简称",
      dataIndex: "alias"
    },
    {
      title: "商家运营类型",
      dataIndex: "type"
    }
  ];

  const onFinish = (values) => {
    console.log("Received values of form:", values);
  };

  const handleClick = () => {
    setVisible(true);
  };

  const handleOk = () => {
    if (company.tmpRows?.length === 0) {
      message.error("请选择商家");
      return false;
    } else {
      setCompany({        //处理确认选中的商家
        selectedRows: company.tmpRows.slice(),
        tmpRows: company.tmpRows
      });
      setVisible(false);
    }
  };

  const handleCancel = () => {
    setVisible(false);
  };

  useEffect(() => {        // 解释如上
    company.selectedRows?.map(() => {
      return addRef.current();
    });
  }, [company.selectedRows]);

  return (
    <section>
      <Form form={form} onFinish={onFinish} autoComplete="off">
        <Form.Item>
          <Button type="primary" onClick={handleClick}>
            选择商家
          </Button>
        </Form.Item>
        {company.selectedRows?.length > 0 && (
          <Form.List name="carWithCompany">
            {(firstFields, { add, remove }) => {
              addRef.current = add;
              firstFields.map((field, index) => {
                return (field.record = company.selectedRows[index]);
              });  
              let result = firstFields?.filter((item) => item.record);  // 过滤record是undefiend的情况
              return (
                <>
                  {result.map((firstField, i) => {
                    return (
                      <div key={firstField.key}>
                        <div style={{ display: 'inline-block' }}>
                          <h4>商家名称</h4>
                          <p>{firstField.record?.name}</p>
                          <Form.Item
                            name={[firstField.name, "id"]}
                            initialValue={firstField.record?.id}
                            style={{ height: "60%" }}>
                            <Input style={{ display: "none" }} />
                          </Form.Item>
                        </div>
                        <div style={{ display: 'inline-block' }}>
                          <div>
                            <span style={{display: 'inline-block', marginRight: 250}}>车型</span>
                            <span>车辆营运类型</span>
                          </div>
                          <Form.List name={[firstField.name, "carModelList"]}>
                            {(secondfields, { add, remove }) => (
                              <>
                                {secondfields.map(
                                  ({ key, name, fieldKey, ...restField }) => (
                                    <Space
                                      key={key}
                                      align="baseline">
                                      <Form.Item
                                        {...restField}
                                        name={[name, "carModel"]}
                                        rules={[
                                          {
                                            required: true,
                                            message: "请选择车型"
                                          }
                                        ]}>
                                        <Select style={{ width: 150 }}>
                                          <Select.Option value="0001"> 1车型 </Select.Option>
                                          <Select.Option value="0002">2车型</Select.Option>
                                          <Select.Option value="0003">3车型</Select.Option>
                                        </Select>
                                      </Form.Item>
                                      <Form.Item
                                        style={{ marginLeft: 26 }}
                                        {...restField}
                                        name={[name, "operationType"]}
                                        rules={[
                                          {
                                            required: true,
                                            message: "请选择营运类型"
                                          }
                                        ]}>
                                        <Select
                                          mode="multiple"
                                          style={{ width: 250 }}>
                                          <Select.Option value="0001">01</Select.Option>
                                          <Select.Option value="0002">02</Select.Option>
                                          <Select.Option value="0003">03</Select.Option>
                                        </Select>
                                      </Form.Item>
                                      <DeleteOutlined onClick={() => remove(name)} />
                                    </Space>
                                  )
                                )}
                                <Form.Item>
                                  <Button
                                    style={{ marginLeft: 24 }}
                                    type="primary"
                                    onClick={() => add()}  // 添加车型&营运类型
                                  >
                                  添加  
                                  </Button>
                                </Form.Item>
                              </>
                            )}
                          </Form.List>
                        </div>
                        <CloseOutlined   // 整条数据删除
                        style={{  position: 'absolute',display: 'inline-block', margin: '16px 24px 0 0'}}
                          onClick={() => {
                            remove(firstField.name);
                            company.selectedRows?.splice(i, 1);
                            company.tmpRows?.splice(i, 1);
                            result.splice(i, 1);
                            setCompany({
                              selectedRows: company.selectedRows,
                              tmpRows: company.tmpRows
                            });
                          }}
                        />
                      </div>
                    );
                  })}
                </>
              );
            }}
          </Form.List>
        )}

        <Form.Item>
          <Button type="primary" htmlType="submit">提交</Button>
        </Form.Item>
      </Form>
      <Modal
        title="选择商家组织"
        visible={visible}
        onCancel={handleCancel}
        onOk={handleOk}
      >
        <Table
          columns={columns}
          dataSource={data}
          rowKey={(row) => row.id}
          rowSelection={{
            selectedRowKeys: company.tmpRows.map((item) => item.id),
            onSelect: (row, selected) => {
              let old = company.tmpRows.slice();
              if (selected) old.push(row);
              else old = old.filter((item) => item.id !== row.id);
              setCompany({ tmpRows: old });
            },
            onSelectAll: (selected, rows, changeRows) => {
              let old = company.tmpRows.slice();
              if (selected) old.push(...changeRows);
              else {
                old = old.filter((item) => {
                  for (const row of changeRows)
                    if (row.id === item.id) return false;
                  return true;
                });
              }
              setCompany({ tmpRows: old });
            }
          }}
          pagination={{
            total: data.length,
            showTotal: (total) => `共${total}条数据`
          }}
        />
      </Modal>
    </section>
  );
};

ReactDOM.render(<Demo />, document.getElementById("container"));
复制代码

代码很长,但是为了能更快的拿代码去编译,更直观的看,小编就没采取附上gitlab链接,而是选择直接粘上,有强迫症的童鞋可忽略。

最终demo表单的效果图如下:

image.png

这么看图片有点朴实无华,样式什么的需要的话自己去画吧,时间紧任务重。

最终处理的数据结构如下:

image.png

这样,就构造好了后端小哥需要的数据结构啦(嘿嘿),可直接使用antdcodesandbox打开。

注意

最后,需要注意的几点问题:

  1. 删除重新添加

选好Table中的数据map到Form.List,进行删除操作,再另行选取数据的时候,可能会出现以下情况:

image.png

这时候就需要过滤一下。代码中 let result = firstFields?.filter((item) => item.record);的作用就是处理过滤。

2.数据回显问题

当编辑的时候表单需要的数据需要回显,这时候处理嵌套的Form.List数据,使其一一对应。小编处理的方式是这部分数据map时候return两次,可能说的抽象了点,大致代码如下:

result.map(item => {
  return {
    b: item.id,
    c: item.content?.map(items => {
      return {
        d: items.car,
        e: items.type
      }
    })
  }
})
复制代码

到这里其实也就差不多了,文章写的比较匆忙,有些东西估计没注意到,但功能大体的代码逻辑是没有啥大问题的,如需自取吧哈哈。

文末:附上一小段前段时间跟前端大佬的对话哈哈哈

前端大佬:你要举一反三

小编: 我反了, 反不动。。

很幸运,工作中遇到了有趣的人,亦师亦友,给他比个❤️吧嘿嘿

文章分类
前端
文章标签