用react-dnd做一个可拖拽排列的list

今天的需求来啦:做一个可以通过拖拽排列的list。最终效果是这样:

开始之前,我找了个市面上比较主流的工具库,做个比对:

这个是atlassian用来开发jira看板功能的库,有动画效果,很漂亮,确实对得起这个包的名字。优点是相对易用,而且好看,缺点是仅适合开发list一类的组件,而且几年前就已经不再维护了,对于新版的函数式react可能不太兼容。

  • dnd-kit
    这个是atlassian 在弃用react-beautiful-dnd后重新维护的一个版本,效果是一样的美丽,如果是web端的,还是很推荐的。
  • reat-dnd
    这是个比较全面的dnd库,可以实现各种功能,要注意的是这是基于H5的开发,如果你开发的端不是H5,可能不适用。

react-dnd的官方文档介绍了如何做一个棋盘,类似的,这篇文章将介绍如何用react-dnd做一个draggable list。

Draggable

首先将list中的每一个item做成draggable组件。

引入useDrag

tsx 复制代码
// ListItem.tsx
import { styled } from '@mui/material/styles';
import * as React from 'react';
import { useDrag } from 'react-dnd';

const StyledListItem = styled('div', {
  shouldForwardProp: prop => prop !== 'isDragging'
})<{ isDragging: boolean }>(({ isDragging, theme }) => ({
  opacity: isDragging ? 0.5 : 1,
  border: '1px blue solid',
}));

const ListItem = () => {
  const [{isDragging}, dragRef] = useDrag(() => ({
    type: 'listItem',
    collect: monitor => ({
      isDragging: !!monitor.isDragging(),
    }),
  }))

  return (
    <StyledListItem ref={dragRef} isDragging={isDragging}>
      list item
    </StyledListItem>
  )
}

export default ListItem;

这样我们在demo中就能看到,这个组件就可以用鼠标拖了:

Droppable

我们再将list容器做成droppable组件,这样draggable组件才可以放在droppable的list里。

引入useDrop

tsx 复制代码
// SortableList.tsx
import React, { forwardRef } from 'react';
import { useDrop } from 'react-dnd';
import ListItem from './components/listItem';

const SortableList = () => {

  const canMoveItem = () => {
    console.log('canMoveItem')
    return true;    //<--- 具体逻辑后面再写,这里先设为true,即容器内部一直是可以drop的。
  }

  const moveItem = () => {
    console.log('moveItem');    //<--- 具体逻辑后面再写。
  }

  const [{ isOver, canDrop }, dropRef] = useDrop(
    () => ({
      accept: 'listItem',
      canDrop: () => canMoveItem(),
      drop: () => moveItem(),
      collect: (monitor) => ({
        isOver: !!monitor.isOver(),
        canDrop: !!monitor.canDrop()
      })
    }),
    [canMoveItem, moveItem]
  )
  
  return (
    <div>
      isOver: {String(isOver)}<br />
      canDrop: {String(canDrop)}
      <div
        ref={dropRef}
        style={{
          position: 'relative',
          width: '600px',
          height: '400px',
          border: '2px purple solid'
        }}
      >
        <ListItem></ListItem>
      </div>
    </div>
  );
}

export default SortableList;

这样页面上,我再容器内部拖动时,isOvertruecanDroptrue,拖到容器外isOver就变成false了:

既是draggable,又是droppable

当有多个list item,可以通过拖拽调整顺序,这样我需要将list item也变成droppable

tsx 复制代码
// ListItem.tsx
// 将list item也变成droppable
const [spec, dropRef] = useDrop({
    accept: 'listItem',
    hover: (item, monitor) => {
        const dragIndex = item.index
        const hoverIndex = index
        const hoverBoundingRect = ref.current?.getBoundingClientRect()
        const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
        const hoverActualY = monitor.getClientOffset().y - hoverBoundingRect.top

        // 如果是向下拖,只有当hover的坐标小于自身高度的一半时,继续保持drag,否则相当于要挪动其他的list item,给当前的item腾个地方
        // if dragging down, continue only when hover is smaller than middle Y
        if (dragIndex < hoverIndex && hoverActualY < hoverMiddleY) return
        // 如果是向上拖,只有当hover的坐标大于自身高度的一半时,继续保持drag,否则相当于要挪动其他的list item,给当前的item腾个地方
        if (dragIndex > hoverIndex && hoverActualY > hoverMiddleY) return
        moveListItem(dragIndex, hoverIndex)
        item.index = hoverIndex
    },
});

那么如何让一个list item既是draggable,又是droppable呢?用以下方式:

tsx 复制代码
// ListItem.tsx
const ref = useRef(null);

const dragDropRef = dragRef(dropRef(ref))

重排list

上一步我们把moveListItem传给了父组件,那么在父组件中,我们可以这样来改变list的顺序:

tsx 复制代码
// Mock data
const PETS = [
  { id: 1, name: 'dog' },
  { id: 2, name: 'cat' },
  { id: 3, name: 'fish' },
  { id: 4, name: 'hamster' },
]
tsx 复制代码
// SortableList.tsx
const moveListItem = useCallback(
    (dragIndex, hoverIndex) => {
        console.log({dragIndex, hoverIndex})
        const dragItem = pets[dragIndex]
        const hoverItem = pets[hoverIndex]
        // 将PETS数组中,drag的item和hover的item交换位置
        setPets(pets => {
            const updatedPets = [...pets]
            updatedPets[dragIndex] = hoverItem
            updatedPets[hoverIndex] = dragItem
            return updatedPets
        })
    },
    [pets],
)

这样我们就能得到这样的效果:

这样一个sortable list就大功告成啦!

总结

现在这个list是没有任何动画的,要是listItem在移动的过程中是有个过渡效果的,就显得丝滑多了,那么接下来我将用react-flip-move将动画效果加到每个item中,期待下一篇文章吧,各位可以先收藏,动画的文章完成后我会将链接贴到这里。

相关推荐
王哲晓9 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v14 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云24 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry1 小时前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x2 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落3 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
理想不理想v3 小时前
vue经典前端面试题
前端·javascript·vue.js