使用Antd实现可编辑单元格&表格

发布于 2022-03-13  12 次阅读


近日在项目中碰到了实现一个带可编辑单元格和支持弹窗编辑的表格的需求,查阅Antd的官方文档,觉得官方的实现很规整,但稍有些繁琐,这里简化一个版本,便于上手。

需求细化


假设有如图所示的表格,现在需要将Grade字段改为<InputNumber>组件,要求每次修改数值都会将更改后的值存储到本地state内。此外,在点击铅笔按钮后会打开模态窗,用户同样可以在模态窗内对数值进行修改,修改同样需要被同步到本地state内。

核心思想

  • 封装一个可编辑单元格,利用<InputNumber>onChange钩子将数据写回存储。
  • 所有的可编辑单元格组件采用ant-table中columns的render函数实现。
  • 模态窗同样由ant-table中columns的render函数控制渲染,接受当前行作为props,和表格在同一数据源做修改。

实现可编辑单元格组件

const EditableInputNumber = (props) => {
  const { text, field, record, tableData, setTableData } = props
  const onValueChange = useCallback((value) => {
    let newData = [...tableData]
    const idx = newData.findIndex(item => item.displayId === record.displayId)
    if (idx !== -1) {
      newData[idx][field] = value
      setTableData(newData)
    }
  }, [tableData, field, record]);

  return (
    <>
      <InputNumber
        min={0}
        max={100}
        step={1}
        value={text}
        onChange={onValueChange}
      />
    </>
  )
}

在antd的columns内进行如下配置:

{
  title: 'Grade',
  dataIndex: 'grade',
  key: 'grade',
  render: (text, record) => {
  return (
    <EditableInputNumber
        text={text}
        record={record}
        field={'grade'}
        tableData={tableData}
        setTableData={setTableData}
    />
  )}
}

这样写的好处在于节省了大量使用官方文档配置Provider、表单、onRow方法的时间,缺点在于加深了组件之间的耦合度,但考虑到这类table在实际业务中并不会进行深度的复用,所以采用了这种简单粗暴的配置方式。

实现模态窗

在实现模态窗时有以下两点需要考虑:
+ 如何拿到当前行的数据
+ 如何将修改后的数据同步回数据源
我们使用antd的form组件来控制数据间的同步。通过Form.useForm()钩子获取到表单实例,再通过form.setFieldsValue()来同步当前行数据到表单内,和form.validateFields().then()来将提交后的表单数据同步回数据源中。具体实现代码如下:

const [editRowForm] = Form.useForm()
const [modalVisible, setModalVisible] = useState(false)
const [currentRow, setCurrentRow] = useState()
const [tableData, setTableData] = useState(dataSource)

// 编辑按钮的回调函数
const onRowEdit = useCallback((rowRecord) => {
  setCurrentRow(rowRecord)
  editRowForm.setFieldsValue(rowRecord)
  showEditModal()
})

// 表单提交按钮的回调函数
const onEditModalSubmit = useCallback(() => {
  editRowForm.validateFields().then((values) => {
    let newTableData = [ ...tableData ]
    const targetIndex = tableData.findIndex(record => record.displayId === currentRow.displayId)
    newTableData[targetIndex] = { ...values }
    setTableData(newTableData)
    closeEditModal()
  })
}, [tableData, currentRow])

// 模态窗组件
<Modal
  destroyOnClose={true}
  width='1000px'
  title='Edit Record'
  forceRender={true}
  onCancel={closeEditModal}
  visible={modalVisible}
  footer={
    <>
    <Button shape="round" onClick={closeEditModal}>
      Cancel
    </Button>
    <Button
      shape="round"
      type="primary"
      onClick={onEditModalSubmit}
    >
      Submit
    </Button>
    </>
  }>
  <Form name='editRowForm' form={editRowForm}>
    <span>
      <div>ID</div>
      <Form.Item
        name="displayId"
        initialValue={currentRow?.displayId}>
        <Input />
      </Form.Item>
    </span>
    <span>
      <div>Name</div>
      <Form.Item
        name="name"
        initialValue={currentRow?.name}>
        <Input />
      </Form.Item>
    </span>
      <span>
      <div>Grade</div>
      <Form.Item
        name="grade"
        initialValue={currentRow?.grade}>
        <InputNumber />
      </Form.Item>
    </span>
  </Form>
</Modal>

此外,还需要将编辑按钮(铅笔)的回调绑定到onRowEdit函数上。

{
  title: 'Actions',
  dataIndex: 'actions',
  key: 'actions',
  width: 120,
  render: (text, record) => {
    return (
      <>
        <Button
          type='text'
          icon={<EditOutlined/>}
          onClick={() => {onTopicEdit(record)}}
        />
        <Button
          type='text'
          icon={<DeleteOutlined/>}
        />
      </>
    )
  }
}

最终效果

可编辑单元格:

模态窗