Foreword.
In this series of articles, we will consider the task of developing and testing sortable Drag-and-Drop components. There are a lot of scenarios for using drag-and-drop behavior, here are a few of them:
- Uploading or removing files and images (banalest).
- Sortable tables.
- Sortable notes or stickers.
- Reorderable tabs. Look at your browser's opened tabs, you can reorder them with the drag-and-drop behavior.
- Captchas validation (assembling the puzzle).
- Games (chess and checkers).
In the first part of the article, we will create a small semblance of a restaurant in the style of the cult sitcom ALF, with the functionality of drag-and-drop sorting of dishes between visitors' tables. You can play with the working online demo by the following link Demo.
Let's imagine that in our restaurant, a newbie junior waiter mixed up the orders of visitors and placed all dishes incorrectly, and our task is to put everything in its place. To do this, we need to drag the desired dish and drop it on the appropriate table. Let's help our waiter and put things in order here.
Technologies and libraries used:
Project Structure.
The structure is as simple and clear as possible, with abstract naming to not stick to some particular use case. All data used is mocked, so as not to be distracted by the unnecessary implementation. Styles are used to make things more visually pleasing, but they don't affect the project in any way, so don’t focus on them. Some examples will omit optional code parts with appropriate comments:
// Omitted pieces of code.
Drag-and-Drop Provider.
To enable the drag-and-drop functionality in our application, we have to wrap the desired component in the DnD Provider along with the backend
prop.
File: src/components/Container/Container.tsx
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { DropBoxContainer } from "../DnD/DropBox/DropBoxContainer";
// Omitted pieces of code.
<DndProvider backend={HTML5Backend}>
<DropBoxContainer selectionsData={MOCK_DATA} />
</DndProvider>
// Omitted pieces of code.
Drop Box container.
The next step is to create the DropBoxContainer
. It is responsible for keeping the data in the correct order, the sorting functionality, and displaying the DropBox
components. In a real case, data storage can be taken to a higher level, like React context or Redux store, as well as sorting functionality, which can be placed into separate utils files, but for the sake of the demo, it will be pretty enough.
Inside this file, we will iterate over all of our data (visitor's food tables in our case) and render each item as a separate DropBox
component.
File: src/components/DnD/DropBox/DropBoxContainer.tsx
// Omitted pieces of code.
<div className={styles.container}>
{selections.map((item, index) => {
return (
<div className={styles.itemContainer}>
<DropBox
key={index}
index={index}
selection={selections[index]}
updateSelectionsOrder={updateSelectionsOrder}
/>
<Table />
</div>
);
})}
</div>
// Omitted pieces of code.
Drop Box item.
Go ahead and create the DropBox
file that will act as a separate container/box for each draggable file. For more clear understanding, I will try to explain in simple words using the example of our application:
-
File: src/components/DnD/DropBox/DropBoxContainer.tsx
- It’s like a food court or food hall, the place where all visitors sit at their tables. It’s some kind of container where all things happen. -
File: src/components/DnD/DropBox/DropBox.tsx
- This is a specific table at which the visitor and their food are placed. It's a place where draggable items are dropped. -
File: src/components/DnD/DragBox/DragBox.tsx
- This is the plate of the food itself, or in other words, it’s a draggable item.
DropBox
component accepts several props:
-
index
- that we are taking from the map() function. -
selection
- it’s a data item, which in our case is a food dish. -
updateSelectionsOrder
- function to update items order.
And here comes the first "magic" associated with the drop behavior.
File: src/components/DnD/DropBox/DropBox.tsx
// Omitted pieces of code.
const [isHovered, setIsHovered] = useState(false);
const [_, drop] = useDrop({
accept: [DragTypes.Card],
drop(item: DragItem) {
updateSelectionsOrder(item.index, index);
},
collect: (monitor) => {
if (monitor.isOver()) {
setIsHovered(true);
} else {
setIsHovered(false);
}
},
});
// Omitted pieces of code.
As the documentation says about the useDrop hook:
The useDrophook provides a way for you to wire in your component into the DnD system as a drop target. By passing in a specification into the useDrophook, you can specify including what types of data items the drop-target will accept, what props to collect, and more. This function returns an array containing a ref to attach to the Drop Target node and the collected props.
Let’s take a closer look piece by piece:
accept
: Required. A string, a symbol, or an array of either. Specifies a type to which the drop target will react, it will only react to the items produced by the drag sources only of the specified type or types. Simply put, if you'll try to drag an item over the drop target that doesn't have a type specified in the "accept" property, the drop target will not react to it and will ignore it. It prevents interaction with unwanted and unspecified items.
drop(item, monitor)
: Optional. Called when a compatible drag item is dropped on the drop target. In our case, when we drag a dish and drop it on some table, this function is called, and we run the provided updateSelectionsOrder
callback, to update the arrangement of dishes.
collect
: Optional. The collecting function. It should return a plain object of the props to return for injection into your component. It receives two parameters, monitor and props. We use it with the DropTargetMonitor, to receive information on whether the drag operation is in progress. It helps to apply some visual effects when we hover a dish over the table and draw a dashed border around items.
To designate a drop target, we're attaching the drop
returned value from the useDrop hook to the drop-target portion of the DOM.
File: src/components/DnD/DropBox/DropBox.tsx
// Omitted pieces of code.
return (
<div
className={clsx(styles.dropContainer, {
[styles.hovered]: isHovered,
})}
ref={drop}
>
<DragBox dragItem={selection} index={index} />
</div>
);
// Omitted pieces of code.
Drag Box item.
Finally, we reached our target and most delicious items (according to the ALF's taste). Let’s consider our DragBox
dish. It receives two props:
dragItem
: Data of the draggable item. For this application, it was simplified to only "id", "name", and "icon" properties.
index
: Index of the draggable item, that will be used to operate with the drag behavior that we will explore further.
File: src/components/DnD/DragBox/DragBox.tsx
// Omitted pieces of code.
const [{ isDragging }, drag] = useDrag({
type: DragTypes.Card,
item: { type: DragTypes.Card, id, index, name, icon },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
// Omitted pieces of code.
To wire our component as a drag source, we use the useDrag hook. Here we specify the type
that we discussed above in the useDrop hook, item
- draggable item data that will be passed to the drop
function of the useDrop hook in the DropBox
component, and collect
function to receive info whether our item is dragging, for visual effects.
To designate a drag item, we're attaching the drag
returned value from the useDrag hook to the draggable portion of the DOM.
File: src/components/DnD/DragBox/DragBox.tsx
// Omitted pieces of code.
return (
<div
className={clsx(styles.container, {
[styles.dragging]: isDragging,
})}
ref={drag}
>
<img src={image} className={styles.icon} />
<p className={styles.name}>{name}</p>
</div>
);
// Omitted pieces of code.
Conclusion.
Finally, we have a working version of the application with sortable draggable components, which, with the necessary adaptation, will surely find its use in the real world. In this example, we have considered only a small part of the capabilities of this library, which will be enough to start creating your own implementation. For a deeper understanding, check out the documentation, which also has a great "Examples" section where you can find useful usage examples. In the next article, we will develop unit tests for these components and cover the main use cases.
Top comments (0)