近日在项目中碰到了实现一个带可编辑单元格和支持弹窗编辑的表格的需求,查阅Antd的官方文档,这里基于官方简化一个版本,便于上手。
需求细化
假设有如图所示的表格,现在需要将Grade字段改为<InputNumber>
组件,要求每次修改数值都会将更改后的值存储到本地state内。此外,在点击铅笔按钮后会打开模态窗,用户同样可以在模态窗内对数值进行修改,修改同样需要被同步到本地state内。
核心思想
- 封装一个可编辑单元格,利用
<InputNumber>
的onChange
钩子将数据写回存储。 - 所有的可编辑单元格组件采用ant-table中columns的
render
函数实现。 - 模态窗同样由ant-table中columns的
render
函数控制渲染,接受当前行作为props,和表格在同一数据源做修改。
实现可编辑单元格组件
这里将数据源简化成一个state存储,实际使用的时候可以Memo或者放到Redux,不再赘述。
const [tableData, setTableData] = useState(dataSource);
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={() => {onRowEdit(record)}}
/>
<Button
type='text'
icon={<DeleteOutlined/>}
onClick={() => {onRowDelete(record)}}
/>
</>
)
}
}