用react-beautiful-dnd添加拖放功能

1,708 阅读9分钟

如果你曾经使用过Jira、Trello、Confluence或任何其他Atlassian产品,你很可能遇到过拖放功能,使用户可以在多个(有时是巨大的)列表中拖动项目。这是一个非常有用的功能,似乎总是能顺利地工作,但将这一功能构建到一个应用程序中可能是一个挑战。

什么是react-beautiful-dnd?

react-beautiful-dnd是Atlassian的开源库,它允许网络开发者轻松地将拖放功能集成到他们的应用程序中。 react-beautiful-dnd是目前React世界中最受欢迎的拖放库,如果你想在你的网站上实现拖放功能,它是一个不错的选择。

首先,让我们看看react-beautiful-dnd的一些重要特性,看看是什么让它如此受欢迎。

  1. 它是高度可定制的。该库尽量做到无头,这意味着只有围绕动画的一些基本样式。它们是可以调整的,并且可以由你自己的实现来代替。你甚至可以实现你自己的传感器,用鼠标以外的其他输入源(如键盘)来控制拖放列表(没有限制,如例子所示)。
  2. 不创建额外的DOM节点,这意味着构建布局是简单和可预测的,确保你可以轻松设计你的拖放项目和列表
  3. 伴随着第一点,react-beautiful-dnd提供了广泛的选项和元数据,允许你用它创建无尽的组合和不同的用例

查看官方文档中所有功能的综合清单。

使用 react-beautiful-dnd 组件实现拖放功能

该库已深度集成到React生态系统中,这意味着所有功能都是围绕关键的React组件构建和控制的。

[DragDropContext](https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/drag-drop-context.md)

这是包含有关您的拖放列表的所有信息的上下文。它基于React上下文,所以你应该用这个元素包裹所有与拖放有关的东西,使其正常工作。此外,它接受onDrag 函数,这是控制你的列表数据的关键回调。我们将在后面进行详细介绍。

[Droppable](https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md)

这个元素包含你的列表和投放区,当元素被投放时,它将成为源头。它必须用droppableId ,而且必须包裹在一个返回React元素的函数中。这个函数被调用时有一些内部参数和一个snapshot 参数,其中包含关于列表状态的信息。这可以用于样式设计或围绕这个元素的事件的回调。

[Draggable](https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/draggable.md)

这是一个所有列表元素的容器。你必须用这个元素来包装你的每一个列表项。与Droppable 元素类似,Children 元素是一个函数,用Snapshot 属性调用。

为了演示这些元素是如何使用的,我将向你展示一些例子。我们将创建两个基本的例子来演示该库的主要功能。第一个将是简单的,将展示这些元素是如何一起工作的。第二个将更高级,包含多个列表,类似于Trello板,以展示这个库的功能。

用 react-beautiful-dnd 构建一个拖放式列表

数据结构

首先,我们需要一些数据来进行演示。

interface DataElement {
    id: string;
    content: string;
}

type List = DataElement[]

在这个结构中,我们要将一个DataElements 的列表可视化,并使列表中的元素能够被拖放。

设置拖放元素

让我们首先创建我们需要的元素,让我们的列表准备好拖放。正如我们上面所学到的,第一步是创建一个DragDropContext ,将我们想要的东西都包含在我们的列表中。

在其中,我们创建一个可拖放的容器并映射到我们的DataElements ,然后为每个元素渲染一个Draggable 。每个元素渲染的内容由你决定。为了简单起见,我们将渲染一个准备好的ListElement 组件,该组件在内部只采用元素的样式,使其漂亮,但与文章没有关系。

function DragAndDropList() {
    return (
        <DragDropContext>  
            <Droppable droppableId="droppable" >  
                {(provided, snapshot) => (  
                    <div  
                        {...provided.droppableProps}  
                        ref={provided.innerRef}  
                    >  
                        {elements.map((item, index) => (  
                            <Draggable draggableId={item.id} index={index}>  
                                {(provided, snapshot) => (  
                                   <div  
                                     ref={provided.innerRef}  
                                     {...provided.draggableProps}  
                                     {...provided.dragHandleProps}  
                                   >  
                                    <ListItem item={item} />
                                   </div>  
                                )}  
                            </Draggable>  
                        ))}  
                    </div>  
                )}  
            </Droppable>  
        </DragDropContext>
    )
}

为了这个例子的目的,我们也将省略列表的样式,但这基本上是将你的数据显示在一个列表中,一个在另一个下面。使用react-beautiful-dnd的惊人之处在于,仅仅通过编写这段代码,就已经可以抓取元素。列表和元素也能正确地动画化。

然而,正如你在下面的例子中所看到的,如果你把元素移动到不同的位置,元素会短暂地结束在正确的位置,但列表会跳回到之前的状态,元素会在原来的位置。

Card Elements Jumping Back To Original Spot After Being Moved To A Different Spot

原因是底层数据数组中元素的顺序保持不变,因为react-beautiful-dnd并没有操作你的数据,也不应该负责这样做。相反,react-beautiful-dnd将只对状态进行动画处理,并给你适当的回调来处理你的数据。

我们的工作是在正确的时间把数据放在正确的表格中。所以,现在我们需要在项目被丢弃时设置一个钩子,并正确处理我们的数据。

首先,让我们把我们的数据放在React状态下,使其更容易改变和操作。

function DragAndDropList() {
    const [items, setItems] = useState(baseData)
    return (...)
}

为了钩住拖放动画的生命周期,我们可以在我们的DragDropContext ,添加一个onDragEnd 函数。

<DragDropContext onDragEnd={onDragEnd}>

现在我们需要在我们的组件内实现这个函数,以便在这个函数被调用时以正确的方式塑造我们的状态。为了获得我们需要的所有信息,该函数接收以下结果对象作为参数。这个对象有以下的结构和属性。

interface DraggableLocation {
    droppableId: string;
    index: number;
}

interface Combine {
    draggableId: string;
    droppableId: string;
}

interface DragResult {
    reason: 'DROP' | 'CANCEL';
    destination?: DraggableLocation;
    source: DraggableLocation;
    combine?: Combine;
    mode: 'FLUID' | 'SNAP';
    draggableId: DraggableId;
}

这个结果对象包含了所有关于我们需要如何操作我们的数据的信息,以便它不会被扣回到之前的位置。

两个属性,destinationsource ,是这里最重要的。顾名思义,source 包含了元素之前的位置信息,destination 包含了元素被丢弃的位置。

这两个属性都包含droppableIdindex ,我们需要这两个属性来操作我们的数据阵列。现在我们有了我们需要的一切,让我们写下我们要改变数据数组的逻辑。

function DragAndDropList() {
    const [items, setItems] = useState(baseData)

    function onDragEnd(result) {
        const newItems = [...items];
        const [removed] = newItems.splice(result.source.index, 1);
        newItems.splice(result.destination.index, 0, removed);
        setItems(newItems)
    }

    return (...)
}

在这里,我们用source.indexdestination.index 来改变我们数据的正确位置。

一般来说,这就是我们需要的全部,以拥有一个功能齐全的拖放列表。但在我们继续之前,让我们添加一个小功能,使其不那么容易出错。正如你在定义中看到的,destination 属性可以是undefined 。这将意味着该元素没有被投放到droppable 元素中。

只需添加这几行代码来防止这种行为。

function onDragEnd(result) {
    if (!result.destination) {
        return;
    }
    const newItems = [...items];
    const [removed] = newItems.splice(result.source.index, 1);
    newItems.splice(result.destination.index, 0, removed);
    setItems(newItems)
}

这就是了!下面是整个流程的操作。

用react-beautiful-dnd构建多个拖放列表

现在我们已经看到了基本功能,让我们看看一个稍微高级的--但更经常使用的--用例。

也许你有多个列表,不仅想在每个列表内拖放项目,还想在列表之间拖放项目,就像你在看板上一样。通过react-beautiful-dnd,你可以做到这一点。让我们首先渲染多个列表,并使项目从一个列表拖到另一个列表成为可能。

与第一个例子类似,我们需要用DragDropContext 来包装一切。此外,我们需要创建一个常量,包含我们想要的列表的信息,并对其进行迭代,以呈现所有需要的lists 元素。

const lists = ['todo', 'inProgress', "done"];

function DragList() {  
    return (  
        <DragDropContext>  
            <ListGrid>
                {lists.map((listKey) => (  
                    <List elements={elements[listKey]} key={listKey} prefix={listKey} />  
                ))}
            </ListGrid>  
        </DragDropContext>
    );  
}

我们为数组中的每个元素返回的list 元素几乎与上面的例子相同,唯一的例外是它不包含数据的状态或操作它的功能。

所以,我们只使用列表的渲染部分,它看起来会是这样的。

const DraggableElement = ({ prefix, elements }) => (  
    <Droppable droppableId={`${prefix}`}>  
        {(provided) => (  
            <div  
                {...provided.droppableProps}  
                ref={provided.innerRef}  
            \>  
                {elements.map((item, index) => (  
                        <ListItem key={item.id} item={item} index={index}/>  
                ))}  
                {provided.placeholder}
            </div>  
        )}  
    </Droppable>  
 )

给每个listDroppable 元素一个唯一的属性ID,称为droppableId ,这一点很关键。有了这个ID,我们以后就可以识别这个元素来自哪个列表,以及它被丢到哪个列表。

管理状态

棘手的部分是决定我们要在哪里以及如何保持我们的状态。因为我们可以有从一个列表到另一个列表的项目,我们需要把状态移到容纳所有列表的包装组件中。让我们从在DragList 组件中创建一个状态开始,它将看起来像这样。

const data = {
    todo: [
        {
            id: '91583f67-0617-4df2-bd74-3c018460da6c'
            listId: 'todo',
            content: 'random content'
        }, 
        ...

    ]: 
    inProgress: [...]
    done: [...]
}

理想情况下,我们可能会使用reducer 函数来处理这个状态,但为了使例子简短,我们将使用useState 钩子。注意,我们有一个大的对象,包含了我们所有列表的数据。正如我们在单个拖放列表的例子中所做的那样,我们也创建了函数onDrag

const removeFromList = (list, index) => {  
    const result = Array.from(list);  
    const [removed] = result.splice(index, 1);  
    return [removed, result]  
}  

const addToList = (list, index, element) => {  
    const result = Array.from(list);  
    result.splice(index, 0, element);  
    return result  
}

const onDragEnd = (result) => {  
    if (!result.destination) {  
        return;  
    }  
    const listCopy = { ...elements }  

    const sourceList = listCopy[result.source.droppableId]  
    const [removedElement, newSourceList] = removeFromList(sourceList, result.source.index)  
    listCopy[result.source.droppableId] = newSourceList  

    const destinationList = listCopy[result.destination.droppableId\]  
    listCopy[result.destination.droppableId] = addToList(destinationList, result.destination.index, removedElement)  

    setElements(listCopy)  
}

正如你所看到的,代码本身看起来与单个列表的例子很相似,但我们现在必须考虑用destinationsource 属性识别的各自列表。

幸运的是,result 对象总是通过draggableId 属性提供这一信息,现在这对于了解元素来自哪个列表以及它们接下来要去哪里相当有用。现在你也可以在列表之间拖放项目了。

下面是完整的例子。

总结

拖放是一个广泛使用的功能,可以使你的应用程序变得强大。react-beautiful-dnd 库提供了将拖放功能纳入你的应用程序所需的所有功能。它很灵活,没有主见,而且动画效果很好。

就像开源编程中的许多事情一样,大多数问题都可以通过使用库为(以及由!)开发者准确解决。很高兴看到像Atlassian这样的公司将他们的解决方案提供给世界其他地方,这样网络开发者就不必不断地从头开始构建功能。

The postAdding drag-and-drop functionality with react-beautiful-dndappeared first onLogRocket Blog.