For some time, it was necessary to write a lot of JavaScript code in order to implement the famous drag 'n drop feature in a web application.
Fortunately, in January of 2008, W3C released the fifth version of HTML which provides the scripting Drag and Drop API that can be used with JavaScript.
TL;DR
In this article you're going to learn how to implement a few reusable components in order to add drag and drop capabilities to your next VueJS Project.
The whole sample code available in this article is based on VueJS 3.
It's important to mention that you may find several third-party libraries that implement drag and drop features. That's fine and you will probably save time by using them.
The goal here is just to practice a little bit of VueJS, see how HTML 5 Drag and Drop API works and also create your own reusable and lightweight components without the need of any external dependency.
If you still don't know how to create a VueJS project from scratch, I recommend you to take a look at this article through which I explain how I structure my own VueJS projects from scratch.
Create a new VueJS Project and let's get hands dirty!
Droppable Item
We're going to start by creating a simple component that will allow other elements to be dragged into it.
We're going to call it DroppableItem
and it will look like this:
<template>
<span
@dragover="handleOnDragOver"
@dragleave="onDragLeave"
@drop="onDrop"
>
<slot />
</span>
</template>
<script>
export default {
name: 'DroppableItem',
props: [
'onDragOver',
'onDragLeave',
'onDrop'
],
setup(props) {
const handleOnDragOver = event => {
event.preventDefault()
props.onDragOver && props.onDragOver(event)
}
return { handleOnDragOver }
}
}
</script>
Let's dive deeper into each part of this implementation.
The template
is very simple. It is made of a unique span
element with a slot
inside it.
We're going to add some event listeners to this very root element, which are:
@dragover
: triggered when dragging an element over it;@dragleave
: triggered when dragging an element out of it;@drop
: triggered when dropping an element into it;
Even though it's not a good practice, we're not defining the prop types in this example just to keep it simple.
Notice that we wrap the onDragOver
event within a handleDragOver
method. We do this to implement the preventDefault()
method and make the component capable of having something dragged over it.
We are also making use of a slot
to allow this component to receive HTML content and "assume the form" of any element that is put inside it.
That's pretty much what's needed to create our DropableItem
.
DraggableItem
Now, let's create the component that will allow us to drag elements around the interface.
This is how it will look like:
<template>
<span
draggable="true"
@dragstart="handleDragStart"
>
<slot />
</span>
</template>
<script>
export default {
name: 'DraggableItem',
props: ['transferData'],
setup(props) {
const handleDragStart = event => {
event.dataTransfer.setData('value', JSON.stringify(props.transferData))
}
return { handleDragStart }
}
}
</script>
Let's dive deeper into this implementation. Starting with the template
:
-
draggable
- This attribute informs the browser that this is a draggable element.
Initially, we need to set the draggable
attribute as true
to enable the Drag and Drop API for the span
element that is around our slot
. It's important to mention that, in this case, even though we're working with VueJS, we have to set the value "true" explicitly, otherwise it won't work as expected.
@dragstart
- This is the default HTML event listened by VueJS. It is triggered when the user clicks, holds and drags the element.
Now let's take a look at the component's setup
:
We defined a method named onDragStart
that will be called when the user starts to drag the component.
In this method, we pass the transferData
prop value to the dataTransfer
property of the dragstart
event.
According to MDN Web Docs:
The DataTransfer object is used to hold the data that is being dragged during a drag and drop operation.
We need to serialize the value before setting it to dataTransfer
.
This will allow us to retrieve it when the element has been dropped.
So far, so good!
This is all we need to build generic and reusable wrapper components to drag and drop elements around our application.
Now, to make use of them, we need to define the content of their default slots.
Let's suppose we want to create draggable circles that can be dragged into a square area.
Assuming they will be implemented in the App
component, here is how it would look like:
<template>
<div>
<DraggableItem v-for="ball in balls" :key="ball.id" :transferData="ball">
<div class="circle">
{{ ball.id }}
</div>
</DraggableItem>
<hr />
<DroppableItem>
<div class="square" />
</DroppableItem>
</div>
</template>
<script>
import { computed } from 'vue'
import DraggableItem from '@/components/DraggableItem'
import DroppableItem from '@/components/DroppableItem'
export default {
name: 'App',
components: {
DraggableItem,
DroppableItem
},
setup() {
const balls = [ { id: 1 }, { id: 2 }, { id: 3 } ]
return { balls }
}
}
</script>
<style>
.circle {
width: 50px;
height: 50px;
border-radius: 50%;
border: 1px solid red;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 5px;
}
.square {
display: inline-block;
width: 250px;
height: 250px;
border: 1px dashed black;
padding: 10px;
}
</style>
In this example, we can already drag each one of the balls, but nothing happens when we do it.
In order to make this implementation really work, we need to improve the code to make it more dynamic.
We are going to add:
availableBalls
- a computed property that will represent the balls available to be dragged. As the user drags a ball into the square, it will no longer be available to be dragged again.selectedBalls
- a reactive variable that will represent all of the balls that were dragged into the droppable square.isDroppableItemActive
- a reactive variable that will represent the state of the droppable square. We will use it to change the background color of the square when an element is being dragged over it.onDragOver
- a method that will be called when a ball is dragged over the square. It will be responsible for setting theisDroppableItemActive
variable and changing its background color.onDragLeave
- a method that will be called when a ball is dragged out of the square. It will be responsible for resetting theisDroppableItemActive
variable and its background color.onDrop
- a method that will be called when a ball is dropped into the square. It will reset its background color and update theselectedBalls
variable.
Notice that we use the dataTransfer.getData()
of Drag and Drop API to retrieve the data of that item that was dragged.
As it is a serialized value, we need to use JSON.parse
to "unserialize" it and turn it into a valid object.
We are going to use Lodash FP's differenceBy
method just for the sake of simplicity but you can implement your own filtering.
This is how our App
component will look like after the improvements:
<template>
<div>
<DraggableItem v-for="ball in availableBalls" :key="ball.id" :transferData="ball">
<span class="circle">
{{ ball.id }}
</span>
</DraggableItem>
<hr />
<DroppableItem v-bind="{ onDragOver, onDragLeave, onDrop }">
<span :class="droppableItemClass">
<span class="circle" v-for="ball in selectedBalls" :key="ball.id">
{{ ball.id }}
</span>
</span>
</DroppableItem>
</div>
</template>
<script>
import { differenceBy } from 'lodash/fp'
import { computed, ref } from 'vue'
import DraggableItem from './DraggableItem'
import DroppableItem from './DroppableItem'
export default {
name: 'DraggableBalls',
components: {
DraggableItem,
DroppableItem
},
setup() {
const balls = [ { id: 1 }, { id: 2 }, { id: 3 } ]
const selectedBalls = ref([])
const isDroppableItemActive = ref(false)
const availableBalls = computed(() => differenceBy('id', balls, selectedBalls.value))
const droppableItemClass = computed(() => ['square', isDroppableItemActive.value && 'hover'])
const onDragOver = () => {
isDroppableItemActive.value = true
}
const onDragLeave = () => isDroppableItemActive.value = false
const onDrop = event => {
const ball = JSON.parse(event.dataTransfer.getData('value'))
selectedBalls.value = [
...selectedBalls.value,
ball
]
isDroppableItemActive.value = false
}
return { availableBalls, selectedBalls, droppableItemClass, onDragOver, onDragLeave, onDrop }
}
}
</script>
<style>
.circle {
width: 50px;
height: 50px;
border-radius: 50%;
border: 1px solid red;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 5px;
}
.square {
display: inline-block;
width: 250px;
height: 250px;
border: 1px dashed black;
padding: 10px;
}
.hover {
background-color: rgb(172, 255, 158);
}
</style>
And this is the visual result:
You can find a more complete and fully-working example in this repo.
I hope you liked!
Please, share and comment.
Cover image by E-learning Heroes
Top comments (3)
Would it be difficult to add drop zones to a nested list, so I could move around the "cells"?
Hey @imaginativeone , it shouldn't be. If you follow the same logic implemented here.
Every "piece" of your nested list that would allow an element to be dragged into, would be a
DroppableItem
with the proper events configured.If you need something like being able to move a child node to another parent or sort the tree, the algorithm within the
onDrop
would be a little more complex than the example I've created for this article. But you would be able to do it.Post here if you get anything new in regards to that. I would love to see how you solved the problem. :)
Nice.