一、前言
目标
在ant design
的Table
组件基础之上利用react-dnd
实现表格列的拖拽排序、并自定义列的显示隐藏。
Tips
- 请先了解
ant design
组件库中Table
组件的用法,本文不再展开 - 本文不展开介绍
react-dnd
的基础知识,不太了解它的同学可以先参考文末的文章学习
当前版本
react
:16.14.0
react-dom
:16.14.0
antd
:3.26.20
react-dnd
:11.1.3
react-dnd-html5-backend
:11.1.3
二、实现代码
为自定义的表格组件取名CustomColumnTable
拖拽方案说明
- 在组件之外还得用
DndProvider
包裹,否则无法使用拖拽功能 - 拖拽实现方案是拖拽表头列实现整列位置替换
- 表头列既可以被拖拽也可以接受被拖拽的列
- 缺陷:无法有效过滤 表格行可选择(rowSelection)时的选择列 和 固定列(fixed)
传入参数说明
这里是分了两种情况:
当外部组件传入的columns
数组不会发生变化
- 此时
dynamicColumns
为false,意味着之后表格列的拖拽排序与显示隐藏全由封装的CustomColumnTable
组件来控制
当columns
数组会发生变化时
- 比如在不同场景下,显示的表格列名不同,展示方式不同,这时外部组件传入的
columns
可能会发生变化,无法完全交由CustomColumnTable
组件控制。此时外部组件传入columns
时可以为每个子项添加selected
属性,表示CustomColumnTable
组件是否能控制该列的显示隐藏和拖拽 - 同时外部组件需传递
dynamicColumns
onChangeColumn
两个参数,且onChangeColumn
函数的参数是已处理好的新columns数组,外部组件拿到后可以用来替换原columns数组
区分selected
和visible
selected
表示CustomColumnTable
组件是否能控制该列显示隐藏和拖拽位置,默认为true
,false
表示CustomColumnTable
组件暂时无法控制它visible
表示列是否显示,这由完全CustomColumnTable
组件来控制,true
显示,false
隐藏selected
的优先级比visible
高,在列的selected
属性为false
下,无论visible
属性是否为true
,表格都不会显示该列
import React, { useState, useRef } from 'react';
import { Button, Checkbox, Popover, Table } from 'antd';
import { createDndContext, DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
const type = 'DragTableHeadCol';
const DNDContext = createDndContext(HTML5Backend);
const swapArray = (arr, index1, index2) => {
const dragCol = arr[index1];
arr.splice(index1, 1);
arr.splice(index2, 0, dragCol);
return arr;
};
/**
* @param index 表头列的位置下标
* @param moveCol 拖拽结束时排序方法
*/
const dragdrop = (index, moveCol) => {
const ref = React.useRef();
const [, drop] = useDrop({
accept: type,
drop: item => {
// item.index表示被拖拽组件的下标
// index是接受被拖拽组件的下标
moveCol(item.index, index);
},
});
const [, drag] = useDrag({
item: { type, index },
});
// 让组件既可以被拖拽也可以接受被拖拽组件
drop(drag(ref));
return { ref };
};
// 对tableHeadRow进行封装
const DragTableHeadRow = ({ children, moveCol }) => {
// 对列进行处理,使其可拖拽
const Ths = children.map((th, index) => {
const {
props: { className, style, children: thChildren },
} = th;
const { ref } = dragdrop(index, moveCol);
const cloneTh = React.cloneElement(
th,
{
...th.props,
ref,
style: { cursor: 'move', ...style },
},
thChildren,
);
return cloneTh;
});
return <tr>{Ths}</tr>;
};
/**
* @param {columns} 传入的列
* @param {dynamicColumns} 若传入的columns是动态变化的(columns数组的元素有动态增减),传true
* @param {onChangeColumn} 由外部管理列的变化 dynamicColumns = true时必传,参数为新的表格列
*/
const CustomColumnTable = ({
columns,
dynamicColumns = false,
onChangeColumn,
...props
}) => {
// 过滤不被选择的列(默认全选)
const initColumns = columns.filter(item => item.selected ?? true);
// dynamicColumns = true 下使用
const visibleColumns = initColumns.filter(item => item.visible ?? true) || [];
// 初始化nowColumns (dynamicColumns = false 下使用,默认全选)
const [nowColumns, setColumns] = useState(initColumns.map(item => ({ visible: true, ...item })));
// 真正渲染的列
const realColumns = dynamicColumns
? visibleColumns
: nowColumns.filter(item => item.visible) || [];
// 可拖拽的列
const dragColumns = dynamicColumns ? columns : nowColumns;
// 初始化checkBoxChecked (dynamicColumns = false 下使用,默认全选)
const [checkBoxChecked, setCheckBoxChecked] = useState(initColumns.map(item => item.title));
// 真正显示列的title数组
const realCheckBoxChecked = dynamicColumns
? visibleColumns.map(item => item.title)
: checkBoxChecked;
// 拖拽结束处理列位置的函数
const moveCol = (dragIndex, hoverIndex) => {
const newColumns = swapArray(
dragColumns,
dragColumns.findIndex(item => realColumns[dragIndex].title === item.title),
dragColumns.findIndex(item => realColumns[hoverIndex].title === item.title),
);
if (dynamicColumns) {
onChangeColumn(newColumns);
} else {
setColumns([].concat(newColumns));
}
};
/**
* @param targetItem 点击的目标元素
* @param visible 是否显示
* @param list 显示(选中)的元素列表
*/
const checkCol = (targetItem, visible, list) => {
if (dynamicColumns) {
const tempColumns = columns.map(item => {
if (targetItem === item.title) return { ...item, visible };
return item;
});
onChangeColumn(tempColumns);
} else {
setColumns(state =>
state.map(item => {
if (targetItem === item.title) return { ...item, visible };
return item;
}),
);
setCheckBoxChecked(list);
}
};
/**
* @param isCheckAll 是否全选或全不选
*/
const checkAll = isCheckAll => {
if (dynamicColumns) {
const tempColumns = columns.map(item =>
item.selected ?? true ? { ...item, visible: isCheckAll } : item,
);
onChangeColumn(tempColumns);
} else {
const checked = nowColumns.map(item => item.title);
setCheckBoxChecked(isCheckAll ? checked : []);
setColumns(state => state.map(item => ({ ...item, visible: isCheckAll })));
}
};
const components = {
header: {
row: prop => {
return <DragTableHeadRow {...prop} moveCol={moveCol} />;
},
},
};
const menu = (
<>
<Checkbox
checked={realCheckBoxChecked.length === initColumns.length}
indeterminate={
realCheckBoxChecked.length !== initColumns.length && realCheckBoxChecked.length > 0
}
onClick={() => {
const isCheckAll = realCheckBoxChecked.length !== initColumns.length;
checkAll(isCheckAll);
}}
>
全部
</Checkbox>
<Checkbox.Group
style={{ width: '100%' }}
value={realCheckBoxChecked}
onChange={values => {
if (values.length > realCheckBoxChecked.length) {
const showItem = values.find(item => !realCheckBoxChecked.includes(item));
checkCol(showItem, true, values);
}
if (values.length < realCheckBoxChecked.length) {
const hideItem = realCheckBoxChecked.find(item => !values.includes(item));
checkCol(hideItem, false, values);
}
}}
>
{initColumns.map(item => (
<div key={item.title} style={{ minWidth: '200px' }}>
<Checkbox value={item.title} disabled={item.title === '操作' || item.title === '序号'}>
{item.title}
</Checkbox>
</div>
))}
</Checkbox.Group>
</>
);
const manager = useRef(DNDContext);
return (
<>
<div style={{ textAlign: 'left', margin: '4px' }}>
<Popover content={menu} placement="bottomLeft" trigger="click">
<Button icon="filter" size="small" />
</Popover>
</div>
<DndProvider manager={manager.current.dragDropManager}>
<Table {...props} columns={realColumns} components={components} />
</DndProvider>
</>
);
};
export default CustomColumnTable;
三、拓展
自定义接受组件的样式
react-dnd
的useDrop
函数返回的第一个参数是其collect
函数返回的对象,在collect
函数里可以返回几个需要用到的属性
注意:这里添加了自定义类名drop-over
const dragdrop = (index, moveCol) => {
const ref = React.useRef();
const [{ isOver }, drop] = useDrop({
accept: type,
collect: monitor => {
// 获取被拖拽的元素
const { index: dragIndex } = monitor.getItem() || {};
// 若被拖拽的元素和接受元素是同一个,则返回为空
if (dragIndex === index) return {};
return {
// 返回 isOver
isOver: monitor.isOver()
};
},
drop: item => {
moveCol(item.index, index);
},
});
const [, drag] = useDrag({
item: { type, index },
});
drop(drag(ref));
// 返回 isOver
return { ref, isOver };
};
...
const DragTableHeadRow = ({ children, moveCol }) => {
// 对列进行处理,使其可拖拽
const Ths = children.map((th, index) => {
const {
props: { className, style, children: thChildren },
} = th;
const { ref, isOver } = dragdrop(index, moveCol);
// 拿到 isOver后,便可以用来判断添加自定义样式
const cloneTh = React.cloneElement(
th,
{
...th.props,
ref,
className: `${className} ${isOver ? 'drop-over' : ''}`,
style: { cursor: 'move', ...style },
},
thChildren,
);
return cloneTh;
});
return <tr>{Ths}</tr>;
};
自定义被拖拽组件样式
方法原理同上,这次使用到的是useDrag
函数
const [{ isDragging }, drag] = useDrag({
item: { type, index },
collect: monitor => ({
isDragging: monitor.isDragging(),
}),
});
四、实现效果
五、参考文章
对于react-dnd
如何应用可参考 用 React Hooks 的方式使用 react-dnd