React+Antd 实现添加和编辑表单form复用

3,012 阅读6分钟

假设,我们要使用react + Antd(4.24.8)实现如下的添加和编辑用户的功能,同时要做到复用同一个表单组件分别实现添加和编辑

image.png

我们可以给出如下的代码和涉及的知识:

  • useState, useEffect, useRef,forwardRef, useCallback的使用
  • antd表单组件的使用和封装
  • 利用react的effect机制刷新状态
  • antd Button, Space, Table, message, Switch, Modal, Form, Input, Select组件的使用

贴出代码并给出具体介绍:

UserList.js

UserList组件是承载添加和编辑按钮的组件,根据你的业务把两个按钮对应的点击事件处理好就行,下面给出触发添加和编辑的核心代码和JSX结构

import React, { useState, useEffect, useRef } from 'react'
import { Button, Space, Table, message, Switch, Modal } from 'antd'
import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
import api from '../../../api'
import UserFrom from '../../../components/user-manage/userFrom'
export default function UserList() {
  const [userList, setUserList] = useState([])
  const [roleList, setRoleList] = useState([])
  const [regionList, setRegionList] = useState([])
  const [curUserNode, setCurUserNode] = useState({})
  const [isOpen, setIsOpen] = useState(false)
  const [isAdd, setIsAdd] = useState(true)
  const [confirmLoading, setConfirmLoading] = useState(false)
  const addFrom = useRef(null)
  useEffect(() => {
    getUserList()
    getRoleList()
    getRegionList()
  }, [])

  const columns = [
    {
      title: '操作',
      key: 'handle',
      render: item => {
        return (
          <Space size="middle">
            <Button
              danger
              shape="circle"
              icon={<DeleteOutlined />}
              disabled={item.default}
              onClick={() => confirmDelete(item)}
            />
            <Button
              type="primary"
              shape="circle"
              icon={<EditOutlined />}
              disabled={item.default}
              onClick={() => {
                setCurUserNode(item)
                setIsAdd(false) //不是add则是update
                setIsOpen(true)
              }}
            />
          </Space>
        )
      }
    }
  ]

  const getUserList = async () => {
    const { data: res } = await api.getUserList().catch(() => message.error('数据请求失败'))
    setUserList(res)
  }

  const getRoleList = async () => {
    const { data: res } = await api.getRoleList().catch(() => message.error('数据请求失败'))
    setRoleList(res)
  }

  const getRegionList = async () => {
    const { data: res } = await api.getRegionList().catch(() => message.error('数据请求失败'))
    setRegionList(res)
  }


  const handleAddOk = addFrom => {
    setConfirmLoading(true)
    const form = addFrom.current
    form.validateFields().then(value => {
      api
        .addUser({
          ...value,
          roleState: true,
          default: value.roleId === 1 ? true : false
        })
        .then(() => {
          getUserList()
          setConfirmLoading(false)
          setIsOpen(false)
          message.success('添加成功')
        })
        .catch(() => message.error('添加失败'))
    })
  }

  const handleEditOk = addFrom => {
    setConfirmLoading(true)
    const form = addFrom.current
    form.validateFields().then(value => {
      api
        .editUser(
          {
            ...value,
            roleState: true,
            default: value.roleId === 1 ? true : false
          },
          curUserNode.id
        )
        .then(() => {
          getUserList()
          setConfirmLoading(false)
          setIsOpen(false)
          message.success('修改成功')
        })
        .catch(() => message.error('修改失败'))
    })
  }

  return (
    <div>
      <Button
        type="primary"
        onClick={() => {
          setIsAdd(true)
          setIsOpen(true)
        }}
      >
        添加用户
      </Button>
      <Table dataSource={userList} columns={columns} rowKey={item => item.id} />

      <Modal
        open={isOpen}
        title={isAdd ? '添加用户' : '编辑用户'}
        okText={isAdd ? '确认添加' : '确认修改'}
        cancelText="取消"
        confirmLoading={confirmLoading}
        onCancel={() => {
          setIsOpen(false)
          setIsAdd(true)
        }}
        onOk={() => {
          return isAdd ? handleAddOk(addFrom) : handleEditOk(addFrom)
        }}
      >
        <UserFrom
          ref={addFrom}
          roleList={roleList}
          regionList={regionList}
          isAdd={isAdd}
          curUser={curUserNode}
        ></UserFrom>
      </Modal>
    </div>
  )
}

userForm.js

UserForm是封装的表单组件,根据传入参数决定是添加还是编辑

import React, { forwardRef, useCallback, useEffect, useState } from 'react'
import { Form, Input, Select } from 'antd'
const userFrom = forwardRef((props, ref) => {
  const [isDisabled, setIsDisabled] = useState(false)

  const { regionList, roleList, isAdd, curUser } = props
  const [form] = Form.useForm()
  const handleRoleChange = useCallback(
    value => {
      if (value === 1) {
        setIsDisabled(true)
        form.setFieldValue('region', '')
      } else {
        setIsDisabled(false)
      }
    },
    [form]
  )

  const initForm = useCallback(() => {
    if (isAdd) {
      form.resetFields()
      setIsDisabled(false)
    } else {
      form.setFieldsValue(curUser)
      handleRoleChange(curUser.roleId)
    }
  }, [form, isAdd, curUser, handleRoleChange])

  useEffect(() => {
    initForm()
    //
  }, [initForm])

  return (
    <Form form={form} ref={ref} layout="vertical">
      <Form.Item
        name="username"
        label="用户名"
        rules={[
          {
            required: true,
            message: '请输入用户名'
          }
        ]}
      >
        <Input />
      </Form.Item>
      <Form.Item
        name="password"
        label="密码"
        rules={[
          {
            required: true,
            message: '请输入密码'
          }
        ]}
      >
        <Input />
      </Form.Item>
      <Form.Item
        name="roleId"
        label="角色"
        rules={[
          {
            required: true,
            message: '请选择角色'
          }
        ]}
      >
        <Select
          style={{
            width: 150
          }}
          fieldNames={{ label: 'roleName', value: 'id' }}
          onChange={value => {
            handleRoleChange(value)
          }}
          options={roleList}
        />
      </Form.Item>
      <Form.Item
        name="region"
        label="区域"
        rules={
          isDisabled
            ? ''
            : [
                {
                  required: true,
                  message: '请选择区域'
                }
              ]
        }
      >
        <Select
          style={{
            width: 150
          }}
          disabled={isDisabled}
          fieldNames={{ label: 'title', value: 'value' }}
          options={regionList}
        />
      </Form.Item>
    </Form>
  )
})

export default userFrom

具体解释

1.我们先来看一下Modal模态框的代码

<Modal
        open={isOpen}
        title={isAdd ? '添加用户' : '编辑用户'}
        okText={isAdd ? '确认添加' : '确认修改'}
        cancelText="取消"
        confirmLoading={confirmLoading}
        onCancel={() => {
          setIsOpen(false)
          setIsAdd(true)
        }}
        onOk={() => {
          return isAdd ? handleAddOk(addFrom) : handleEditOk(addFrom)
        }}
      >

根据isOpen设置显示隐藏,根据isAdd属性判断是添加还是编辑,设置confirmLoading选择为异步处理模式(就是点击确定后,模态框会等待,需要请求完成后才会关闭)

然后绑定好确定和取消的回调,在取消时,将状态重置到添加状态,有利于后续清空表单的值; 在确定时,根据isAdd调对应的回调函数,这两个回调函数在后面解释。

2.然后,我们需要创建出对应添加和编辑的两个触发按钮并设置好点击事件

添加Button:这里我们设置了两个状态add和open,表示当前是添加操作且打开modal框

<Button
        type="primary"
        onClick={() => {
          setIsAdd(true)
          setIsOpen(true)
        }}
      >
        添加用户
</Button>

编辑Button:嵌入在Table组件中,点击后主要做了三件事,第一将当前选中的用户对象设置到状态,方便后续传入Form组件

(这里的参数item是antd回传的,item就是Table组件中当前选中行所对应的数据源中的对象)

<Table dataSource={userList} columns={columns} rowKey={item => item.id} />

然后设置isAdd为false表示编辑状态,设置isOpen为true打开模态框

const columns = [
    {
      title: '操作',
      key: 'handle',
      render: item => {
        return (
          <Space size="middle">
            <Button
              danger
              shape="circle"
              icon={<DeleteOutlined />}
              disabled={item.default}
              onClick={() => confirmDelete(item)}
            />
            <Button
              type="primary"
              shape="circle"
              icon={<EditOutlined />}
              disabled={item.default}
              onClick={() => {
                setCurUserNode(item)
                setIsAdd(false) //不是add则是update
                setIsOpen(true)
              }}
            />
          </Space>
        )
      }
    }
  ]

3.接下来将UserForm组件包裹在Modal组件中

<UserFrom
          ref={addFrom}
          roleList={roleList}
          regionList={regionList}
          isAdd={isAdd}
          curUser={curUserNode}
></UserFrom>

这里我们传入了很多属性,一一进行解释

ref:我们使用const addFrom = useRef(null)创建了一个引用,并绑定到ref属性上,为了在UserForm组件内部使用forwardRef进行透传,将UserForm组件进行包裹:

const userFrom = forwardRef((props, ref) => {
  const [form] = Form.useForm()
  return (
    <Form form={form} ref={ref} layout="vertical"></Form>
  )
})

export default userFrom

这样可以在第二个参数中获得ref,并将透传的ref绑定在Form组件上,这样在父组件UserList中就可以拿到Form的引用了

roleList、regionList:表单项需要用到的数据源

isAdd:标识添加编辑

curUser:当前选中的用户,编辑状态下进行数据的回填

OK,接下来让我们进行UserForm组件的编写

4.具体表单项的编码请查询上方详细代码,这里具体解释需要进行联动的两个表单项

这里有一个具体的业务需求:当角色为超级管理员时,则不需要选择区域了,需要禁用掉区域select,当选择其他角色时,区域select又需要打开,同时也需要同步修改验证规则,因为我们设置了不能为空

<Form.Item
        name="roleId"
        label="角色"
        rules={[
          {
            required: true,
            message: '请选择角色'
          }
        ]}
      >
        <Select
          style={{
            width: 150
          }}
          fieldNames={{ label: 'roleName', value: 'id' }}
          onChange={value => {
            handleRoleChange(value)
          }}
          options={roleList}
        />
      </Form.Item>
      <Form.Item
        name="region"
        label="区域"
        rules={
          isDisabled
            ? ''
            : [
                {
                  required: true,
                  message: '请选择区域'
                }
              ]
        }
      >
        <Select
          style={{
            width: 150
          }}
          disabled={isDisabled}
          fieldNames={{ label: 'title', value: 'value' }}
          options={regionList}
        />
      </Form.Item>

于是,这里我们通过isDisabled状态来控制禁用状态,对角色select绑定监听事件handleRoleChange(value),逻辑比较简单,就是判断选择为超级管理员时,设置禁用状态并将region区域置为空即可

const handleRoleChange = useCallback(
    value => {
      if (value === 1) {
        setIsDisabled(true)
        form.setFieldValue('region', '')
      } else {
        setIsDisabled(false)
      }
    },
    [form]
  )

5.接下来是非常重要的一步,如何判断是添加还是编辑呢?并且做出对应的逻辑处理?

const initForm = useCallback(() => {
    if (isAdd) {
      form.resetFields()
      setIsDisabled(false)
    } else {
      form.setFieldsValue(curUser)
      handleRoleChange(curUser.roleId)
    }
  }, [form, isAdd, curUser, handleRoleChange])

  useEffect(() => {
    initForm()
    //
  }, [initForm])

我们创建了一个initForm函数,该函数首先判断传入的isAdd

如果是添加:则重置表单属性,并取消禁用

如果是编辑:根据传入的curUser回填属性,这里注意,我们又调了一次handleRoleChange方法,这里的目的就是需要根据选择的数据判断是否需要禁用区域选择框。

initForm这一步的操作非常重要,因为我们在useEffect中依赖设置了initForm,而initForm的依赖又设置了isAdd, curUser,这样每次点击添加或者编辑,传入的props就会改变,从而引起initForm改变,从而引起useEffect重新执行

这一切都是为了每次点击编辑和添加时,可以获得对应的最新状态,如果不这样操作,当点击了编辑后,联动效果无法产生,同时回填的数据也会保存,再点击添加时,数据就会回显了,这显然不符合预期

6.这一切都完成后,数据我们就处理完了,只需要分别去调用添加和编辑的回调就可以了

  const handleAddOk = addFrom => {
    setConfirmLoading(true)
    const form = addFrom.current
    form.validateFields().then(value => {
      api
        .addUser({
          ...value,
          roleState: true,
          default: value.roleId === 1 ? true : false
        })
        .then(() => {
          getUserList()
          setConfirmLoading(false)
          setIsOpen(false)
          message.success('添加成功')
        })
        .catch(() => message.error('添加失败'))
    })
  }

  const handleEditOk = addFrom => {
    setConfirmLoading(true)
    const form = addFrom.current
    form.validateFields().then(value => {
      api
        .editUser(
          {
            ...value,
            roleState: true,
            default: value.roleId === 1 ? true : false
          },
          curUserNode.id
        )
        .then(() => {
          getUserList()
          setConfirmLoading(false)
          setIsOpen(false)
          message.success('修改成功')
        })
        .catch(() => message.error('修改失败'))
    })
  }

其实逻辑都很简单,只是调用的接口不同

首先setConfirmLoading打开loading,然后通过ref引用拿到表单对象 const form = addFrom.current

接着调用form.validateFields()进行表单验证,这是一个Promise,在then回调中调用对应的接口,接口调用成功后,在接口的then回调中取消loading,并在此请求user数据源刷新页面,关闭模块框即可!

刚开始写文,可能很多地方没有说清楚,欢迎朋友们指正!