VueJS - Drag 'n Drop
Tags: vue, javascript, frontend, beginners
Published: 2021-10-01 11:58:08

For some time, it was necessary to write a lot of [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) code in order to implement the famous drag 'n drop feature in a web application.

Fortunately, in [January of 2008](https://www.w3.org/TR/2008/WD-html5-20080122/), W3C released the fifth version of [HTML](https://html.spec.whatwg.org/multipage/) which provides the scripting [Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_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](https://v3.vuejs.org/)._

---

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](https://dev.to/vcpablo/vuejs-one-more-way-of-structuring-your-project-13mj) 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:

```vue
<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`](https://v3.vuejs.org/guide/component-slots.html) 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:

```vue
<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](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer):

> 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:

```vue
<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 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](https://github.com/lodash/lodash/wiki/FP-Guide) `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:

```vue
<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:

[VueJS - Drag and Drop Example](https://www.loom.com/share/97ccd7682699469c8cb23c93ad2432a9)

You can find a more complete and fully-working example in [this repo](https://github.com/vcpablo/vuejs-drag-n-drop).

I hope you liked!
Please, share and comment.

---

_Cover image by [E-learning Heroes](https://articulate-heroes.s3.amazonaws.com/uploads/rte/xzfjcani_Ways-Help-Learners-Practice-Their-Skills-with-Scenarios-in-Rise-360.png)_