VueJS - Drag 'n Drop

vcpablo

Pablo Veiga

Posted on October 1, 2021

VueJS - Drag 'n Drop

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>

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 the isDroppableItemActive 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 the isDroppableItemActive 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 the selectedBalls 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>
Enter fullscreen mode Exit fullscreen mode

And this is the visual result:

VueJS - Drag and Drop Example

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

💖 💪 🙅 🚩
vcpablo
Pablo Veiga

Posted on October 1, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related