介绍
React Dnd是一组React高阶组件,使用的时候只需要将对应的api包裹目标元素,即可实现拖动或者接收拖动元素的功能。
安装
pnpm add react-dnd react-dnd-html5-backend -S
前置使用
如果要使用React DnD,需要在组件外部包裹DndProvider,并且backend的值为HTML5Backend
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
<DndProvider backend={HTML5Backend}>
<xxxx />
</DndProvider>
一个简单的小🌰
如图,实现将底部的色块拖动到大的盒子里面,并显示对应的色块
import SpaceItem from '@/components/spaceItem/SpaceItem'
import WhiteSpace from '@/components/whiteSpace'
import { cn, randomString } from '@/utils'
import { Card, Col, Row, Table } from 'antd'
import { useRef, useState } from 'react'
import { DropTargetMonitor, useDrag, useDrop } from 'react-dnd'
import { arrayMove } from 'react-sortable-hoc'
type Item = {
color: string
}
// 用来装的盒子
function Box() {
const [drops, setDrops] = useState<Item[]>([])
const [{ isOver, canDrop }, drop] = useDrop({
accept: 'item',
drop: (item: Item, monitor: DropTargetMonitor<Item>) => {
console.log(item, monitor)
setDrops((drops) => [...drops, item])
},
collect(monitor) {
return {
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}
},
})
return (
<div
ref={drop}
className={cn('w-full h-[300px] bg-white border-2 border-orange-400')}
>
<SpaceItem align="left" styles={{ flexWrap: 'wrap' }}>
{drops.map((item, index) => (
<div
key={index}
className={cn(`w-[50px] h-[50px]`)}
style={{ backgroundColor: item.color }}
/>
))}
</SpaceItem>
</div>
)
}
// 用来拖拽的元素
function Item(props: Item) {
const [{ isDragging }, drag] = useDrag({
type: 'item',
item: props,
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
})
return (
<div
ref={drag}
className={cn(
`w-[50px] h-[50px]`,
isDragging ? 'opacity-30 shadow-md' : 'opacity-100',
)}
style={{ backgroundColor: props.color }}
/>
)
}
// 拖拽
export default function DND() {
const generateDatas = (length = 10, id = '') => {
return Array.from({ length }, (_, index) => ({
id: id ? `${id}${index}` : index,
title: randomString(5),
content: randomString(20),
}))
}
const [lists, setLists] = useState<ListItem[]>(generateDatas())
return (
<Card size="small">
<Row gutter={24}>
<Col span={8}>
<Box />
<WhiteSpace gap={20} />
<SpaceItem align="left">
<Item color="red" />
<Item color="orange" />
<Item color="pink" />
<Item color="blue" />
<Item color="green" />
</SpaceItem>
</Col>
</Row>
</Card>
)
}
useDrag
让元素可以被拖动
返回值:返回值一共三个 1、第一个值是collect里面返回的所有值,常用于表示状态 2、第二个值是拖拽物的ref 3、第三个值是处于拖拽状态的拖拽物的引用,一般不用
参数:参数一共两个 1、第一个参数是一个对象,用于描述drag的信息,传递数据等
type
表示当前拖拽物的类型,对应useDrop里面的accept的值,只有type和accept值相同的才能互相拖拽并接收拖拽
item
用于传递一些数据,在useDrop里面的drop函数的第一个参数能接收到item传递的数据
collect
是一个函数,参数是monitor(DragSourceMonitor),它的返回值是useDrag的第一个返回值,常用于返回一些拖拽状态之类的数据
canDrag
表示是否可以拖拽,可以是一个boolean,也可以是一个函数,返回一个boolean,参数是monitor,可以覆盖Monitor中的canDrag方法
isDragging
表示是否正在拖拽中,可以是一个boolean,也可以是一个函数,返回一个boolean,参数是monitor,可以覆盖Monitor中的isDragging方法
isDragging: (monitor) => {
return monitor.getItem() ? index === monitor.getItem().index : false;
},
collect: (monitor: any) => ({
//当传入isDragging方法时,monitor.isDragging()方法指代传入的方法
isDragging: monitor.isDragging(),
}),
monitor
monitor里面包含了 canDrag、isDragging、getItemType、getItem、getDropResult等方法
2、第二个参数是依赖,是一个数组
useDrop
让元素可以放置(接收)拖拽元素
返回值:返回值一共两个 1、第一个值是collect里面返回的所有值,常用于表示状态 2、第二个值是放置物的ref
参数:参数只有一个,是一个对象
accept
对应useDrag的第一个参数的type字段的值,只有type和accept值相同的才能互相拖拽并接收拖拽
drop
代表拖拽元素被拖拽进接收物里面的回调,是一个函数,参数为item和monitor(DropTargetMonitor),item为useDrag的第一个参数的item字段的值
hover
代表拖拽元素悬浮在接收物上的回调,是一个函数,参数为item和monitor(DropTargetMonitor),item为useDrag的第一个参数的item字段的值
canDrop
是一个函数或者boolean,代表是否可以接收拖拽物,参数为item和monitor(DropTargetMonitor),item为useDrag的第一个参数的item字段的值
collect
是一个函数,参数是monitor(DropTargetMonitor),它的返回值是useDrop的第一个返回值,常用于返回一些接受状态之类的数据
monitor
monitor里面包含了canDrop、isOver、getItemType、getItem、getDropResult等方法
卡片拖拽🌰
import SpaceItem from '@/components/spaceItem/SpaceItem'
import WhiteSpace from '@/components/whiteSpace'
import { cn, randomString } from '@/utils'
import { Card, Col, Row, Table } from 'antd'
import { useRef, useState } from 'react'
import { DropTargetMonitor, useDrag, useDrop } from 'react-dnd'
import { arrayMove } from 'react-sortable-hoc'
type ListItem = {
id: number | string
title: string
content: string
}
// 用来拖拽的元素
function ListItem(
props: ListItem & {
index: number
swapPosition: (dragIndex: number, targetIndex: number) => void
},
) {
const { id, title, content, index, swapPosition } = props
const ref = useRef(null)
const [{ isDragging }, drag] = useDrag({
type: 'listItem',
item: { ...props, index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
})
const [, drop] = useDrop({
accept: 'listItem',
hover(item: ListItem & { index: number }) {
swapPosition(item.index, index)
item.index = index
},
})
drop(drag(ref))
return (
<div
ref={ref}
className={cn('w-full bg-cyan-300 my-3 p-2 rounded-md', {
'bg-orange-500': isDragging,
})}
>
<div>{id}</div>
<div>{title}</div>
<div>{content}</div>
</div>
)
}
// 拖拽
export default function DND() {
const generateDatas = (length = 10, id = '') => {
return Array.from({ length }, (_, index) => ({
id: id ? `${id}${index}` : index,
title: randomString(5),
content: randomString(20),
}))
}
const [lists, setLists] = useState<ListItem[]>(generateDatas())
const swapPosition = (dragIndex: number, targetIndex: number) => {
console.log(dragIndex)
console.log(targetIndex)
if (dragIndex !== targetIndex) {
setLists((pre) => arrayMove(pre, dragIndex, targetIndex))
}
}
return (
<Card size="small">
<Row gutter={24}>
<Col span={8}>
<div className="border-2 border-red-500 p-2 rounded-lg">
{lists.map((item, index) => (
<ListItem
key={item.id}
{...item}
index={index}
swapPosition={swapPosition}
/>
))}
</div>
</Col>
</Row>
</Card>
)
}
卡片拖拽例子2
两个大卡片可以拖拽排序,单个大卡片里的小卡片可以相互拖拽排序,不同的大卡片里面的小卡片也可以相互拖拽
import SpaceItem from '@/components/spaceItem/SpaceItem'
import WhiteSpace from '@/components/whiteSpace'
import { cn, randomString } from '@/utils'
import { Card, Col, Row, Table } from 'antd'
import { useRef, useState } from 'react'
import { DropTargetMonitor, useDrag, useDrop } from 'react-dnd'
import { arrayMove } from 'react-sortable-hoc'
type ListItem = {
id: number | string
title: string
content: string
}
type swapBoxItemListFn = (
sourceIndex: number,
destinationIndex: number,
sourceBoxItemId: number,
destinationBoxItemId: number,
) => void
type BoxItemProps = {
index: number
id: number
listData: ListItem[]
swap: (sourceIndex: number, destinationIndex: number) => void
swapBoxItemList: swapBoxItemListFn
}
type BoxListItemProps = {
index: number
id: number | string
swap: swapBoxItemListFn
boxItemId: number // 自己所属大盒子的id
} & ListItem
// 用来拖拽的大盒子元素
function BoxItem(props: BoxItemProps) {
const { index, id, listData, swapBoxItemList } = props
const ref = useRef(null)
const [{ isDragging }, drag] = useDrag({
type: 'boxItem',
item: props,
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
})
const [, drop] = useDrop({
accept: 'boxItem',
drop(item: BoxItemProps) {
const { index: ind, swap } = item
swap(ind, index)
},
})
drop(drag(ref))
return (
<div
ref={ref}
className={cn('w-[50%] bg-cyan-300 my-3 p-2 rounded-md pt-8', {
'bg-orange-500 opacity-30': isDragging,
})}
>
<div className="text-center text-[24px]">{id}</div>
{listData?.map((item, index) => (
<BoxListItem
key={item.id}
boxItemId={id}
index={index}
{...item}
swap={swapBoxItemList}
/>
))}
</div>
)
}
// 用来拖拽的大盒子里面的字元素
function BoxListItem(props: BoxListItemProps) {
const { index, boxItemId, id, title, content } = props
const ref = useRef(null)
const [{ isDragging }, drag] = useDrag({
type: 'boxListItem',
item: props,
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
})
const [, drop] = useDrop({
accept: 'boxListItem',
drop(item: BoxListItemProps) {
const { index: ind, swap, boxItemId: boxId } = item
swap?.(ind, index, boxId, boxItemId)
},
})
drop(drag(ref))
return (
<div
ref={ref}
className={cn('w-full bg-green-300 my-3 p-2 rounded-md', {
'bg-pink-500': isDragging,
})}
>
<div>{id}</div>
<div>{title}</div>
<div>{content}</div>
</div>
)
}
// 拖拽
export default function DND() {
const generateDatas = (length = 10, id = '') => {
return Array.from({ length }, (_, index) => ({
id: id ? `${id}${index}` : index,
title: randomString(5),
content: randomString(20),
}))
}
const [boxItems, setBoxItems] = useState<number[]>([1, 2])
const [list1, setList1] = useState(generateDatas(10, '1-'))
const [list2, setList2] = useState(generateDatas(10, '2-'))
// 交换大盒子
const swapBoxItem = (sourceIndex: number, destinationIndex: number) => {
setBoxItems((pre) => arrayMove(pre, sourceIndex, destinationIndex))
}
// 交换大盒子里面的
const swapBoxItemList = (
sourceIndex: number,
destinationIndex: number,
sourceBoxItemId: number,
destinationBoxItemId: number,
) => {
// 判断是否是跨盒子拖拽
const isSmaeBox = sourceBoxItemId === destinationBoxItemId
if (isSmaeBox) {
if (sourceBoxItemId === 1) {
setList1((pre) => arrayMove(pre, sourceIndex, destinationIndex))
} else if (sourceBoxItemId === 2) {
setList2((pre) => arrayMove(pre, sourceIndex, destinationIndex))
}
} else {
if (sourceBoxItemId === 1) {
const item = list1[sourceIndex]
setList1((pre) => {
pre.splice(sourceIndex, 1)
console.log(pre)
return Array.from(pre)
})
setList2((pre) => {
pre.splice(destinationIndex, 0, item)
return Array.from(pre)
})
} else if (sourceBoxItemId === 2) {
const item = list2[sourceIndex]
setList2((pre) => {
pre.splice(sourceIndex, 1)
return Array.from(pre)
})
setList1((pre) => {
pre.splice(destinationIndex, 0, item)
return Array.from(pre)
})
}
}
}
return (
<Card size="small">
<Row gutter={24}>
<Col span={16}>
<WhiteSpace gap={24} />
<div className="flex border-2 border-orange-400 rounded-lg p-2 gap-6">
{boxItems.map((item, index) => (
<BoxItem
key={item}
id={item}
index={index}
swap={swapBoxItem}
swapBoxItemList={swapBoxItemList}
listData={item === 1 ? list1 : list2}
/>
))}
</div>
</Col>
</Row>
</Card>
)
}
表格排序
import SpaceItem from '@/components/spaceItem/SpaceItem'
import WhiteSpace from '@/components/whiteSpace'
import { cn, randomString } from '@/utils'
import { Card, Col, Row, Table } from 'antd'
import { useRef, useState } from 'react'
import { DropTargetMonitor, useDrag, useDrop } from 'react-dnd'
import { arrayMove } from 'react-sortable-hoc'
// 表格拖拽
function TrItem(props: any) {
const { index: destination, swaptr, ...rests } = props
const ref = useRef(null)
const [{ isDragging }, drag] = useDrag({
type: 'trItem',
item: props,
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
})
const [, drop] = useDrop({
accept: 'trItem',
drop(item: any) {
const { index: sourceIndex } = item
swaptr(sourceIndex, destination)
},
})
drop(drag(ref))
return (
<tr {...rests} ref={ref} className={cn({ 'bg-pink-400': isDragging })} />
)
}
// 拖拽
export default function DND() {
const generateDatas = (length = 10, id = '') => {
return Array.from({ length }, (_, index) => ({
id: id ? `${id}${index}` : index,
title: randomString(5),
content: randomString(20),
}))
}
const [dataSource, setDataSource] = useState(generateDatas(15))
// 交换表格
const swaptr = (sourceIndex: number, destinationIndex: number) => {
setDataSource((pre) => arrayMove(pre, sourceIndex, destinationIndex))
}
return (
<Card size="small">
<Row gutter={24}>
<Col span={16}>
<WhiteSpace gap={24} />
<Table
dataSource={dataSource}
pagination={false}
columns={[
{ title: 'ID', dataIndex: 'id' },
{ title: '标题', dataIndex: 'title' },
{ title: '内容', dataIndex: 'content' },
]}
rowKey="id"
components={{
body: {
row: (props: any) => {
const rowKey = props['data-row-key']
const index = dataSource.findIndex(
(item) => item.id === rowKey,
)
return <TrItem {...props} index={index} swaptr={swaptr} />
},
},
}}
/>
</Col>
</Row>
</Card>
)
}
tips: 以上示例代码中出现的部分SpaceItem WhiteSpace cn randomString 均为本人项目中的封装,仅供参考。