antd可编辑单元格实现原理

最近在项目复盘的时候,发现使用了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

posted @ 2022-03-17 11:09  沧浪浊兮  阅读(2120)  评论(0编辑  收藏  举报