React-DnD拖拽库的使用

85 阅读6分钟

介绍

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>

一个简单的小🌰

image.png

如图,实现将底部的色块拖动到大的盒子里面,并显示对应的色块

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等方法

卡片拖拽🌰

image.png

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

两个大卡片可以拖拽排序,单个大卡片里的小卡片可以相互拖拽排序,不同的大卡片里面的小卡片也可以相互拖拽

image.png

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>
  )
}

表格排序

image.png

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 均为本人项目中的封装,仅供参考。