React DnD 是什么?
React DnD是React和Redux核心作者 Dan Abramov创造的一组React 高阶组件,可以在保持组件分离的前提下帮助构建复杂的拖放接口;
其中一些概念类似于Flux和Redux架构。
这并非巧合,因为 React DnD 在内部使用 Redux。
开始之前我们先来看一下效果;
先上代码:
import React, { useState, useCallback, useRef, useEffect, } from 'react'; import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import update from 'immutability-helper'; import { CloseOutlined, CloseCircleFilled } from '@ant-design/icons'; import PropTypes from 'prop-types'; import './dragModule.scss'; const type = 'DragableBodyRow'; const DragableBodyRow = ({ index, moveRow, className, style, label, value, onRemoveClick, }) => { const ref = useRef(); const [{ isOver, dropClassName }, drop] = useDrop( () => ({ accept: type, collect: (monitor) => { const { index: dragIndex } = monitor.getItem() || {}; if (dragIndex === index) { return {}; } return { isOver: monitor.isOver(), dropClassName: dragIndex < index ? ' drop-over-downward' : ' drop-over-upward', }; }, drop: (item) => { moveRow(item.index, index); }, }), [index, moveRow], ); const [, drag] = useDrag( () => ({ type, item: { index }, // canDrag: index > 1, // 收集器函数 // collect: monitor => ({ // isDragging: monitor.isDragging(), // }), }), [index], ); drag(drop(ref)); return ( <div ref={ref} className={`${className}${isOver ? dropClassName : ''}`} style={{ cursor: 'move', ...style }} > <span className="drag-list-item"> <span className="drag-list-item-content">{label}</span> <CloseOutlined className="drag-list-item-remove" onClick={() => onRemoveClick(value)} /> </span> </div> ); }; DragableBodyRow.propTypes = { index: PropTypes.number, moveRow: PropTypes.func.isRequired, className: PropTypes.string, style: PropTypes.object, label: PropTypes.string, value: PropTypes.number, onRemoveClick: PropTypes.func, }; const DragModule = ({ value, onChange }) => { const [dragData, setData] = useState([]); useEffect(() => { let currentData = []; if (dragData.length > value.length) { currentData = dragData.filter(a => value.find(b => a.value === b.value)); } else { currentData = dragData.concat(value.filter(a => !(dragData.find(b => a.value === b.value)))); } setData(currentData); // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); const moveRow = useCallback( (dragIndex, hoverIndex) => { const dragRow = dragData[dragIndex]; setData( update(dragData, { $splice: [ [dragIndex, 1], [hoverIndex, 0, dragRow], ], }), ); }, [dragData], ); const onRemoveClick = (key) => { if (key) { const newData = value.filter(i => i.value !== key); onChange(newData); } else { onChange([]); } }; return ( <DndProvider backend={HTML5Backend}> <div className="tag-select-drag"> <div className="tag-select-drag-overflow"> { dragData.map((item, index) => ( <DragableBodyRow key={String(index)} index={index} moveRow={moveRow} label={item.label} value={item.value} onRemoveClick={onRemoveClick} className="drag-list-overflow" /> )) } </div> <CloseCircleFilled className="tag-select-drag-allclear" onClick={() => onRemoveClick()} /> </div> </DndProvider> ); }; DragModule.propTypes = { value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), onChange: PropTypes.func, }; DragModule.defaultProps = { value: undefined, onChange: () => {}, }; export default DragModule;
const length = 10;const value = [];for (let i = 0; i < length; i++) { value.push({ value: i, label: `这是第${i}列` })}
<DragModule value={value}/>
复制代码
安装
npm install react-dnd react-dnd-html5-backend
复制代码
React DnD 的基本概念
items & types
与 Flux(或 Redux)一样,React DnD 使用数据而不是视图作为事实的来源。当您在屏幕上拖动某物时,我们并不是说正在拖动组件或 DOM 节点。
所以这里用一个“数据对象对象items”描述拖动的元素,例如{id: 1}
types类型是唯一标识应用程序中整个项目类别的字符串(或符号),类型很有用,因为随着您的应用程序的增长您可能希望使更多内容可拖动,但您不一定希望所有现有的放置目标突然开始对新项目做出反应。这些类型允许您指定兼容的拖放源和放置目标。您可能会在您的应用程序中枚举类型常量,类似于您可能如何枚举 Redux 操作类型。
Backend
React DnD 使用HTML5 拖放 API。
这是一个合理的默认设置,因为它会截取拖动的 DOM 节点并将其用作开箱即用的“拖动预览”。当光标移动时,您不必进行任何绘图,这很方便。此 API 也是处理文件放置事件的唯一方法。
它所做的就是将 DOM 事件转换为 React DnD 可以处理的内部 Redux 操作
Monitors
React DnD 通过 Monitor 来存储拖放状态并且提供查询;
Connectors
Backend 关注 DOM 事件,组件关注拖放状态,connector 可以连接组件和 Backend ,可以让 Backend 获取到 DOM。
Hooks API
useDrag
该useDrag钩子提供了一种将您的组件作为拖动源连接到 DnD 系统的方法。
import { useDrag } from 'react-dnd'
function DraggableComponent(props) {
const [collected, drag, dragPreview] = useDrag(() => ({
type, // 必须,唯一标识
item: { id }, // 必须:描述被拖动数据的普通 JavaScript 对象
collect: () => {},// 可选, 返回的对象供组件使用 -> collectedProps
canDrag: () => {}, // 元素是否可拖拽
...
}))
return collected.isDragging ? (
<div ref={dragPreview} />
) : (
<div ref={drag} {...collected}>
...
</div>
)
}
复制代码
collected: 包含从 collect 函数收集的属性的对象。如果没有collect定义函数,则返回一个空对象。
drag: 拖动源的连接器功能。这必须附加到 DOM 的可拖动部分。
dragPreview: 拖动预览的连接器功能。这可能会附加到 DOM 的预览部分。
useDrop
该useDrop钩子提供了一种将组件连接到 DnD 系统作为放置目标的方法。
import { useDrop } from 'react-dnd'
function myDropTarget(props) {
const [collectedProps, drop] = useDrop(() => ({
accept, // 必须,与useDrag中的type对应;
collect: () => {},// 可选, 返回的对象供组件使用 -> collectedProps
drop: () => {}, // 可选的。在目标上放置兼容项目时调用。
hover: () => {}, // 可选的。当项目悬停在组件上时调用。
canDrop: () => {}, // 可选, 元素是否可放置
}))
return <div ref={drop}>Drop Target</div>
}
复制代码
collected: 包含从 collect 函数收集的属性的对象。如果没有collect定义函数,则返回一个空对象。
drop:放置目标的连接器功能。这必须附加到 DOM 的放置目标部分。
更好的更新复杂数据
immutability-helper
参考: