React 之可伸缩列的表格

4,425 阅读4分钟

大家好,我是张三岁🤣,一只法系前端⚖️。爱分享🖋️、爱冰冰🧊🧊。
欢迎小伙伴们加我微信:maomaoibingbing,拉你进群,一起讨论,期待与大家共同成长🥂。

前言

最近在项目中遇到了一个需求,客户希望表格的列宽可以拖动伸缩。该项目的技术栈是 React + Ant Design 。后来看了文档后进行了简单的改动和封装。以下将使用函数式组件,希望能对各位有所帮助,蟹蟹٩('ω')و。

一、业务场景

客户希望表格的标题栏可以拖动来改变整列的宽窄,为了 向别人装x 更方便的查看数据多的列,并增加表格组件的交互效果。

一只普通的表格.png

二、实现思路

Ant Design 文档中已经明确给出了类式组件版本的 Demo ,此处在这个示例的基础上进行修改,改为函数式组件,并对其进行封装,提高其可复用性。此处奉上 官方文档地址传送门

三、进行编码

1. 安装依赖

首先我们需要安装依赖 react-resizable ,此依赖是实现该拖拽伸缩功能的核心。

# npm 安装
npm install react-resizable --save

# yarn 安装
yarn add react-resizable

2. 重写案例

接下来我们将示例的类式写法改为函数式。

// 列表页面
import { useState } from 'react';
import { Table } from 'antd';
import { Resizable } from 'react-resizable';// 核心依赖
import './resizable-title.css';// 此种引入方式不会在类名中添加哈希值,故仅作为替换样式时使用

const ResizableTitle = ({ onResize, width, ...restProps }) => {
    if (!width) { return (<th {...restProps} />) };

    return (
        <Resizable
            width={width}
            height={0}
            handle={
                <span
                    className="react-resizable-handle"
                    onClick={e => { e.stopPropagation() }}
                />
            }
            onResize={onResize}
            draggableOpts={{ enableUserSelectHack: false }}
        >
            {/* 此处增加行内样式目的:让标题的文字不可选中 */}
            <th {...restProps} style={{ ...restProps?.style, userSelect: 'none' }} />
        </Resizable>
    );
};

const List = () => {
    // 表格数据
    const data = [
        {
            key: 0,
            date: '2018-02-11',
            amount: 120,
            type: 'income',
            note: 'transfer',
        },
        {
            key: 1,
            date: '2018-03-11',
            amount: 243,
            type: 'income',
            note: 'transfer',
        },
        {
            key: 2,
            date: '2018-04-11',
            amount: 98,
            type: 'income',
            note: 'transfer',
        },
    ];

    // 列配置
    const columns = [
        {
            title: 'Date',
            dataIndex: 'date',
            width: 200,
        },
        {
            title: 'Amount',
            dataIndex: 'amount',
            width: 100,
            sorter: (a, b) => a.amount - b.amount,
        },
        {
            title: 'Type',
            dataIndex: 'type',
            width: 100,
        },
        {
            title: 'Note',
            dataIndex: 'note',
            width: 100,
        },
        {
            title: 'Action',
            key: 'action',
            render: () => <a>Delete</a>,
        }
    ];
    // 用 useState 创建响应式数据
    const [cols, setCols] = useState(columns);
    const colsArray = cols.map((col, index) => {
        return {
            ...col,
            onHeaderCell: column => ({ width: column.width, onResize: handleResize(index) })
        };
    });

    // todo 调整列宽
    const handleResize = index => {
        return (_, { size }) => {
            const temp = [...cols];
            temp[index] = { ...temp[index], width: size.width };
            setCols(temp);
        };
    };

    return (
        <div>
            <Table
                components={{ header: { cell: ResizableTitle } }}
                columns={colsArray}
                dataSource={data}
                pagination={false}
            />
        </div>
    );
};

export default List;

此处直接使用普通的 CSS ,单独将控制伸缩列的样式放到一个文件中,避免与其他样式代码冲突。页面中其他样式代码可以单独放入同目录下的 index.less ,并用 import styles from './index.less' 方式引入。

/* resizable-title.css */

/* 列表页面 */
.react-resizable-handle {
  position: absolute;
  right: -5px;
  bottom: 0;
  z-index: 1;
  width: 10px;
  height: 100%;
  cursor: col-resize;
}

可拖动伸缩列的表格.gif

效果似乎还不错,但是我们接下来还要对其进行封装。

3. 封装复用

为了提高复用性,我们需要把代码封装成一个公共组件。内部使用 Ant Design 的表格 Table 组件。

// components/ResizableTable/index.jsx
// 可拖拽表格组件
import { useState } from 'react';
import { Table } from 'antd';
import { Resizable } from 'react-resizable';
import './index.css';

const ResizableTitle = ({ onResize, width, ...restProps }) => {
    if (!width) { return (<th {...restProps} />) };

    return (
        <Resizable
            width={width}
            height={0}
            handle={
                <span
                    className="react-resizable-handle"
                    onClick={e => { e.stopPropagation() }}
                />
            }
            onResize={onResize}
            draggableOpts={{ enableUserSelectHack: false }}
        >
            <th {...restProps} style={{ ...restProps?.style, userSelect: 'none' }} />
        </Resizable>
    );
};

const ResizableTable = ({ columns = [], ...props }) => {
    // * 列数据
    const [cols, setCols] = useState(columns);
    const colsArray = cols.map((col, index) => {
        return {
            ...col,
            onHeaderCell: column => ({ width: column.width, onResize: handleResize(index) })
        };
    });

    // todo 调整列宽
    const handleResize = index => {
        return (_, { size }) => {
            const temp = [...cols];
            temp[index] = { ...temp[index], width: size.width };
            setCols(temp);
        };
    };

    return (
        <Table
            components={{ header: { cell: ResizableTitle } }}
            columns={colsArray}
            {...props}
        />
    );
};

export default ResizableTable;
// 列表页面
import ResizableTable from '@/components/ResizableTable';// 可拖拽表格组件

const List = () => {
    // 表格数据
    const data = [
        // ...
    ];

    // 列配置
    const columns = [
        // ...
    ];

    return (
        <div>
            <ResizableTable
                columns={columns}
                dataSource={data}
                pagination={false}
            />
        </div>
    );
};

export default List;
/* components/ResizableTable/index.css */
.react-resizable-handle {
  position: absolute;
  right: -5px;
  bottom: 0;
  z-index: 1;
  width: 10px;
  height: 100%;
  cursor: col-resize;
}

4. 组件优化

当然,仅仅封装普通表格 Table 组件是不够的,万一想使用 ProComponents 中的超级表格组件 ProTable 呢。毕竟在后台管理系统中, ProTable 的合理使用可以节省不少开发时间。同时也是为了让该自定义组件更加完善,真正够用且好用。

// components/ResizableTable/index.jsx
// 可拖拽表格组件
import { useState } from 'react';
import { Table } from 'antd';
import ProTable from '@ant-design/pro-table';
import { Resizable } from 'react-resizable';
import './index.css';

const ResizableTitle = ({ onResize, width, ...restProps }) => {
    if (!width) { return (<th {...restProps} />) };

    return (
        <Resizable
            width={width}
            height={0}
            handle={
                <span
                    className="react-resizable-handle"
                    onClick={e => { e.stopPropagation() }}
                />
            }
            onResize={onResize}
            draggableOpts={{ enableUserSelectHack: false }}
        >
            <th {...restProps} style={{ ...restProps?.style, userSelect: 'none' }} />
        </Resizable>
    );
};

export const ResizableTable = ({ columns = [], ...props }) => {
    // * 列数据
    const [cols, setCols] = useState(columns);
    const colsArray = cols.map((col, index) => {
        return {
            ...col,
            onHeaderCell: column => ({ width: column.width, onResize: handleResize(index) })
        };
    });

    // todo 调整列宽
    const handleResize = index => {
        return (_, { size }) => {
            const temp = [...cols];
            temp[index] = { ...temp[index], width: size.width };
            setCols(temp);
        };
    };

    return (
        <Table
            components={{ header: { cell: ResizableTitle } }}
            columns={colsArray}
            {...props}
        />
    );
};

export const ResizableProTable = ({ columns = [], ...props }) => {
    // * 列数据
    const [cols, setCols] = useState(columns);
    const colsArray = cols.map((col, index) => {
        return {
            ...col,
            onHeaderCell: column => ({ width: column.width, onResize: handleResize(index) })
        };
    });

    // todo 调整列宽
    const handleResize = index => {
        return (_, { size }) => {
            const temp = [...cols];
            temp[index] = { ...temp[index], width: size.width };
            setCols(temp);
        };
    };

    return (
        <ProTable
            components={{ header: { cell: ResizableTitle } }}
            columns={colsArray}
            {...props}
        />
    );
};

// 默认暴露 普通表格
export default ResizableTable;
// 列表页面
// 可以使用"默认引入"和"模块引入"两种方式,此处使用模块引入方式
import { ResizableTable, ResizableProTable } from '@/components/ResizableTable';

const List = () => {
    // 表格数据
    const data = [
        // ...
    ];

    // 列配置
    const columns = [
        // ...
    ];

    return (
        <>
            <div>普通表格 ResizableTable:</div>
            <br />

            <ResizableTable
                columns={columns}
                dataSource={data}
                pagination={false}
            />

            <br />
            <div>超级表格 ResizableProTable:</div>
            <br />

            <ResizableProTable
                columns={columns}
                dataSource={data}
                pagination={false}
            />
        </>
    );
};

export default List;

自定义公共组件优化.gif

小结

本次我们解决了 React 中表格标题行拖动伸缩的问题,同时也进行了公共组件的封装与优化。不积跬步,无以至千里;不积小流,无以成江海。为了更好更强,我们需要不断积累问题的解决方案,才能不断进步。