我总是忘记如何使用任何拖放库,直到我不得不再次使用它。目前,我一直在合作的是 react-beautiful-dnd,这是 Atlassian 为 Jira 等产品创建的库。我承认,我通常不是 Atlassian 产品的最大粉丝,但它是一个很好的拖放库,特别是对于列表的使用。
react-beautiful-dnd 确实有点冗长,尤其是在使用嵌套列表时,所以我将很多细节移到了可重用的组件上,并制作了一些演示与您分享。
目标及样例
我为这篇文章做了两个演示。首先,我创建了拖放组件,以简化 react-beautiful-dnd 库的使用。在第二个中,我再次使用这些组件进行嵌套拖放,您可以在其中拖动类别或在类别之间拖动项目。
- 拖放演示
- 嵌套拖放演示
这些演示几乎没有任何样式 - 我更感兴趣的是以尽可能少的样式展示原始功能,所以不要太关注它有多漂亮(不是)。
简单的拖放列表
首先,我们将制作一个简单的拖放列表。
您需要一个重新排序功能来获取拖放内容的新顺序:
helpers.js
export const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list)
const [removed] = result.splice(startIndex, 1)
result.splice(endIndex, 0, removed)
return result
}
DragDropContext、Draggable 和 Droppable 组件用于创建包含可拖动项的列表,因此我创建了一个 ListComponent 来处理完整的可拖动/可放置列表:
ListComponent.js
import React, { useState } from 'react'
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'
import { reorder } from './helpers.js'
export const ListComponent = () => {
const [items, setItems] = useState([
{ id: 'abc', name: 'First' },
{ id: 'def', name: 'Second' },
])
const handleDragEnd = (result) => {
const { source, destination } = result
if (!destination) {
return
}
const reorderedItems = reorder(items, source.index, destination.index)
setItems(reorderedItems)
}
return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="droppable-id">
{(provided, snapshot) => {
return (
<div ref={provided.innerRef} {...provided.droppableProps}>
{items.map((item, index) => {
return (
<Draggable draggableId={item.id} index={index}>
{(provided, snapshot) => {
return (
<div ref={provided.innerRef} {...provided.draggableProps}>
<div {...provided.dragHandleProps}>Drag handle</div>
<div>{item.name}</div>
</div>
)
}}
</Draggable>
)
})}
{provided.placeholder}
</div>
)
}}
</Droppable>
</DragDropContext>
)
}
如您所见,即使在最简单的示例中,它也嵌套了 12 层深。好在我们在 JavaScript 中使用了 2 空格缩进!我们还有多个提供和快照要处理,当它嵌套时,您现在有四个和多个占位符,所以它开始变得非常混乱。
我不使用隐式返回使情况变得更糟,但就我个人而言,我真的很不喜欢使用隐式返回,因为它使调试(阅读:console.log())东西变得更加困难。
无论如何,我喜欢将它们分解成自己的组件:拖放。
Drop
Drop 包含 Droppable,并传递引用和道具。我还添加了一个类型,该类型将与嵌套拖放一起使用,但暂时可以忽略。
Drop.js
import { Droppable } from 'react-beautiful-dnd'
export const Drop = ({ id, type, ...props }) => {
return (
<Droppable droppableId={id} type={type}>
{(provided) => {
return (
<div ref={provided.innerRef} {...provided.droppableProps} {...props}>
{props.children}
{provided.placeholder}
</div>
)
}}
</Droppable>
)
}
Drag
拖动手柄拖动手柄(奇数句),通常由六个点的图标表示,但对于此简化示例,它只是一个带有一些文本的 div。
Drag.js
import { Draggable } from 'react-beautiful-dnd'
export const Drag = ({ id, index, ...props }) => {
return (
<Draggable draggableId={id} index={index}>
{(provided, snapshot) => {
return (
<div ref={provided.innerRef} {...provided.draggableProps} {...props}>
<div {...provided.dragHandleProps}>Drag handle</div>
{props.children}
</div>
)
}}
</Draggable>
)
}
合在一起
我发现制作一个导出所有内容的索引.js组件很有用。
index.js
import { DragDropContext as DragAndDrop } from 'react-beautiful-dnd'
import { Drag } from './Drag'
import { Drop } from './Drop'
export { DragAndDrop, Drag, Drop }
Usage使用
现在,您可以使用拖放来代替本文开头的所有废话:
import React, { useState } from 'react'
import { DragAndDrop, Drag, Drop } from './drag-and-drop'
import { reorder } from './helpers.js'
export const ListComponent = () => {
const [items, setItems] = useState([
/* ... */
])
const handleDragEnd = (result) => {
// ...
}
return (
<DragAndDrop onDragEnd={handleDragEnd}>
<Drop id="droppable-id">
{items.map((item, index) => {
return (
<Drag key={item.id} id={item.id} index={index}>
<div>{item.name}</div>
</Drag>
)
})}
</Drop>
</DragAndDrop>
)
}
您可以在演示中看到整个事情协同工作。
更易于阅读,只要使用相同的拖动手柄样式,您就可以根据需要制作可拖动的外观。(我只添加了最基本的样式来区分元素。
带有类别的嵌套拖放列表
这些组件还可用于嵌套拖放。最重要的是为嵌套拖放添加一个类型,以区分是在同一外部类别内拖放,还是在类别之间拖放。
首先,我们将拥有一个类别数组,而不是只有一个项目数组,该数组中的每个对象都将包含项目。
const categories = [
{
id: 'q101',
name: 'Category 1',
items: [
{ id: 'abc', name: 'First' },
{ id: 'def', name: 'Second' },
],
},
{
id: 'wkqx',
name: 'Category 2',
items: [
{ id: 'ghi', name: 'Third' },
{ id: 'jkl', name: 'Fourth' },
],
},
]
handleDragEnd 函数变得更加复杂,因为现在我们需要处理三件事:
- 拖放类别
- 拖放同一类别中的项目
- 将项目拖放到其他类别中
为此,我们将收集源和目标的可放置 ID,这将是类别 ID。然后,要么是简单的重新排序,要么需要将源添加到新目标。在下面的新handleDragEnd函数中,您可以看到处理的所有三种情况:
const handleDragEnd = (result) => {
const { type, source, destination } = result
if (!destination) return
const sourceCategoryId = source.droppableId
const destinationCategoryId = destination.droppableId
// Reordering items
if (type === 'droppable-item') {
// If reordering within the same category
if (sourceCategoryId === destinationCategoryId) {
const updatedOrder = reorder(
categories.find((category) => category.id === sourceCategoryId).items,
source.index,
destination.index
)
const updatedCategories = categories.map((category) =>
category.id !== sourceCategoryId ? category : { ...category, items: updatedOrder }
)
setCategories(updatedCategories)
} else {
// Dragging to a different category
const sourceOrder = categories.find((category) => category.id === sourceCategoryId).items
const destinationOrder = categories.find(
(category) => category.id === destinationCategoryId
).items
const [removed] = sourceOrder.splice(source.index, 1)
destinationOrder.splice(destination.index, 0, removed)
destinationOrder[removed] = sourceOrder[removed]
delete sourceOrder[removed]
const updatedCategories = categories.map((category) =>
category.id === sourceCategoryId
? { ...category, items: sourceOrder }
: category.id === destinationCategoryId
? { ...category, items: destinationOrder }
: category
)
setCategories(updatedCategories)
}
}
// Reordering categories
if (type === 'droppable-category') {
const updatedCategories = reorder(categories, source.index, destination.index)
setCategories(updatedCategories)
}
}
现在,您可以看到该类别具有可放置类别类型,并且该项目具有可放置项目类型,从而区分了它们。我们现在有两层 <Drop> 和 <Drag> 组件。
NestedListComponent.js
import React, { useState } from 'react'
import { DragAndDrop, Drag, Drop } from './drag-and-drop'
import { reorder } from './helpers.js'
export const NestedListComponent = () => {
const [categories, setCategories] = useState([
/* ... */
])
const handleDragEnd = (result) => {
/* ... */
}
return (
<DragAndDrop onDragEnd={handleDragEnd}>
<Drop id="droppable" type="droppable-category">
{categories.map((category, categoryIndex) => {
return (
<Drag key={category.id} id={category.id} index={categoryIndex}>
<div>
<h2>{category.name}</h2>
<Drop key={category.id} id={category.id} type="droppable-item">
{category.items.map((item, index) => {
return (
<Drag key={item.id} id={item.id} index={index}>
<div>{item.name}</div>
</Drag>
)
})}
</Drop>
</div>
</Drag>
)
})}
</Drop>
</DragAndDrop>
)
}
如果没有拖放组件,我什至不会向您展示这是什么样子。
在嵌套拖放演示中,您可以测试在类别之间拖动、在类别内拖动以及拖动类别本身(包括其包含的所有项目)。
总结
拖放可能会变得非常笨拙,尤其是在将 react-beautiful-dnd 库用于嵌套列表时。通过创建可重用的组件,您可以使其更易于使用和理解。
我总是试图看看我的工作在似乎失控时是否可以驯服它,这只是一个例子。希望您喜欢这篇文章和演示!