最近在項目復盤的時候,發現使用了antd可編輯單元格。用的時候沒仔細看,現在詳細看下實現原理。
antd可編輯單元格:https://ant.design/components/table-cn/#components-table-demo-edit-cell
項目復盤:https://www.cnblogs.com/shixiu/p/16011009.html
結論
實現思路
在table組件的某個單元格需要可編輯時,渲染form組件包裹單元格
實現細節
使用context將form的一些數據交互方法傳遞到單元格上
js代碼
import React, { useContext, useState, useEffect, useRef } from 'react';
import { Table, Input, Button, Popconfirm, Form } from 'antd';
const EditableContext = React.createContext(null);
const EditableRow = ({ index, ...props }) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
</Form>
);
};
const EditableCell = ({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = useState(false);
const inputRef = useRef(null);
const form = useContext(EditableContext);
useEffect(() => {
if (editing) {
inputRef.current.focus();
}
}, [editing]);
const toggleEdit = () => {
setEditing(!editing);
form.setFieldsValue({
[dataIndex]: record[dataIndex],
});
};
const save = async () => {
try {
const values = await form.validateFields();
toggleEdit();
handleSave({ ...record, ...values });
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};
let childNode = children;
if (editable) {
childNode = editing ? (
<Form.Item
style={{
margin: 0,
}}
name={dataIndex}
rules={[
{
required: true,
message: `${title} is required.`,
},
]}
>
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
</Form.Item>
) : (
<div
className="editable-cell-value-wrap"
style={{
paddingRight: 24,
}}
onClick={toggleEdit}
>
{children}
</div>
);
}
return <td {...restProps}>{childNode}</td>;
};
class EditableTable extends React.Component {
constructor(props) {
super(props);
this.columns = [
{
title: 'name',
dataIndex: 'name',
width: '30%',
editable: true,
},
{
title: 'age',
dataIndex: 'age',
},
{
title: 'address',
dataIndex: 'address',
},
{
title: 'operation',
dataIndex: 'operation',
render: (_, record) =>
this.state.dataSource.length >= 1 ? (
<Popconfirm title="Sure to delete?" onConfirm={() => this.handleDelete(record.key)}>
<a>Delete</a>
</Popconfirm>
) : null,
},
];
this.state = {
dataSource: [
{
key: '0',
name: 'Edward King 0',
age: '32',
address: 'London, Park Lane no. 0',
},
{
key: '1',
name: 'Edward King 1',
age: '32',
address: 'London, Park Lane no. 1',
},
],
count: 2,
};
}
handleDelete = (key) => {
const dataSource = [...this.state.dataSource];
this.setState({
dataSource: dataSource.filter((item) => item.key !== key),
});
};
handleAdd = () => {
const { count, dataSource } = this.state;
const newData = {
key: count,
name: `Edward King ${count}`,
age: '32',
address: `London, Park Lane no. ${count}`,
};
this.setState({
dataSource: [...dataSource, newData],
count: count + 1,
});
};
handleSave = (row) => {
const newData = [...this.state.dataSource];
const index = newData.findIndex((item) => row.key === item.key);
const item = newData[index];
newData.splice(index, 1, { ...item, ...row });
this.setState({
dataSource: newData,
});
};
render() {
const { dataSource } = this.state;
const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};
const columns = this.columns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record) => ({
record,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
handleSave: this.handleSave,
}),
};
});
return (
<div>
<Button
onClick={this.handleAdd}
type="primary"
style={{
marginBottom: 16,
}}
>
Add a row
</Button>
<Table
components={components}
rowClassName={() => 'editable-row'}
bordered
dataSource={dataSource}
columns={columns}
/>
</div>
);
}
}
ReactDOM.render(<EditableTable />, mountNode);
分析
class
可以看到這竟然是個class組件
構造函數
// 基本寫法
constructor(props) {
super(props);
...
}
this.columns
可以看到在構造函數了虛擬了列數據
this.columns = [
{
title: 'name',
dataIndex: 'name',
width: '30%',
editable: true, // 本列可編輯
},
{
title: 'age',
dataIndex: 'age',
},
{
title: 'address',
dataIndex: 'address',
},
{
title: 'operation',
dataIndex: 'operation',
render: (_, record) => // 這里的第二個參數record指代該行數據
this.state.dataSource.length >= 1 ? ( // 調用了state數據判斷,我們看完state和方法再回來。
<Popconfirm title="Sure to delete?" onConfirm={() => this.handleDelete(record.key)}>
<a>Delete</a>
</Popconfirm> // 該列固定戰術Delete字符,點擊時彈出氣泡確認,確認刪除時傳入該行key調用this.handleDelete方法刪除
) : null,
},
];
this.state
在這里模擬了表單數據
this.state = {
dataSource: [
{
key: '0',
name: 'Edward King 0',
age: '32',
address: 'London, Park Lane no. 0',
},
{
key: '1',
name: 'Edward King 1',
age: '32',
address: 'London, Park Lane no. 1',
},
],
count: 2,
};
方法
handleDelete
刪除方法,看多了函數組件看class的setState有點不適應。
很常見的刪除原理:比對該行key,然后用 dataSource.filter()方法設置狀態
handleDelete = (key) => {
const dataSource = [...this.state.dataSource];
this.setState({
dataSource: dataSource.filter((item) => item.key !== key),
});
};
handleAdd
增加方法。在這里模擬了一條新數據。
handleAdd = () => {
const { count, dataSource } = this.state;
const newData = {
key: count,
name: `Edward King ${count}`,
age: '32',
address: `London, Park Lane no. ${count}`,
};
this.setState({
dataSource: [...dataSource, newData],
count: count + 1,
});
};
handleSave
保存方法。應該是可編輯單元格編輯后調用保存吧。我們可以先去看render(){}內部
handleSave = (row) => {
const newData = [...this.state.dataSource];
const index = newData.findIndex((item) => row.key === item.key);
const item = newData[index];
newData.splice(index, 1, { ...item, ...row });
this.setState({
dataSource: newData,
});
};
render(){}
這里直接逐行注釋了
// 從state中解構出表單數據
const { dataSource } = this.state;
// components對象,描述了編輯行EditableRow方法與編輯單元格EditableRow方法
const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};
// 列數組處理:不可編輯的列直接返回值;可編輯列增加**onCell方法**,該方法返回該單元格數據、editable等屬性、以及保存方法
const columns = this.columns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record) => ({
record,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
handleSave: this.handleSave,
}),
};
});
// 返回一個增加行按鈕、一個table,tabe上有components屬性
return (
<div>
<Button
onClick={this.handleAdd}
type="primary"
style={{
marginBottom: 16,
}}
>
Add a row
</Button>
<Table
components={components}
rowClassName={() => 'editable-row'}
bordered
dataSource={dataSource}
columns={columns}
/>
</div>
);
EditableRow
可編輯行利用Context把Form表單的交互方法傳遞給了Table組件的每個單元格
// 創建一個Context對象,用來
const EditableContext = React.createContext(null);
const EditableRow = ({ index, ...props }) => {
const [form] = Form.useForm(); // antd表單方法,form對象擁有一些方法能實現交互,詳見參考1
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}> <!--透傳form方法至tr內部 -->
<tr {...props} />
</EditableContext.Provider>
</Form>
);
};
Context基本理解
Context 設計用來解決祖先組件向后代組件傳遞數據的問題(prop drilling)。為跨層級的組件搭建一座橋梁。
舉個例子:用戶登錄之后,很多組件需要拿到用戶相關信息,如果按照prop傳遞的方式獲取,會變得異常繁瑣,而且很難判斷數據的真正來源
而如果使用Context,則在Provider的后代組件的任意位置,都可以Consumer數據.
使用redux
不也可以解決這個問題嗎?答案是:你說的對。不過redux本身就是通過context實現的。
案例:
// 從react庫中引入createContext方法
import React, { Component, createContext } from 'react'
// 通過createContext方法創建用於存放用戶數據的Context類
const UserContext = createContext()
// 通過UserContext的屬性Provider(組件)發布用戶數據
// 傳遞給Provider的value屬性的值可以是任意數據類型,甚至是函數
class App extends Component {
state = {
user: {
name: 'abc',
},
}
render() {
const {
user,
} = this.state
return (
<UserContext.Provider value={user}>
<UseUserInfoComponent />
</UserContext.Provider>
)
}
}
// 通過UserContext的屬性Consumer(組件)消費用戶數據
// Consumer的子組件必須是一個function,通過function的參數接收頂層傳入的數據
class UseUserInfoComponent extends Component {
render() {
return (
<UserContext.Consumer>
{
context => (
<div>
<p>{context.name}</p>
</div>
)
}
</UserContext.Consumer>
)
}
}
EditableCell
const EditableCell = ({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = useState(false);
const inputRef = useRef(null);
const form = useContext(EditableContext); // 使用useContext獲取之前的form,這種寫法簡化了之前的consumer組件返回函數
useEffect(() => { // 經典聚焦方法。編輯第一步:聚焦
if (editing) {
inputRef.current.focus();
}
}, [editing]);
const toggleEdit = () => {
setEditing(!editing);
form.setFieldsValue({ // 修改數據,在這里時修改該行name列數據
[dataIndex]: record[dataIndex],
});
};
const save = async () => { // 保存方法異步執行
try {
const values = await form.validateFields();
toggleEdit();
handleSave({ ...record, ...values });
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};
let childNode = children;
if (editable) {
childNode = editing ? ( // 對編輯狀態中的渲染form組件,否則正常渲染
<Form.Item
style={{
margin: 0,
}}
name={dataIndex}
rules={[
{
required: true,
message: `${title} is required.`,
},
]}
>
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
</Form.Item>
) : (
<div
className="editable-cell-value-wrap"
style={{
paddingRight: 24,
}}
onClick={toggleEdit}
>
{children}
</div>
);
}
return <td {...restProps}>{childNode}</td>;
};
antd Form.useForm()
react context基本使用
context基本使用
usecontext