一、写在前面
前段时间接了一个需求,在一个表单中,需要实现通过拖拽来调整表单项的顺序,这个需求之前也做过好多次,便想要通过文章记录下实现过程以及踩过的坑,算是一个小小的总结。
我们先来展示最后的代码效果:
二、准备工作
首先我们通过CRA搭建一个基础的项目,写上一些基础代码:
- demo-app\src\App.js
import React, { useState } from 'react'
import './App.css'
const INITIAL_LIST = [
{ id: '1', task: 'task 1', },
{ id: '2', task: 'task 2', },
{ id: '3', task: 'task 3', },
];
const Item = ({ item }) => (
<div className='task-item'>
{item.task}
</div>
)
const List = ({ list }) => (
<div className='task-list'>
{list.map((item, index) => (
<Item key={item.id} item={item}></Item>
))}
</div>
);
const App = () => {
const [list, setList] = useState(INITIAL_LIST);
return (
<div>
<div className='task-title'>任务列表</div>
<List list={list} />;
</div>
)
};
export default App;
- demo-app\src\App.css
.task-title {
text-align: center;
}
.task-list {
border: 1px solid #282c34;
padding: 12px 12px 0;
width: 200px;
margin: 0 auto;
}
.task-item {
border: 1px solid #282c34;
padding: 12px;
margin-bottom: 12px;
font-size: 16px;
}
然后我们通过 npm install react-beautiful-dnd
安装 react-beautiful-dnd 依赖。
启动项目后,我们就可以看到页面了。
三、正式开始
1. 我们使用 DragDropContext 作为拖拽列表的容器元素。
import React, { useState } from 'react'
+import { DragDropContext } from 'react-beautiful-dnd';
import './App.css'
...
const List = ({ list }) => (
+ <DragDropContext>
<div className='task-list'>
{list.map((item, index) => (
<Item key={item.id} item={item}></Item>
))}
</div>
+ </DragDropContext>
);
2.接下来,我们添加 react-beautiful-dnd 提供的 Droppable 组件,它是一个高阶组件(HOC),它暴露了一些 react-beautiful-dnd 的属性,提供给列表中使用。
import React, { useState } from 'react'
-import { DragDropContext } from 'react-beautiful-dnd';
+import { DragDropContext,Droppable } from 'react-beautiful-dnd';
import './App.css'
...
const List = ({ list }) => (
<DragDropContext>
- <div className='task-list'>
+ <Droppable droppableId="droppable">
+ {(provided) => (
+ <div className='task-list' ref={provided.innerRef} {...provided.droppableProps}>
{list.map((item, index) => (
<Item key={item.id} item={item}></Item>
))}
</div>
+ )}
+ </Droppable>
</DragDropContext>
);
Droppable 组件本质上是定义了所有拖拽项目的拖拽区域,我们必须在list元素外包裹它,以使用 Dropable 组件的属性。在后面我们将在list元素上使用这些属性,来更好地了解它们。
3. 给List组件添加拖拽结束事件
DragDropContext 提供了一个onDragEnd事件处理方法,我们给List组件使用,当拖拽操作结束时调用。
-const List = ({ list, onDragEnd }) => (
- <DragDropContext>
+const List = ({ list, onDragEnd }) => (
+ <DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div className='task-list' ref={provided.innerRef} {...provided.droppableProps}>
{list.map((item, index) => (
<Item key={item.id} item={item}></Item>
))}
</div>
)}
</Droppable>
</DragDropContext>
);
在APP组件中,我们添加了一个拖拽事件处理方法,传递给了List组件,同时我们也定义了reorder函数,用来对数组进行重新排序。
+const reorder = (list, startIndex, endIndex) => {
+ const result = Array.from(list);
+ const [removed] = result.splice(startIndex, 1);
+ result.splice(endIndex, 0, removed);
+ return result;
+};
const App = () => {
const [list, setList] = useState(INITIAL_LIST);
+ const handleDragEnd = ({ destination, source }) => {
+ if (!destination) return;
+ setList(reorder(list, source.index, destination.index));
+ };
return (
<div>
<div className='task-title'>任务列表</div>
- <List list={list} />;
+ <List list={list} onDragEnd={handleDragEnd} />;
</div>
)
};
在handleDragEnd方法中,我们看到有destination和source两个参数,分别代表着拖拽元素,以及要交换位置的目标元素,如果目标元素为空,我们就取消拖拽事件,不做任何处理,如果不为空,我们就交换两者位置。
4. 将Item组件变成可拖拽组件 我们使用 Draggable 组件包裹Item组件,使其能够进行拖拽操作。我们将index作为 Draggable 组件的索引属性,使其能够追踪到列表顺序的变化(handleDragEnd 中使用到的index),同时将item的id作为 Draggable 组件的唯一标识符———draggableId。
-import { DragDropContext, Droppable } from 'react-beautiful-dnd';
+import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
将Item组件改造成如下的样子:
const Item = ({ item, index }) => (
<Draggable
key={item.id}
index={index}
draggableId={item.id}
>
{(provided, snapshot) => (
<div className='task-item'
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{item.task}
</div>
)}
</Draggable>
)
Draggable组件,与Droppable组件相同,是一个HOC组件,它将 Droppable 组件的属性暴露给用于拖拽列表的子元素。
到这一步,我们就基本实现了拖拽功能。
四、遇到问题
1. 拖拽不起作用,控制台出现以下报错:
解决办法:关闭React的StrictMode。
// src/index.js
root.render(
- <React.StrictMode>
<App />
- </React.StrictMode>
);
2. 拖拽时出现容器元素高度塌陷的情况,如图:
解决办法:在容器元素最后添加provided.placeholder,拖拽时会生成一个替代元素。
const List = ({ list, onDragEnd }) => (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div className='task-list' ref={provided.innerRef} {...provided.droppableProps}>
{list.map((item, index) => (
<Item key={item.id} item={item} index={index}></Item>
))}
+ {provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
3. 如何给拖拽列表和拖拽项添加拖拽时的样式?
通过 Droppable 组件的 snapshot.isDraggingOver 和 Draggable 组件的 snapshot.isDragging 来设置。
我们按照下面的代码改造 List 组件和 Item 组件。
// List 组件
const List = ({ list, onDragEnd }) => (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided, snapshot) => (
<div className='task-list' ref={provided.innerRef} {...provided.droppableProps}
style={{
...(snapshot.isDraggingOver ? {
background: 'lightblue',
borderRadius: '16px',
} : {}),
}}>
{list.map((item, index) => (
<Item key={item.id} item={item} index={index}></Item>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
//Item组件
const Item = ({ item, index }) => (
<Draggable
key={item.id}
index={index}
draggableId={item.id}
>
{(provided, snapshot) => (
<div className='task-item'
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
// default drag style
...provided.draggableProps.style,
// customized drag style
background: snapshot.isDragging
? 'pink'
: 'transparent',
}}
>
{item.task}
</div>
)}
</Draggable>
)
最后实现的效果如下图:
4. 拖拽项目没有移动到正确的位置?
解决方法:这通常是由于拖拽项目没有指定高度造成的。您可以尝试通过向可拖拽对象添加一个高度值来修复这个问题。
五、总结
本文中,我们一步一步实现如何给列表添加拖拽排序的功能,这个基本上可以满足了绝大部分的场景,如果有更多更复杂的业务需求,我们也可以去 github 里 react-beautiful-dnd 的仓库去找解决方案,上面提供了很多的例子以及API的详解。美中不足的是 react-beautiful-dnd 的没有官网,提供的demo网站看不到详细代码。
最后的最后,本文如有错误,欢迎大家指正!如果对你的工作有帮助,也不要吝啬你的赞啊~