在ant design
的Table
组件基础之上利用react-dnd
实现表格列的拖拽排序、并自定义列的显示隐藏。
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
包裹,否则无法使用拖拽功能这里是分了两种情况:
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