react-dnd 拖拽能力教程

news2024/11/19 8:26:35

前言

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

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

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

让一个元素支持拖动是一件非常容易做到的事情,只需要在对应的 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/82237.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SQL SERVER 2019卸载和安装

卸载过程删除SQL Server2019包括sql server这个数据库和它的管理工具SQLServer Management Studio以及他们的注册表信息和安装的目录&#xff0c;以上&#xff0c;最重要的是一定要有耐心&#xff0c;一步一步慢慢来。 首先打开一定要把SQL的服务都关掉&#xff0c;这个很重要…

压缩包文件如何设置加密、删除加密?

压缩包是将文件压缩成RAR、ZIP格式文件&#xff0c;将文件压缩成压缩包之后&#xff0c;就更方便转发以及保存&#xff0c;而且压缩包文件可以进行加密&#xff0c;这样也能够起到对文件的保护作用&#xff0c;今天和大家分享如何对压缩包进行加密以及如何删除压缩包密码。 压…

战略,就没一本好书

战略这个词被工商业界引爆&#xff0c;都是1965年的事。想来到现在已经快60年了。但是实话说&#xff0c;战略这么重要的事&#xff0c;其实没几本好书&#xff0c;也就是说&#xff0c;这个领域&#xff0c;实在没什么有效的研究成果。&#xff08;1&#xff09;起点1965年是个…

Vue 进阶二 | 系统性学习 | 无知的我费曼笔记

无知的我正在复盘Vue 该笔记特点是 重新整理了涉及资料的一些语言描述、排版而使用了自己的描述对一些地方做了补充说明。比如解释专有名词、类比说明、对比说明、注意事项提升了总结归纳性。尽可能在每个知识点上都使用一句话 || 关键词概括更注重在实际上怎么应用提出并回答…

数据可视化之卡塔尔世界杯,世界杯8强全部出炉,你看好那支队伍?

2022年下半年可算是集结了众多国际赛事&#xff0c;前有csgo major&#xff0c;英雄联盟总决赛&#xff0c;后有斯诺克英锦赛。当然这些赛事里面最万众瞩目的就是4年一度的卡塔尔世界杯了。本届世界杯开赛前最大的看点就是世界杯的花费&#xff0c;卡塔尔2022年世界杯花费2290亿…

他让我看重采样

周末邓总让我帮忙看下重采样的代码&#xff0c;然后我就用上了自己的神器。我的神器就是Google之后总结了下代码&#xff0c;完整的代码可以往下看&#xff0c;我们平时也会用到重采样&#xff0c;通道转换、交织和非交织的相互转换、给音频重新map等等。这些都是做音频需要搞的…

(红帽系统)redhat7.2 相关服务器配置

远程连接服务器配置 简介 使用SSH可以在本地主机和远程服务器之间进行加密的传输数据&#xff0c;实现数据的安全传输。而OpenSSH是SSH协议的免费开源实现&#xff0c;它采用安全加密的网络连接工具代替了telnet、ftp等 实现步骤 第一步 进入红帽系统 第二步 检查安装系统时…

需求处理的流程及问题挑战

本文主要讲需求的一般处理流程&#xff0c;以及可能存在的问题及挑战。 一、需求处理流程&#xff1a; 1、需求的生命周期&#xff1a; 起点是提需求&#xff0c;终点是拒绝或接受需求。每个人希望自己的需求能被接受或满足&#xff0c;但资源总是有限的。每个需求从产生到实…

SRM系统的国内品牌前五名是哪几家,大概价位是多少?

SRM系统的国内品牌前五名是哪几家&#xff0c;大概价位是多少&#xff1f;SRM系统是采购数字化转型过程中的产物&#xff0c;SRM系统与ERP与SCM系统打通后&#xff0c;能够破除信息壁垒&#xff0c;增加采购部门与业务部门的沟通效率&#xff0c;从而实现企业人、物、财成本的降…

JavaSE——多线程详细

目录 一、多线程 1.1 基本介绍 1.2 进程和线程的关系 1.3 多线程并发概念 二、实现线程的方式 2.1 继承Thread类 2.2 实现java.lang.Runnable接口 2.3 匿名类 2.4 实现Callable接口&#xff08;JDK8新特性&#xff09; 2.5 run和start的区别 2.6 线程声明周期 三、…

双非本科怎么了,照样拿到阿里 offer! 分享阿里技术四面 + 交叉面 +HR 面难忘经历

说一下 java 类加载器的工作机制&#xff1f;类加载在哪个区域进行的&#xff1f; 说一下 java 的线程模型&#xff1f; violate 了解吗&#xff1f;它的原理是什么&#xff1f;violate 是线程安全的吗&#xff1f; 保证线程安全的解决方法有哪些&#xff1f;说一说读写锁吧…

前端高频手写面试题总结

实现字符串的repeat方法 输入字符串s&#xff0c;以及其重复的次数&#xff0c;输出重复的结果&#xff0c;例如输入abc&#xff0c;2&#xff0c;输出abcabc。 function repeat(s, n) {return (new Array(n 1)).join(s); }递归&#xff1a; function repeat(s, n) {return…

通过 JFR 与日志深入探索 JVM - TLAB 原理详解

什么是 TLAB&#xff1f; TLAB&#xff08;Thread Local Allocation Buffer&#xff09;线程本地分配缓存区&#xff0c;这是一个线程专用的内存分配区域。既然是一个内存分配区域&#xff0c;我们就先要搞清楚 Java 内存大概是如何分配的。 我们一般认为 Java 中 new 的对象…

模板模式

文章目录思考模板模式1.模板模式的本质2.何时选用模板模式3.优缺点4.模板方法的结构5.实现思考模板模式 模板模式其实就是抽离共用方法到抽象类中&#xff0c;然后再规定其具体实现步骤 1.模板模式的本质 模板方法模式的本质:固定算法骨架。 模板方法模式主要是通过制定模板&am…

系统集成企业需具备哪些证书?

IT信息化企业&#xff0c;系统集成企业需要做的资质证书有哪些&#xff1f;经常遇到有新成立的系统集成商问智达鑫业小编&#xff0c;该申请哪些企业资质&#xff0c;接下来了小编整理下目前市场上使用频率比较高的一些资质证书&#xff0c;大家可以参考下。 信息系统建设和服务…

A-Level考试常见问题综合解答

关于A Level的Q&A 问&#xff1a;参加A Level的考试与其他考试相比有什么优势吗&#xff1f; 答&#xff1a;A Level考试的门数相较其他国际课程更少&#xff0c;学生有更多的时间花费在每门课上取得更好的GPA和最终成绩。问&#xff1a;就读的学校就直接提供A Level课程&a…

jmeter断言

jmeter断言常用的有响应断言和json断言&#xff1b; 常用的响应断言&#xff1a; 1.字符串&#xff1a;如果响应中包含了指定的字符串&#xff0c;判断为成功&#xff0c;不支持正则表达式&#xff1b;如下图&#xff1a; 2.包括&#xff1a;如果响应中包含了指定的字符串&…

mac清空废纸篓怎么恢复?

众所周知&#xff0c;电脑只要在运行都会产生一些临时文件或者文档&#xff0c;而这些文件会存放在电脑的存储空间里&#xff0c;方便我们后续的使用。当Mac中存储的文件过多时&#xff0c;就会影响到我们的正常使用&#xff0c;只有通过清理电脑文件&#xff0c;来释放更多的存…

【JavaWeb开发-Servlet】拾起海中的漂流瓶超强版

目录 原版&#xff1a; 一、思路&#xff1a; 二、实现&#xff1a; 三、资源分享 四、部署服务器时记得修改文件路径 原版&#xff1a; 【JavaWeb开发-Servlet】拾起海中的漂流瓶增强版_代码骑士的博客-CSDN博客【代码】【JavaWeb开发-Servlet】拾起海中的漂流瓶增强版…

SMART PLC运动超驰功能编程应用(含V2.7版本固件下载)

什么是运动控制超驰功能,运动超驰功能如何开启,请参看下面的导图部分: 下面一步步教大家如何更新CPU固件版本。 S7-200 SMART PLC自定义脉冲控制功能块相关详细组态设置,请参看下面的博客。链接如下: S7-200 SMART PLC自定义脉冲轴控功能块AxisControl_FB(梯形图)_RXX…