基于React Beautiful DnD在React项目中实现列表拖拽

170 阅读4分钟

一、写在前面

前段时间接了一个需求,在一个表单中,需要实现通过拖拽来调整表单项的顺序,这个需求之前也做过好多次,便想要通过文章记录下实现过程以及踩过的坑,算是一个小小的总结。

我们先来展示最后的代码效果:

二、准备工作

首先我们通过CRA搭建一个基础的项目,写上一些基础代码:

  1. 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;

  1. 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 依赖。

启动项目后,我们就可以看到页面了。

1697014251079.png

三、正式开始

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. 拖拽不起作用,控制台出现以下报错:

1697095321209.png

解决办法:关闭React的StrictMode。

// src/index.js

root.render(
-  <React.StrictMode>
    <App />
-  </React.StrictMode>
);

2. 拖拽时出现容器元素高度塌陷的情况,如图:

image.png

解决办法:在容器元素最后添加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>
)

最后实现的效果如下图:

image.png

4. 拖拽项目没有移动到正确的位置?

解决方法:这通常是由于拖拽项目没有指定高度造成的。您可以尝试通过向可拖拽对象添加一个高度值来修复这个问题。

五、总结

本文中,我们一步一步实现如何给列表添加拖拽排序的功能,这个基本上可以满足了绝大部分的场景,如果有更多更复杂的业务需求,我们也可以去 github 里 react-beautiful-dnd 的仓库去找解决方案,上面提供了很多的例子以及API的详解。美中不足的是 react-beautiful-dnd 的没有官网,提供的demo网站看不到详细代码。

  1. react-beautiful-dnd 的git地址
  2. react-beautiful-dnd示例网站

最后的最后,本文如有错误,欢迎大家指正!如果对你的工作有帮助,也不要吝啬你的赞啊~