We all used those Kanban-style todo or task management apps where there are columns for each stages of a tasks and we can drag and drop tasks from one column to another.
Implementing this drag and drop feature in react is way easier than I thought. Everything you need is already there without using any library or package. So let's get started. ๐
*** ( Typescript is used but its not noticable in code examples JS users can easily understand and the CSS styles are omited in the code examples. Get to the github link to see the full code. )
The idea ๐ก
The main idea behind it is pretty simple. We are going to use default HTML Drag and Drop API that uses various drag and drop events. To make any HTML element draggable we just need to add draggable attribute to an element. Then we will maintain a state where will store data for all the dropped elements and then show them in the div. To achieve this will use various drag and drop even listeners already available with HTML5 and React.
๐ฏ The main trick is here is that we can not move an element/component literally. We will pass its data with it when we drag and then recreate the element/component again from the passed data inside the div we are dropping it in.
We will use events listeners like :
- OnDrop : This event activates when a valid draggable element is dropped in its area.
- OnDragStart : This is fired when dragging of an element is started.
- OnDragEnd : This is fired when dragging is ended.
- OnDragOver : This is fired when we drag over the area of an element
There are many more but for our use these are enough. So lets start the implementation.
The implementation ๐งโ๐ป
Let's create some task elements in a div from where we will drag them from :
and a todo div where we will drop them :
Now to make the task elements draggable we just add the draggable attribute.
<div
className="..."
draggable
>
Task 1
</div>
Now we can drag the task div. When we drag an element we need to pass its data with it so that we can recreate it. In this we case will pass its name.
To do this lets use onDragStart
which fires when we start dragging an element.
<div
className="..."
draggable
onDragStart={(e) => {
handleOnDrag(e, "Task 1");
}}
>
Task 1
</div>
Here we are passing the event e and the name of the task to the handleOnDrag
function.
Lets write the handleOnDrag
function
function handleOnDrag(e: React.DragEvent, name: string) {
e.dataTransfer.setData("name", name);
}
Here we are setting the name string as data under "name"
key. Now whenever we drag it the name data will go along with it.
We handled the dragging part now lets drop it.
We need a state where we will store data for all the dropped tasks and later show them in the div where we dropped them.
const [tasks, setTasks] = useState<string[]>()
Now we will use onDrop
attribute to signal the dropping event. But before doing this we have to handle onDragOver
for an edge case. Just do this :
<div
className="..."
onDragOver={handleOnDragOver}
>
{/* The div we are dropping the task in */}
</div>
And now the handleOnDragOver
function
function handleOnDragOver(e: React.DragEvent) {
e.preventDefault();
}
This will stop a default behavior where the onDragOver keeps firing idefinately.
Now for the onDrop
<div
className="..."
onDragOver={handleOnDragOver}
onDrop={handleOnDrop}
>
{/* The div we are dropping the task in */}
</div>
Now lets write the handleOnDrop
function
function handleOnDrop(e: React.DragEvent) {
if(tasks) {
setTasks([...tasks,e.dataTransfer.getData("name")] )
} else {
setTasks([e.dataTransfer.getData("name")])
}
}
Here we are creating a new array with the dropped task div's data and setting that array in the tasks
state. The e.dataTransfer.getData("name")
part will look familiar. Because we used e.dataTransfer.setData("name", name)
in the handleOnDrag
function to name the dragged task. And now we are getting that name data that we set previously when we started dragging it.
Now we have the data for the task that we are dropping. We now just have to show the task in the div using the data we got. We will use the same Task div we created earlier, inside the div we dropping it in. We will map through all the tasks in the tasks
state and show them one by one.
<div
className="..."
onDragOver={handleOnDragOver}
onDrop={handleOnDrop}
>
{tasks &&
tasks.map((taskName) => {
return (
<div
className="..."
draggable
onDragStart={(e) => {
handleOnDrag(e, taskName);
}}
>
{taskName}
</div>
);
})}
</div>
Finally we have the dragged task in the div we dropped it in.
But there is a catch. The Task is getting duplicated every time we drag and drop it the task again in the div.
To solve we have to delete the previous instance of the task in the state when we move it. Its pretty easy to do with filter()
function handleOnDrop(e: React.DragEvent) {
if(tasks) {
setTasks([
...tasks.filter(
(taskName) => taskName !== e.dataTransfer.getData("name")
),
e.dataTransfer.getData("name"),
]);
} else {
setTasks([e.dataTransfer.getData("name")])
}
}
Now everything is working. We can drag tasks from the list and drop them in the Todo div.
How to extend the idea further โก๏ธ
The basic stuff is set up. Now we can make it more user friendly by giving visual feedback when we start dragging the task and when we drop it. For that we can use onDragStart
, onDragEnd
, onDragOver
etc. events to change the CSS styles of the elements. We can also add animations.
In part 2, we we will do all of these and also add two more columns for ongoing and completed tasks and make a fully functioning Kanban board-like todo board. We will need to manage three separate states and many more complexities. Part 2 coming soon โ
Part 2 is released๐ฅ
Check it out here
Link to full code : Github Link
I hope this was helpful to you ๐
Follow me on Twitter and LinkedIn
Thanks for reading ๐
Top comments (17)
Great article, thanks! I'm wondering though what is the current browser support for this API? The MDN docs don't say anything abou that.
Edit: it looks like according to caniuse.com, the support is mostly good except on some less popular mobile browsers.
All up to date modern desktop browsers I tested supports it.
But I found than in mobile browsers, it doesn't work as expected with the same code. I will have to look into it. Thanks for the heads up.
Great article! I followed it but using Vue. Here's the repo if anyone is interested: repo.
Excellent work friend ๐
Great stuff, did the same way back some months ago but then found that it doesn't work on mobile devices. So have to rely on other js libraries.
Yes I faced problem in mobile devices too. I think there is some way to fix it. I am working on it.
Other js libraries too had to manually implement it didn't they ? ๐
Great Article Hassan, But if you show live demo with code-pen embedding it would be more helpful.
Thanks for the suggestion. I will try to do that from next time. ๐
Great article. One minor note that your function name was missing "On"
function handleDragOver(e: React.DragEvent)
Thanks for pointing it out :D
Nice article Ahmed, it was a good read. I want to ask, how did you make a gif video in this your article?, I am trying to do the same, but don't know how
Thanks. I just recorded part of the screen with windows' snipping tool and then converted that video to gif using this
Hope it helps :)
Okay thank you very much
Good sharing.
Great article ...
Thank you
Awesome