今天的需求来啦:做一个可以通过拖拽排列的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;
这样页面上,我再容器内部拖动时,isOver
为true
,canDrop
为true
,拖到容器外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中,期待下一篇文章吧,各位可以先收藏,动画的文章完成后我会将链接贴到这里。