๐Ÿงช Challenges

Drag and Drop List Reordering

Challenge 3 - Medium

Description

Implement a list where items can be reordered via drag and drop. Manage the state to reflect the new order of items.

Objective

Handle complex state changes and events.

Interactive Example

#0 - Item 1
#1 - Item 2
#2 - Item 3
#3 - Item 4

1. Abstract

In order to solve this problem I've created two components, the DraggableList that is going to handle the state of the items on the list, and the DragComponent that is going to handle the dragging functionality.

TheDraggableListrecieve the items that is going to show and a method to update that list, has the index of the item being drag as part of the state and a function calledupdateListthat handle the re-arrange of the list. This component is going to pass that function to each list element that can be dragged

TheDragComponentis an element that can be dragged and re-arranged. Is going to have a reference to theupdateListfunction and is going to handle the dragging events. At the beginning of the drag is going to set itself at the item being drag and at the end of the drag is going to set it null. This component also has theonDragOverevent that gets executed anytime that some other component get's dragged over this one. In here, we are going to execute the call to theupdateList.

2. Draggable List - Implementation

This component is separated in three parts:

The state

The state of the component is just a reference to the item being dragged.

0 1 2 // The state const [draggingIndex, setDraggingIndex] = useState<number | null>(null);

The update function

The function that is going to handle the update of the list. Basically this function swap the item being drag with the item that is under it, and then saves the new array and upates the index of the item being drag (because it changed in the array)

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // The update list function const updateList = (overIndex: number) => { if(draggingIndex !== null) { const tempItems = [...items]; const draggedItem = items[draggingIndex]; const itemOver = items[overIndex]; // Change the position of the elements tempItems[draggingIndex] = itemOver; tempItems[overIndex] = draggedItem; // Set the new items array setItems([...tempItems]); // Change the item being drag to the new position. setDraggingIndex(overIndex); } };

The render

Finally, the render. This component is going to map the elements of the array to thisDragComponent to have the functionality of being ragged.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // The render return ( <div> { items.map((item: string, index: number) => { return <DragComponent key={`draggable-${index}`} value={item} index={index} draggingIndex={draggingIndex} setDraggingIndex={setDraggingIndex} updateList={updateList} /> }) } </div> );

3. DragComponent - Implementation

This component is going to handle the dragging logic, and it has 3 important methods:

Drag start

ThehandleDragStartis going to set index of the element being dragged (this element). We have to set the event event.dataTransfer.effectAllowed to 'move' indicating that the element is going to be moved to another position

0 1 2 3 4 const handleDragStart = (event: DragEvent<HTMLDivElement>) => { setDraggingIndex(index); event.dataTransfer.effectAllowed = 'move'; };

Drag over

ThehandleDragOveris being called when SOME element is over this one. We want to update the list, saying that this element was under the one being dragged so we pass the index. We have to set the event.dataTransfer.dropEffect to 'move' to indicate that the element is being moved.

0 1 2 3 4 5 6 7 8 const handleDragOver = (event: DragEvent<HTMLDivElement>) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; if(draggingIndex !== null && draggingIndex !== index) { updateList(index); } };

Drag end

ThehandleDragEndis going to set the dragging index to null.

0 1 2 3 const handleDragEnd = () => { setDraggingIndex(null); };

The render

Finally, in the render we need to add thedraggableprop so the component know that needs to execute the drag events..

0 1 2 3 4 5 6 7 8 9 10 return ( <div onDragStart={handleDragStart} onDragOver={handleDragOver} onDragEnd={handleDragEnd} draggable > #{index} - {value} </div> );

Complete Code

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import React, { useState } from 'react'; import DragComponent from './DragComponent'; interface DragableListPropType { items: string[]; setItems: (items: string[]) => void; }; const DragableList: React.FC<DragableListPropType> = ({items, setItems}) => { const [draggingIndex, setDraggingIndex] = useState<number | null>(null); const updateList = (overIndex: number) => { if(draggingIndex !== null) { const tempItems = [...items]; const draggedItem = items[draggingIndex]; const itemOver = items[overIndex]; // Change the position of the elements tempItems[draggingIndex] = itemOver; tempItems[overIndex] = draggedItem; // Set the new items array setItems([...tempItems]); // Change the item being drag to the new position. setDraggingIndex(overIndex); } }; return ( <div> { items.map((item: string, index: number) => { return <DragComponent key={`draggable-${index}`} value={item} index={index} draggingIndex={draggingIndex} setDraggingIndex={setDraggingIndex} updateList={updateList} /> }) } </div> ); }; export default DragableList;
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import React, {DragEvent} from 'react'; interface DragComponentPropType { value: string; index: number; draggingIndex: number | null; setDraggingIndex: (index: number | null) => void; updateList: (index: number) => void; } const DragComponent: React.FC<DragComponentPropType> = ({value, index, draggingIndex, setDraggingIndex, updateList}) => { /* This is going to set index of the element being dragged (this element). We set the event dataTransfer to 'move' indicating that the element is going to be moved to another position; */ const handleDragStart = (event: DragEvent<HTMLDivElement>) => { setDraggingIndex(index); event.dataTransfer.effectAllowed = 'move'; }; /* This functions is being called when SOME elemnet is over this one. We want to update the list, saying that this element was under the one being dragged so we pass the index. We set the dropEffect to move to indicate that the element is being moved. */ const handleDragOver = (event: DragEvent<HTMLDivElement>) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; if(draggingIndex !== null && draggingIndex !== index) { updateList(index); } }; /* At the end of the drag, we say that this element is not being drag anymore. */ const handleDragEnd = () => { setDraggingIndex(null); }; return ( <div onDragStart={handleDragStart} onDragOver={handleDragOver} onDragEnd={handleDragEnd} draggable > #{index} - {value} </div> ); }; export default DragComponent;
๐Ÿ›๏ธ ๐Ÿงฎ ๐Ÿ“ โš›๏ธ ๐Ÿงช ๐Ÿ