Let's Learn HTML Drag and Drop API
In this article we'll learn all about the Drag and Drop API in modern browsers
By: Ajdin Imsirovic 16 June 2021
In this post we’ll explore the Drag and Drop API in modern browsers until we get comfortable using this functionality.
Understanding HTML Drag and Drop API
Today, in many web apps, the following functionality is almost the expected behavior: a user is “spoiled” to intuitively know that if they click and drag an element, then drop it onto another element, it will probably work (if it makes sense to perform such an action).
A simple, useful example, is building a quiz interface, where we would need to order separate words onto a section of the screen, and we would need to put those words in order - so as to get a passing score.
In this section, we’ll list some basic examples of the Drag and Drop API, as well as some basic terminology.
This API is all about two groups of user-triggered events:
- the event of dragging a source element
- the event of dropping onto a target element
The first group of user-tiggered events, i.e “the source events”, are dragstart
, drag
, and dragend
.
The second group of user-triggered events, i.e “the target events”, are dragenter
, dragover
, dragleave
, and drop
.
Let’s go through a possible drag and drop sequence of events:
- The user clicks the element, holds the click, and starts moving the element on the page
- The dragged (source) element enters the coordinates of another element.
- The source element is dragged over that “another” element.
- The source element leaves the area of the “another” element.
- The source element enters the coordinates of a third (target) element.
- The source element is over the target element and the user releases the left mouse button.
We can “translate” the above scenario into the sequence of events triggered:
dragstart
(emitted from the source element), immediately followed bydrag
(from the source element). Thedrag
event is triggered all the time, as long as the user is moving the element - similar tomousemove
dragenter
(emitted from the target element)dragover
(from target)dragleave
(from target)dragenter
(from another target)drop
(from another target) anddragend
(from source element)
Drag and drop event handlers
Although the above-listed events are fired, nothing “perceptable as a real interaction” happens for the user dragging the source element onto the target element - unless we use the Drag and drop API’s event handlers.
Here are the available event handlers:
ondrag
ondragend
ondragenter
ondragexit
ondragleave
ondragover
ondragstart
ondrop
Due to such a straightforward naming convention, it’s easy to connect the event handlers with their respective events on this API.
Making an element draggable (converting a plain HTML element to a source element)
To make an element into a source element, we need to set its draggable
property to true
, like this:
1
2
3
<span id="drag-it" draggable="true">
Drag this source element around!
</span>
Let’s also style it a bit:
1
2
3
4
5
#drag-it {
background: yellowgreen;
padding: 10px 20px;
display: inline-block;
}
Adding the target element
Let’s add another element: this is our target element.
1
2
3
<div id="drop-it">
Drop the source element to this target element!
</div>
And let’s add its corresponding CSS:
1
2
3
4
5
6
7
8
9
10
#drop-it {
background: lightgray;
padding: 10px 20px;
display: inline-block;
position: absolute;
bottom: 0;
left: 0;
height: 50%;
width: 50%;
}
If we try click-dragging the source element at this point, it will work - since we’ve made it draggable. But other than that, right now it doesn’t do much else. It’s time to hook into those events.
Hooking into drag and drop events with JS
Let’s define a function that will run on the source element.
function dragStarted(event) {
console.log(event);
}
It’s as simple as it can get.
Now let’s listen for it in an HTML attribute:
<span
id="drag-it"
draggable="true"
ondragstart="dragStarted(event)"
>
Drag this source element around!
</span>
We’ve updated the HTML attributes on our source element; specifically, we’ve added an ondragstart
event handler attribute. Then we passed it a value: the name of the function to run, with the built-in event
parameter passed to the function call.
Testing this with the browser console open, we should see something like the following output:
// [object DragEvent]
{
"isTrusted": true
}
Great, we have just verified our code will run when a dragstart
event fires. Let’s now do something more “constructive”:
function dragStarted(evt) {
console.log(evt);
event.currentTarget.style.backgroundColor = "rgba(0,200,100, 0.25)";
}
Now, if a user clicks and drags the source element, it will change the element’s color to rgba(0,200,100,0.25)
as the result of handling the dragstart
event.
This “takes care” of the dragstart
event. Let’s now deal with the dragover
event.
First, we’ll add the event handler as an HTML attribute:
<div
id="drop-it"
draggable="true"
ondragover="allowDrop(event)"
>
Drag the source div onto this target element!
</div>
Next, we’ll define the event handler function that was passed in to the ondragover
attribute:
function allowDrop(evt) {
console.log(event);
}
If we tested the current code by dragging the source element over to the target element, we’ll see the expected output:
// [object DragEvent]
{
"isTrusted": true
}
Now that we’ve set this up, we need to, again, do something “more constructive” in our event handler. This time, we need to prevent the default behavior, because for some elements, browsers do not allow dropping source onto target. This built-in browser functionality will disallow what we’re attempting to do in this exercise. To “switch off” this default browser functionality, we’ll use the preventDefault()
function call:
function allowDrop(evt) {
console.log(event);
event.preventDefault();
}
This has now prepared us for handling the next event, the drop
event.
Again, we’ll begin by adding the ondrop
attribute, and assigning it a value of the event handler function call:
<div
id="drop-it"
draggable="true"
ondragover="draggedOver(event)"
ondrop="dropped(event)"
>
Drag the source div onto this target div!
</div>
Now let’s define our dropped()
function:
function dropped(evt) {
console.log("dropped");
}
This time, we’re simply logging out the word “dropped” so as to differentiate this event handler from the other console.log(event)
pieces of code, as they all just return // [object DragEvent]
etc.
Now, we’ll again update the definition of the dropped()
function to something more useful.
Let’s begin by stopping event propagation, so that the event does not bubble up the DOM tree. We’re also preventing default browser behavior, to make sure we’ve reset any weird operations.
function dropped(evt) {
console.log("dropped");
event.stopPropagation();
event.preventDefault();
}
Next, we’ll need to actually drop the source to target. We’ll do this with the help of the dataTransfer
object.
Drag and drop data with the dataTransfer
object and drop effects
In the context of the Drag and Drop API, the data transfer object is a property on the specific Event
object.
If we say that we’re working with an instance of the Event
object, called evt
, then the data transfer object can be accessed like this:
evt.dataTransfer
Let’s look at a drag-and-drop operation from a different angle.
When we drag an element and look for a place to drop it on the screen, what is it that we are actually doing?
We’re working with data, specifically, with the setData()
method on the dataTransfer
object.
In other words, in a drag-and-drop operation, a user triggers drag and drop events.
We write the code to handle such events using event handlers that we define ourselves.
We pass the specific Event
object’s instance to those event handlers, and then access the dataTransfer
object (a property on a given Event
object’s instance), and then access the dataTransfer
object’s setData()
method.
The setData()
method accepts two parameters:
- the MIME type of the data passed in
- the data itself
The first parameter is there to give some constraints on how the passed-in data should look. It’s almost like a validation parameter. If we specify the MIME type as text
in the first parameter of setData()
, then that MIME type is what is expected to be passed in as the second parameter.
The data stored in the dataTransfer
object also allows us to take care of the so-called drop effects - visual cues for the user on what is about to happen if they complete the drag-and-drop operation.
The drop effect has four distinct varieties:
- copy
- link
- move
- none
The first one copies the source element into the target element.
The second one adds a link to the source element, from the target element.
The third one moves the source element into the target element.
The fourth one doesn’t allow the drop operation to complete.
So, in a nutshell, what we want to do in the next step is to “scoop up” the source element’s data when the drag and drop action begins (i.e in the dragstart
event). After we’ve set this data using setData()
, we next read it when the drop
event fires.
Thus, let’s first extend the definition of our dragStarted()
event handler, like this:
function dragStarted(evt) {
evt.dataTransfer.setData("text", evt.target.id);
evt.currentTarget.style.backgroundColor = "rgba(0,200,100, 0.25)";
}
Basically, we’re acessing the given Event
object’s instance, called evt
in the above function declaration.
Next, we’re accessing the dataTransfer
object, and then accessing its setData
method. All in all, we’re doing this:
evt.dataTransfer.setData()
Next, as already described above, we’re setting the MIME type to text
, and we’re setting the data source to be evt.target.id
.
Let’s try logging this out, to see what comes up:
function dragStarted(evt) {
console.log("We're setting this data:", evt.target.id);
evt.dataTransfer.setData("text", evt.target.id);
evt.currentTarget.style.backgroundColor = "rgba(0,200,100, 0.25)";
}
Here’s the output in the console based on the above improvement:
"We're setting this data:" "drag-it"
We’ll now need to “receive” this data in the drop
event handler, i.e in the dropped()
function definition, which we’ll now update to this:
function dropped(evt) {
evt.preventDefault();
const data = evt.dataTransfer.getData("text");
evt.target.appendChild(document.getElementById(data));
}
We’re overriding the built-in browser behavior with preventDefault()
and then we’re getting the data from the dataTransfer
object. Finally, we’re appending this data onto the target element.
Let’s add another console.log to inspect what the gotten data looks like:
function dropped(evt) {
evt.preventDefault();
const data = evt.dataTransfer.getData("text");
console.log(data);
evt.target.appendChild(document.getElementById(data));
}
The output of the console.log improvement now is this:
"drag-it"
Finally, let’s wrap it up with an overview of the complete code, with a second draggable element added:
Here’s the HTML:
<span
id="drag-it"
draggable="true"
ondragstart="dragStarted(event)"
>
Drag this source element around!
</span>
<span
id="drag-it-2"
draggable="true"
ondragstart="dragStarted(event)"
>
Fruit
</span>
<div
id="drop-it"
ondrop="dropped(event)"
ondragover="allowDrop(event)"
>
Drop the source element onto this target element!
</div>
Here’s the CSS:
#drop-it {
width: 768px;
height: 350px;
padding: 10px;
border: 1px solid #aaaaaa;
position: absolute;
top: 100px;
}
#drag-it, #drag-it-2 {
background: yellowgreen;
padding: 10px 20px;
display: inline-block;
font-size: 35px;
line-height: 50px;
display: inline-block;
margin: 20px;
}
And here’s the JS:
function allowDrop(evt) {
evt.preventDefault();
evt.currentTarget.style.backgroundColor = "rgba(200,200,200, 1)";
}
function dragStarted(evt) {
console.log("We're setting this data:", evt.target.id);
evt.dataTransfer.setData("text", evt.target.id);
evt.currentTarget.style.backgroundColor = "rgba(0,200,100, 0.25)";
}
function dropped(evt) {
evt.preventDefault();
const data = evt.dataTransfer.getData("text");
console.log(data);
evt.target.appendChild(document.getElementById(data));
}
This code is also available online under a codepen titled Simplest drag and drop.
Note:
This exercise comes from Book 5
of my book series on JS, available on Leanpub.com.