react-dnd 拖拽能力教程

2023-11-19

前言

近几年来,低代码、零代码的热度在国内逐年递增。“复杂度同力一样不会消失,也不会凭空产生,它总是从一个物体转移到另一个物体或一种形式转为另一种形式”。用户在使用低零代码构建应用程序时,这些能力只是被平台研发人员提前编写完了。

作为低零代码的基础,前端拖拽能力就尤为重要。

一个完整的拖拽流程分为两部分:拖动+放置

让一个元素支持拖动是一件非常容易做到的事情,只需要在对应的 dom 节点增加 draggable="true" 的属性即可。

真正麻烦的是放置的能力。我们需要监听 ondragstartondragenterondragover 等各个阶段的事件,处理起来过分的繁琐。而社区已经提供了成熟的库 react-dnd 来帮助我们实现这些细节,我们只需要关心业务逻辑即可。

本文将手把手带着小伙伴们掌握拖拽能力,并且提供 demo 让小伙伴们能够更细致的研究。

请添加图片描述

一、创建拖拽容器

首先进入到项目中,安装该能力需要用到的依赖。

pnpm i react-dnd

pnpm i react-dnd-html5-backend
import { HTML5Backend } from "react-dnd-html5-backend";
import { DndProvider } from "react-dnd";

<DndProvider backend={HTML5Backend}>
  <App />
</DndProvider>;

react-dnd 中导出 DndProvider,包裹要实现拖动和放置的区域。

初次使用 dnd 库的小伙伴肯定对 react-dnd-html5-backend 存在疑惑。并且不理解 DndProvider 为什么要传递一个 backend。这里来解释下什么是 backend

pc端移动端 的 dom 层存在不同的事件监听和处理方式。所以 dnd 将这部分单独抽出来。方便后续的扩展。

  • react-dnd-html5-backend:用于控制 html5 事件的 backend。
  • react-dnd-touch-backend:用于控制移动端 touch 事件的 backend。
  • react-dnd-test-backend:用户可参考自定义的 backend。

接下来要创建一个拖动区和放置区:

pages/dnd/index.jsx

// 忽略部分内容,小伙伴们自行补齐
import { CustDrag, CustDrop } from "@/components";

const dndList = [
  { label: "标签1", value: "值1" },
  { label: "标签2", value: "值2" },
  { label: "标签3", value: "值3" },
];

const DndPage = () => {
  const [list, setList] = useState(dndList);
  return (
    <DndProvider backend={HTML5Backend}>
      <div className={styles.center}>
        <span>请拖拽:</span>
        <div style={{ border: "1px solid #000", minHeight: "200px" }}>
          {list.map((item) => {
            return <CustDrag key={item?.value} data={item} />;
          })}
        </div>
        <div style={{ marginTop: "10px" }}>请放置:</div>
        <CustDrop onChange={dropChange} />
      </div>
    </DndProvider>
  );
};

二、拖动能力

接下来实现第一个核心能力:

components/CustDrag/index.jsx

import { useDrag } from "react-dnd";

const CustDrag: FC<CustDragProps> = ({ data }) => {
  const [{ opacity }, dragRef] = useDrag({
    type: "Field",
    item: { ...data },
    collect: (monitor) => ({
      opacity: monitor.isDragging() ? 0.5 : 1,
    }),
  });

  return (
    <div ref={dragRef} style={{ opacity, cursor: "move" }}>
      {data?.label}
    </div>
  );
};

通过 gif 图得知,列表中的每一项都为一个可拖动项,因此我们要给每个数据都设置上拖动的属性和效果。

const [collectedProps, dragRef] = useDrag({ type, item, canDrag, collect });

通过 useDrag 生成的第二个参数 dragRef 指向某一个 div。此 div 将会被赋予 draggable=true 的属性,同时被拖动时所发生的所有事件都会被监听。

想要获取监听后的信息,只需要在 collect 参数里配置好即可在 collectedProps 获取到实时数据。

比如说上述的代码中,通过 monitor.isDragging() 监听到拖动的状态,并且定义一个 opacity 的属性来代表样式的透明度。

接下来看参数传递。

  • type: 自定义一个名称。拖动的 type 和放置的 type 保持一致。
  • item:参数传递。拖动时的数据能够传递到放置区。
  • collect: 收集监听整个拖动事件的状态数据,比如是否正在进行拖动、拖动偏移量等数据。可以通过源代码获取完整的数据。
  • end: 拖动结束时执行的方法。
  • canDrag: 指定当前是否允许拖动。若希望始终允许被拖动,则可以忽略此方法。

请添加图片描述

当我们定义的项可被拖动,且拖动时,有 0.5 的透明度时,就说明这部分的代码编写成功。

三、放置能力

接下来完成第二个核心内容:

这里笔者将分为几个小部分,小伙伴们按着步骤走,逻辑会更加清晰。

  • 实现放置能力
  • 实现放置区数据唯一性
  • 实现被放置后的数据不展示在拖动区

components/CustDrop/index.jsx

1、实现放置能力

// 伪代码,省略部分重复代码
import { useDrop } from "react-dnd";

const CustDrop: FC<CustDropProps> = ({ onChange }) => {
  const [value, setValue] = useState<any[]>([]);
  const [{ canDrop, isOver }, drop] = useDrop({
    accept: 'Field',
    drop: (item) => {
      const targetValue = [...value];
      targetValue.push(item);
      setValue(targetValue);
      onChange(targetValue);
    },
    collect: (monitor) => ({
      // 是否放置在目标上
      isOver: monitor.isOver(),
      // 是否开始拖拽
      canDrop: monitor.canDrop(),
    }),
  });

  // 展示拖动时的界面效果
  const showCanDrop = () => {
    if (canDrop && !isOver && !value.length) return <div>请拖拽到此处</div>;
  };

  const delItem = (ind: number) => {
    const newValue = [...value];
    newValue.splice(ind, 1);
    setValue(newValue);
    onChange(newValue);
  };

  // 展示值
  const showValue = () => {
    return value.map((item, index: number) => {
      return (
        <div key={item?.value}>
          {item?.label} <span onClick={() => delItem(index)}>删除</span>
        </div>
      );
    });
  };

  return (
    <div
      ref={drop}
      style={{ border: '1px solid #000', marginTop: '10px', minHeight: '200px', background: '#fff' }}
    >
      {showCanDrop()}
      {showValue()}
    </div>
  );
};

核心的代码是 useDrop 的使用。

  • accept 接收对应的对应的拖动标识。
  • drop 接受拖动传递过来的数据。
  • collect 收集拖动事件在放置区的数据。比如:是否有成功的拖动到放置区上、是否已经开始拖动,距离放置区的坐标等。并且将监听的数据传递到 useDrop 第一个参数来。

通过抛出的 isOvercanDrop 来判断用户是否正在拖拽中。若用户在拖拽中,则在放置区展示 请拖拽到此处 的文字标识。

当完成上面的代码就实现了最简单的拖拽功能。并且增加 删除 的按钮实现放置区的新增和删除能力。

2、实现放置区数据唯一性

若我们要保证放置区数据的唯一性,我们就需要对正在拖拽的数据进行判断。有两种方案:

  • 若拖拽了相同的数据到放置区,则放置区置红提示用户 数据已经被放置。并且不让用户将数据放置到放置区中。
  • 当成功拖拽了某一个数据后,将该数据在拖动区中删除。

那么接下来按顺序实现上面的两种场景。

const [error, setError] = useState < string > "";
const [{ canDrop, isOver }, drop] = useDrop({
  // 这里省略部分重复代码
  canDrop: (item: any) => {
    setError(undefined);
    const filter = value.filter((it) => it.value === item.value);
    if (!!filter.length) {
      setError("数据已经被放置");
      return false;
    }
    return true;
  },
});

const showCanDrop = () => {
  if (error && isOver) return <div>{error}</div>;
  if (canDrop && !isOver && !value.length) return <div>请拖拽到此处</div>;
};

return (
  <div
    ref={drop}
    style={{
      // ...省略部分代码
      background: error && isOver ? "red" : "#fff",
    }}
  >
    {showCanDrop()}
    {showValue()}
  </div>
);

通过 canDrop 重新自定义 collect 下的 canDrop 数据。当数据重复时,return false 并且设置错误的信息,让用户无法将重复的数据放到放置区。

当然界面上也会给出错误信息的提示,为了让错误信息展示得更醒目,会把背景也标记为红色。

请添加图片描述

当然在某些场景下,我们只需要将拖拽出去的数据删除,就能保证数据的唯一性了。

这时组件传递的 onChange 方法就显得尤为重要。

回到 pages/dnd/index.jsx 文件中来:

const dropChange = (res: any[]) => {
  const valList = (res || []).map((item) => item?.value);
  const filterList = dndList.filter((item) => !valList.includes(item.value));
  setList(filterList);
};

return (
  <DndProvider backend={HTML5Backend}>
    {/* 省略部分重复代码 */}
    <div style={{ marginTop: "10px" }}>请放置:</div>
    <CustDrop onChange={dropChange} />
  </DndProvider>
);

每次放置区的数据改变时,都将当前放置区的数据抛出去进行过滤。

请添加图片描述

至此,最基本的拖拽功能小伙伴们已经顺利掌握了。

四、调整顺序能力

放置区的内容因为是用户自行拖拽的,所以存在用户自身操作问题导致的顺序错误,如果无法通过拖拽实现顺序的调整,则需要用户全部删除错误的数据重新拖拽,操作过于繁琐,因此更体现了放置区调整顺序能力的重要性。

要实现调整顺序的能力,需要给放置区的每一项都增加 react-dnd

所以我们需要小小调整下放置区的代码。

/components/CustDrop/index.tsx

import DropItem from "@/components/DropItem";

const delItem = (ind: number) => {
  const newValue = [...value];
  newValue.splice(ind, 1);
  setValue(newValue);
  onChange(newValue);
};

const showValue = () => {
  return value.map((item, index: number) => {
    return (
      <DropItem key={item?.value} data={item} moveRow={moveRow} index={index} delItem={delItem} />
    );
  });
};

/components/DropItem/index.tsx

给每个项都设置 drop(drag(ref));

import { useDrop, useDrag } from "react-dnd";

const DropItem: FC<DropItemProps> = ({ data, index, moveRow, delItem }) => {
  const subFormItemRef = useRef(null);

  const [{ isDragging }, drag] = useDrag({
    type: "SubFormItem",
    item: {
      index,
      type: "SubFormItem",
    },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  });

  const [, drop] = useDrop({
    accept: "SubFormItem",
    drop: (item: any) => {
      moveRow(item.index, index);
    },
  });

  drop(drag(subFormItemRef));

  return (
    <div style={{ display: "flex" }}>
      <div ref={subFormItemRef} style={{ opacity: isDragging ? 0.5 : 1, cursor: "move" }}>
        {data?.label}
      </div>
      <span style={{ paddingLeft: "20px" }} onClick={() => delItem(index)}>
        删除
      </span>
    </div>
  );
};

至此,教程接近尾声。小伙伴快点操练起来吧。

demo 地址

demo 启动流程:

pnpm i

pnpm start

http://localhost:8000/#/dnd
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

react-dnd 拖拽能力教程 的相关文章

随机推荐

  • BSD、Apache、MIT、GPL、LGPL几种常见的开源协议

    转载地址 https www cnblogs com Vito2008 p 4806677 html 1 BSD开源协议 original BSD license FreeBSD license Original BSD license B
  • u盘安装ubuntu问题:卡在引导界面不动

    问题 一直卡在如图界面不动 分析 既然一直提示syslinux 那我们就看看他是什么东西吧 原因 syslinux分区引导记录问题 解决方案1 安装bootice软件 将制作好的启动盘插入电脑 用bootice更改syslinux引导记录
  • 8.10:如何在Python中判断文件类型?

    在计算机科学领域中 文件类型判断是一个非常基础和重要的问题 不同类型的文件需要采取不同的处理方式 因此在处理文件时 我们需要准确地判断文件类型 Python作为一门流行的编程语言 提供了许多方法来判断文件类型 在本文中 我们将介绍几种常见的
  • @vitejsplugin-vue requires vue (>=3.2.13) or @vuecompiler-sfc to be present in the dependency tree

    运行项目的时候 首先会提示要安装 vue compiler sfc 但是安装后运行项目成功但是页面是空白并且报错 VUE HMR RUNTIME is not defined 摸索了半天 查看到package json依赖文件 没有vue
  • RTX线程通信之——线程标志

    文章目录 Thread Flags 概念 RTX线程标志API 案例 LED灯同步闪亮 小结 参考资料 Thread Flags In a real application we need to be able to communicate
  • mbedtls 入门第四课--移植mbedtls到VS和ESP8266--8266SDK SHA256移植

    承接上篇 我们初步了解了mbedtls的文件路径以及文件作用以后就是想着如何将mbedtls移植到各种平台 博主这里只有两种移植方法 第一是将代码移植到VS中 第二个是将代码移植到博主跑动的比较多的小众SOC ESP8266 移植到ESP8
  • 【华为OD机试】五子棋迷(C++ Python Java)2023 B卷

    时间限制 C C 1秒 其他语言 2秒 空间限制 C C 262144K 其他语言524288K 64bit IO Format lld 题目描述 张兵和王武是五子棋迷 工作之余经常切磋棋艺 这不 这会儿又下起来了 走了一会儿 轮张兵了 对
  • 技术积累 — Keil 查看内存占用/优化代码

    原文链接 转自Sugar的专栏 转载文章 若有不妥 通知后我会立即删除 一 查看内存占用 1 使用Keil编辑代码时 编译成功后 双击红色框框位置 就会弹出 map文件 2 那么map文件中能够读出哪些信息呢 Program Size Co
  • caffe中lstm的实现以及lstmlayer的理解

    本文地址 http blog csdn net mounty fsc article details 53114698 本文内容 本文描述了Caffe中实现LSTM网络的思路以及LSTM网络层的接口使用方法 本文描述了论文 Long ter
  • 自学软件测试需要多久?怎么自学软件测试?自学软件测试可以找到工作吗? 绝对干货!

    一 前言 最近经常有很多朋友问我想要入行软件测试 但是都不知道该怎么学 这里详细的给大家说下 对于0基础的朋友 应该怎么去学习软件测试 学习软件测试有2条路可以选 1 找个靠谱的培训机构去培训啦 你就什么都不用想了 跟着培训结构认真的学习就
  • Hive Sql执行出错 Dag submit failed due to java.io.IOException: All datanodes DatanodeInfoWithStorage

    原因 根本原因是集群中的一个或多个信息块在所有节点中都已损坏 因此映射无法获取数据 命令 hdfs fsck list corruptfileblocks 可用于识别集群中损坏的块 当数据节点中打开的文件数量较少时 也会出现此问题 解决方案
  • 微信小程序传递数组给服务器,微信小程序页面间的数组如何传递

    A页面 数组 对象都需要stringify var listData JSON stringify that data listData var taskArray JSON stringify that data taskArray wx
  • visual studio2019(C#/.NET)安装教程

    前言 好久没有跟新版本了 博主还用的2017 看到最新的2019功能还是很强大的 版本可能越高越好 所以博主写了一个详细的博客 希望可以帮助到大家 一 visual studio 2019 下载 1 下载地址 visual studio官方
  • 爬取药品监督情况数据

    首先打开国家药品监督局的相应网址 国家药品监督局的相应网址 找到某一家企业点击相应的许可证编号那一个栏目 查看相应的许可证情况 上面对应的内容为我们需要爬取的对应的数据 不确定对上述的网页进行访问的时候 我们能够得到对应的企业名称 许可证编
  • apollo5.5感知模块改进

    apollo5 5感知模块改进 最近一直在研究百度的apollo源码感知部分 下面整理一下近期的一些知识点 apollo5 5在感知部分做了些升级 以适应最新的apollo自动驾驶套件 主要的特征及改进 1 全新的数据Pipline服务以及
  • 射发射整改案例

    文章摘录 http www elecfans com emc emi 1244893 html 试验现象 系统在300K 200MHz辐射发射超标 2 诊断过程 1 使用近场探头置于模块显示屏时 1MHz以下频段超标 则低频电磁干扰来源于显
  • pandas 行、列的增删改查

    读取tips xlsx及预览内容 行的增删改查 增加行 直接赋值 可以只写一个值 也可以写列表 df loc 40 1 df loc 40 1 2 3 4 5 6 7 append方法 append 增加行的方法 设置ignore inde
  • 数据结构--图的遍历(广度优先遍历、深度优先遍历)

    目录 图的遍历 广度优先遍历 BFS 广度优先遍历的代码实现 编辑 广度优先遍历序列 编辑 遍历序列的可变性 编辑 BFS算法完整版 编辑 广度优先遍历复杂度分析 广度优先生成树 广度优先生成森林 回顾广度优先遍历 深度优先遍历 DFS 回
  • Opencv项目实战:00 专栏内容介绍

    目录 Opencv项目实战专栏介绍 01 文字检测OCR 02 角度探测器 03 扫描二维码 条形码 04 全景图片拼接 05 物体检测 06 文档扫描仪 07 人脸识别和考勤系统 08 Yolov3更高精度的检测物体 09 物体尺寸测量
  • react-dnd 拖拽能力教程

    前言 近几年来 低代码 零代码的热度在国内逐年递增 复杂度同力一样不会消失 也不会凭空产生 它总是从一个物体转移到另一个物体或一种形式转为另一种形式 用户在使用低零代码构建应用程序时 这些能力只是被平台研发人员提前编写完了 作为低零代码的基