本文首发于 react-beautiful-dnd表格拖拽时行收缩 - 凝结尾迹
最近在使用react-beautiful-dnd开发表格拖拽排序时遇到了一个问题:在选中一行并开始拖拽时 ,行、或者说单元格的样式会发生变化,具体表现为整行宽度收缩。在搜索资料并翻阅文档后,找到了这个问题的解决方法。
先看一下对一个标准表格的某行拖拽时会发生什么吧,可以看到被拖拽行收缩了起来,这实际上是因为列的宽度丢失了。
在react-beautiful-dnd官方文档中提到了在表格中使用的方法:Tables。其中提到可以使用两种策略对表格进行拖拽排序,分别是Fixed layouts
和Dimension locking
,接下来我分别介绍一下。
Fixed layouts
相比于后者,Fixed layouts
性能更好且更容易实现,但是只适用于表格列宽固定的情况,这种情况下,只需要为<Draggable />
包裹的行设置display: table
即可。
如果上述方法不生效,也可以直接为<td>
元素设置一个固定宽度,比如这里将<td>
元素的宽度设置为120px
:
jsx
<DragDropContext onDragEnd={this.onDragEnd}>
<Droppable droppableId="droppable">
{(provided, snapshot) => (
<table
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}
>
<thead>
<tr>
<th>Title</th>
<th>Test</th>
</tr>
</thead>
<tbody>
{this.state.items.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{(provided, snapshot) => (
<tr
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}
>
<td style={{ width: "120px" }}>{item.content}</td>
<td style={{ width: "120px" }}>{item.test}</td>
</tr>
)}
</Draggable>
))}
{provided.placeholder}
</tbody>
</table>
)}
</Droppable>
</DragDropContext>
当然这不是本文的重点,毕竟不是每个表格都可以固定列宽的,很多情况下列宽会根据单元格中的内容自适应,这种情况就只能使用下面的方法了。
Dimension locking
前面提到这种方法适用于列宽根据内容自适应的情况,不仅如此,它同样适用于列宽固定的情况,而且更加具有健壮性,但是性能会比较差。使用这种方法时表格内容最好不要超过50行,就算不考虑性能,上百行内容的拖拽体验想必也不会很好。
这个方法的实现思路简单来说就是:在拖拽前记录被拖拽行每个单元格的原始宽度和高度,并在拖拽中将该行单元格宽高设置为记录值,拖拽结束后移除样式。
jsx
import React, { useState, useEffect, useRef } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
class LockedCell extends React.Component {
ref;
getSnapshotBeforeUpdate(prevProps) {
if (!this.ref) {
return null;
}
const isDragStarting =
this.props.isDragOccurring && !prevProps.isDragOccurring;
if (!isDragStarting) {
return null;
}
const { width, height } = this.ref.getBoundingClientRect();
const snapshot = {
width,
height,
};
return snapshot;
}
componentDidUpdate(prevProps, prevState, snapshot) {
const ref = this.ref;
if (!ref) {
return;
}
if (snapshot) {
if (ref.style.width === snapshot.width) {
return;
}
ref.style.width = `${snapshot.width}px`;
ref.style.height = `${snapshot.height}px`;
return;
}
if (this.props.isDragOccurring) {
return;
}
// inline styles not applied
if (ref.style.width == null) {
return;
}
// no snapshot and drag is finished - clear the inline styles
ref.style.removeProperty("height");
ref.style.removeProperty("width");
}
setRef = (ref) => {
this.ref = ref;
};
render() {
return (
<td ref={this.setRef} style={{ boxSizing: "border-box" }}>
{this.props.children}
</td>
);
}
}
const App = () => {
const [items, setItems] = useState([]);
const [isDragging, setIsDragging] = useState(false);
// 构造测试数据
const getItems = (count) =>
Array.from({ length: count }, (v, k) => k).map((k) => ({
id: `item-${k}`,
content: `Item ${k}`,
}));
useEffect(() => {
setItems(getItems(3));
}, []);
const onDragEnd = (result) => {
setIsDragging(false);
};
const onBeforeDragStart = () => {
setIsDragging(true);
};
return (
<div style={{ padding: "2rem" }}>
<table>
<thead>
<tr>
<th>Item</th>
<th>Two</th>
<th>Three</th>
<th>Four</th>
</tr>
</thead>
<DragDropContext
onDragEnd={onDragEnd}
onBeforeDragStart={onBeforeDragStart} // DIMENSION LOCKING
>
<Droppable droppableId="droppable">
{(provided) => (
<tbody {...provided.droppableProps} ref={provided.innerRef}>
{items.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{(provided, snapshot) => (
<tr
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<LockedCell
isDragOccurring={isDragging}
snapshot={snapshot}
>
{item.content}
</LockedCell>
<LockedCell
isDragOccurring={isDragging}
snapshot={snapshot}
>
2
</LockedCell>
<LockedCell
isDragOccurring={isDragging}
snapshot={snapshot}
>
3
</LockedCell>
<LockedCell
isDragOccurring={isDragging}
snapshot={snapshot}
>
4
</LockedCell>
</tr>
)}
</Draggable>
))}
{provided.placeholder}
</tbody>
)}
</Droppable>
</DragDropContext>
</table>
</div>
);
};
export default App;
可以看到拖拽容器的实现没有什么变化,关键点在于使用<LockedCell>
替代了<td>
,而<LockedCell>
具体做了什么呢?
刚才已经大致描述过了,使用onBeforeDragStart
检测拖拽状态的临界点,拖拽前记录好该行中所有单元格的宽高,拖拽中设置宽高为记录值,拖拽结束后清除样式。这也是本方法性能低的原因,需要频繁读取DOM元素的属性并渲染。
代码的实现可以在这里查看👉 在线代码,最后看一下效果吧,可以看到行拖拽过程中宽度不再收缩了: