如何基于 Ant Design <Form> 创建动态表单?

524 阅读3分钟

背景

我目前的工作是负责开发和维护公司内部管理系统,其中要涉及大量表单的建设,全部是基于 Ant Design 这一套进行开发的。

Ant Design 围绕 <Form> 元素开发的表单系统已经相当完善了,而表单模块中最灵活、有趣的一个功能就是动态表单的构建了,今天就带大家来学学它。

首先,我们先创建好一个 Ant Design 初始项目,很快的。

安装 Ant Design

使用 Vite 创建项目,安装 Ant Design React 依赖。

创建项目:

npm create vite antd-demo -- --template react
cd antd-demo

安装依赖:


# 安装 antd 包
npm install antd --save
npm install

使用 VS Code 打开:

code .

删除 src/index.css 中的内容,修改 src/App.jsx 文件内容如下:

import { Button } from 'antd';

const App = () => (
  <div className="App">
    <Button type="primary">Button</Button>
  </div>
);

export default App;

启动项目,浏览器访问。

$ npm run dev

> antd-demo@0.0.0 dev
> vite


  VITE v5.3.1  ready in 466 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

效果:

如此,我们便完成了 Ant Design 项目的搭建。

接下来,就来学习动态表单的内容。

表单基础知识

阅读官当文档《Form 表单》一节时,会看到关于“动态增减表单项”的内容讲解。

在 Ant Design 表单系统中,所有的表单内容都是包含在 <Form> 之中的。

<Form>
 {/* 表单内容 */}
</Form>

一般表单内是如下三层结构。

<Form>
  <Form.Item>
    <Input />
  </Form.Item>
</Form>

以下就是一个简单的账号密码登录案例:

import { Form, Input, Button, Checkbox } from 'antd';

const App = () => {
  const onFinish = (values) => {
    console.log('Success:', values);
  }

  return (
    <div className="App">
      <Form
        name="basic"
        initialValues={{ remember: true }}
        onFinish={onFinish}
        autoComplete="off"
      >
        <Form.Item
          label="Username"
          name="username"
          rules={[{ required: true, message: 'Please input your username!' }]}
        >
          <Input />
        </Form.Item>

        <Form.Item
          label="Password"
          name="password"
          rules={[{ required: true, message: 'Please input your password!' }]}
        >
          <Input.Password />
        </Form.Item>

        <Form.Item
          name="remember"
          valuePropName="checked"
        >
          <Checkbox>Remember me</Checkbox>
        </Form.Item>

        <Form.Item>
          <Button type="primary" htmlType="submit">
            Submit
          </Button>
        </Form.Item>
      </Form>
    </div>
  )
}

展示效果:

补充数据,点击“Submit”,就能在控制台看到提到的数据了!

是不是很方便呢?

当然,有时候由于业务需要,我们的 UI 需要支持动态添加。上面提前写好的组织方式就不适应了。

动态表单初印象

一旦涉及到动态表单的构建,就要用到 <Form.List> 这个组件。

<Form>
  <Form.List>
    {/* ... */}
  </Form.List>
</Form>

<Form.List> 类似循环组件,其 children 是一个函数。

<Form>
  <Form.List>
    {(fields, { add, remove, move }, { errors }) => (
      <>
        {/* ... */}
      </>
    )}
  </Form.List>
</Form>

这个函数接受 3 个参数:

  1. 第一个参数就是我们要渲染的动态数组 fields,这就是 <Form.List> 中我们要循环遍历的对象
  2. 第二个参数是一个对象,你能从中解构出 add、remove 和 move 三个函数,它们都是用来改变 fields 的
  3. 第三个参数则可以解构出错误列表 errors,它是配合 Form.List 的 rules 一起使用的

好了,有了这些储备,我们就可以写一个简单的 DEMO 了先。

import { Form, Input, Button, Space } from 'antd';

const App = () => {
  const onFinish = (values) => {
    console.log('Success:', values);
  }

  return (
    <div className="App">
      <Form
        name="basic"
        onFinish={onFinish}
        autoComplete="off"
        initialValues={{ options: [] }}
      >
        <Form.List name="options">
          {(fields, { add, remove }) => (
            <>
              {fields.map((field, index) => (
                <Space align="baseline" key={field.key}>
                  <Form.Item {...field} key={field.key} label={`选项 ${index + 1}`}>
                    <Input placeholder="选项内容" />
                  </Form.Item>
                  {fields.length > 1 ? (
                    <Button type="dashed" onClick={() => remove(field.name)}>
                      删除选项
                    </Button>
                  ) : null}
                  <p>{JSON.stringify(field)}</p>
                </Space>
              ))}
              <Form.Item>
                <Button type="dashed" onClick={() => add()}>
                  增加选项
                </Button>
              </Form.Item>
            </>
          )}
        </Form.List>
        <Form.Item>
          <Button type="primary" htmlType="submit">
            提交
          </Button>
        </Form.Item>
      </Form>
    </div>
  )
}

这里需要说明的是。

  1. 动态表单 <Form.List> 我们给取名 options,还在 <Form> 中做了初始化,初始化为一个空数组
  2. 增加项目后,至少会保留一个项目不被删除

下面,我们来看下渲染效果:

现在点击 1 次“增加选项”:

我们能在右侧看到 filed 项的数据结构:{"name":0,"key":0,"isListField":true,"fieldKey":0}。这里的 name/key 对应当前项在数组中的索引值,可是值相同但含义不同——name 用于当前值的设置,key 则用于唯一标识,确保渲染无误。

再点击 1 次“增加选项”:

增加了一个 filed 项目:{"name":1,"key":2,"isListField":true,"fieldKey":2}。

点击“提交”:

填写数据再“提交”:

以上,我们就实现了一个简易版本的动态表单删减能力。

更复杂一点的动态表单

前一个例子,每个 field 只收集一个字符串参数。更复杂一些,可以用来收集对象参数。

import { Form, Input, Button, Space } from 'antd';

const App = () => {
  const onFinish = (values) => {
    console.log('Success:', values);
  }

  return (
    <div className="App">
      <Form
        name="basic"
        onFinish={onFinish}
        autoComplete="off"
        initialValues={{ users: [] }}
      >
        <Form.List name="users">
          {(fields, { add, remove }) => (
            <>
              {fields.map(({ key, name, ...restField }) => (
                <Space align="baseline" key={key}>
                  <Form.Item {...restField} key={`${key}-first`} name={[name, 'first']} rules={[{ required: true, message: 'Missing first name' }]}>
                    <Input placeholder="First Name" />
                  </Form.Item>
                  <Form.Item {...restField} key={`${key}-last`} name={[name, 'last']} rules={[{ required: true, message: 'Missing last name' }]}>
                    <Input placeholder="Last Name" />
                  </Form.Item>
                  <Button type="dashed" onClick={() => remove(name)}>
                    -
                  </Button>
                  <p>{JSON.stringify({ key, name, ...restField })}</p>
                </Space>
              ))}
              <Form.Item>
                <Button type="dashed" onClick={() => add()}>
                  +
                </Button>
              </Form.Item>
            </>
          )}
        </Form.List>
        <Form.Item>
          <Button type="primary" htmlType="submit">
            提交
          </Button>
        </Form.Item>
      </Form>
    </div>
  )
}

展示效果:

直接点击“Submit”,就能看到校验信息!

补充完信息后,校验信息就没了,点击“Submit”,就能看到提交的数据了。

数据结构类型:{"users":[{"first":"a","last":"b"},{"first":"c","last":"d"}]}。

当然,如果你想在初始时,就先展示一个项目,可以修改 <Form>initialValues prop。

<Form
-  initialValues={{ users: [] }}
+  initialValues={{ users: [{}] }}
>

效果如下:

这个小技巧还是很有帮助的,大多数情况下我们都需要这样展示。

嵌套动态表单的创建

还有一个超复杂一点动态表单创建——一个 <Form.List> 嵌套一个 <Form.List>,跟套娃似的。

最终效果如下:

首先我们先绘制最外层的 Group 列表:

import { Form, Input, Button, Space, Card, Typography } from 'antd';

const App = () => {
  const [form] = Form.useForm()

  return (
    <div className="App">
      <Form
        form={form}
        autoComplete="off"
        initialValues={{ groups: [{}] }}
      >
        <Form.List name="groups">
          {(fields, { add, remove }) => (
            <>
              {fields.map(({ key, name, ...restField }, index) => (
                <Card
                  key={key}
                  title={`Group ${index + 1}`}
                  extra={(
                    <Button onClick={() => remove(name)}>×</Button>
                  )}
                >
                  <Form.Item {...restField} key={`${key}-group-name`} label="Name" name={[name, 'name']} rules={[{ required: true }]}>
                    <Input />
                  </Form.Item>
                  <Form.Item {...restField} key={`${key}-group-users`} label="Users" required>
                    {/* 嵌套 Form.List */}
                  </Form.Item>
                </Card>
              ))}
              <Form.Item style={{ marginTop: '1rem' }}>
                <Button type="dashed" block onClick={() => add()}>
                  + Add Group
                </Button>
              </Form.Item>
            </>
          )}
        </Form.List>
      </Form >
    </div >
  )
}

效果如下:

然后再将 {/* 嵌套 Form.List */} 下方增加嵌 User 列表:

<Form.Item {...restField} key={`${key}-group-users`} label="Users" required>
  {/* 嵌套 Form.List */}
  <Form.List name={[name, 'users']}>
    {(subFields, { add: subAdd, remove: subRemove }) => (
      <>
        {subFields.map((subFiled) => (
          <div key={subFiled.key}>
            <Space align="baseline" key={subFiled.key}>
              <Form.Item {...subFiled} key={`${subFiled.key}-first`} name={[subFiled.name, 'first']} rules={[{ required: true }]}>
                <Input placeholder="First Name" />
              </Form.Item>
              <Form.Item {...subFiled} key={`${subFiled.key}-last`} name={[subFiled.name, 'last']} rules={[{ required: true }]}>
                <Input placeholder="Last Name" />
              </Form.Item>
              <Button onClick={() => subRemove(subFiled.name)}>
                ×
              </Button>
            </Space>
          </div>
        ))}
        <Form.Item>
          <Button type="dashed" block onClick={() => subAdd()}>
            + Add User
          </Button>
        </Form.Item>
      </>
    )}
  </Form.List>
</Form.Item>

效果如下:

为了便于观察 Form 表单的值,我们再添加一个预览区域。

<Form
  form={form}
  autoComplete="off"
  initialValues={{ groups: [{}] }}
>
  <Form.List name="groups">
    {/* ... */}
  </Form.List>
  <Form.Item noStyle shouldUpdate>
    {() => (
      <Typography>
        <pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre>
      </Typography>
    )}
  </Form.Item>
</Form>

效果如下:

填写一些数据,观察表单数据结构:

总结

本文带大家了解了 Antd Design <Form> 表单关于动态表单构建的内容。

主要分成 3 块内容:

  1. 基本动态表单构建(成员是简单类型值)
  2. 复杂一点的动态表单构建(成员是对象类型值)
  3. 更复杂一点的动态表单构建(<Form.List> 嵌套 <Form.List>

基本上涵盖了关于动态表单构建的所有内容。

当然,纸上得来终觉浅。大家可以实地跟随本文内容,自己手动敲写一遍,更能起到学习的目的。

希望本文对你的工作会有所帮助,后面有机会我还会更新关于 Ant Design 表单部分的其他内容。感谢你的阅读,再见。