Alpine.js Drag and Drop

— 18 minute read

Alpine.js is a new lightweight JavaScript framework which uses the approach of adding behaviour directly within your HTML markup. According to the documentation;

Alpine.js offers you the reactive and declarative nature of big frameworks like Vue or React at a much lower cost. You get to keep your DOM, and sprinkle in behavior as you see fit.

After including the code – I use the npm method – you can sprinkle the behaviour you need. They provide a neat example for tabs;

<div x-data="{ tab: 'foo' }">
<button class="button" :class="{ 'button--active': tab === 'foo' }" x-on:click="tab = 'foo'">Foo</button>
<button class="button" :class="{ 'button--active': tab === 'bar' }" x-on:click="tab = 'bar'">Bar</button>

<div x-show="tab === 'foo'">Tab Foo</div>
<div x-show="tab === 'bar'">Tab Bar</div>
</div>

If you understand basic HTML and JavaScript, you can hopefully understand what is happening quite easily. Click events are added to <button> elements using the x-on:click attribute. The x-show attribute is calculating which element to show. And :class is used to add an ‘active’ class to the button.

A set of elements become a component by using the x-data attribute, which in the example above defaults the visible tab.


Building drag and drop listing permalink

I recently needed to build a drag and drop interface. Normally, I would reach for a pre-built solution, research which packages are available and decide what to use. This was to avoid cross-browser issues and re-inventing the wheel.

However, JavaScript support has improved vastly over the years and browsers have added native support for drag and drop events. This is a relatively small component, so it didn't make much sense to use a large library to achieve what was needed.


Building it up permalink

The first step in building any component like this is to define the HTML we want to use. I have used the BEM methodology to help the naming convention.

We need two lists, each with their own heading, a container and a divider.

<div class="drag-and-drop">
<div class="drag-and-drop__container drag-and-drop__container--from">
<h3 class="drag-and-drop__title">From</h3>
<ul class="drag-and-drop__items">
<!-- loop through all our items -->
<li class="drag-and-drop__item"></li>
<!-- end -->
</ul>
</div>
<div class="drag-and-drop__divider"></div>
<div class="drag-and-drop__container drag-and-drop__container--to">
<h3 class="drag-and-drop__title">To</h3>
<ul class="drag-and-drop__items">
<!-- loop through all our selected items -->
<li class="drag-and-drop__item"></li>
<!-- end -->
</ul>
</div>
</div>

Create an Alpine.js component permalink

Now we need to start adding our Alpine.js. Start by adding the x-data attribute to setup our new component scope.

<div class="drag-and-drop" x-data>
...
</div>

Next we need denote which elements are draggable. We want the <li> to be draggable, so we add the attribute to it.

<li class="drag-and-drop__item" draggable="true"></li>

Bind events permalink

We need to define where the draggable element can be dropped. We need to bind the event to both <ul> elements. We can use Alpine.js' x-on attribute here.

<ul class="drag-and-drop__items" x-on:drop="">
...
</ul>

There are other events we can listen to, to add affordance to the user interaction.

<ul class="drag-and-drop__items"
x-on:drop=""
x-on:dragover.prevent=""
x-on:dragleave.prevent="">

...
</ul>

Setup data and toggle permalink

At the moment, these are just shells of events and don't actually do anything. To make them useful, we can start manipulating the data on the component. First we need to setup what properties the component should care about. We want to know when an element is being added or removed. We can set up those defaults like this;

<div class="drag-and-drop" x-data="{ adding: false, removing: false }">
...
</div>

Now we can manipulate those attributes inside our events. For our “To” list, we want to know when we're adding a new item, so we update the adding property. Similar, we would update the removing property on the “From” list. Here is what the “To” list looks like now.

<ul class="drag-and-drop__items"
x-on:drop=""
x-on:dragover.prevent="adding = true"
x-on:dragleave.prevent="adding = false">

...
</ul>

We've changed some values, but there is no visible UI feedback when this happens. We can do this by changing the styles, by modifying the classes on the list. When the adding property is true we can add a new class using :class attribute. This class is removed when the adding property is false.

<ul class="drag-and-drop__items"
:class="{ 'drag-and-drop__items--adding': adding }"
x-on:drop=""
x-on:dragover.prevent="adding = true"
x-on:dragleave.prevent="adding = false">

...
</ul>

We would use the same syntax when removing items, by toggling the removing property and change the class. I have used the class drag-and-drop__items--removing but we could use the same class name for both lists.

We can apply the same idea to styling the item we want to drag. At the moment, we only have the draggable attribute applied. Lets set each item to be a component, bind some events and update the class.

<li
class="drag-and-drop__item"
:class="{ 'drag-and-drop__item--dragging': dragging }"
draggable="true"
x-data="{ dragging: false }"
x-on:dragstart.self="dragging = true"
x-on:dragend="dragging = false">

...
</li>

Note we have added a .self modifier to the event binding. The documentation says;

Adding .self to an event listener will only trigger the handler if the $event.target is the element itself.


Handle the drag and drop permalink

So far we have bound a few events and used those events to bring affordance the user interface by updating the dragging item style and showing where the item might be dropped. Next we need to actually move the element.

We need to use the dragstart event to update the dataTransfer object. This object stores information which is available to the drop event. As we are moving our item, we set the effectAllowed property to move. Secondly, we need to add information about our element to the event. As each item has an id, we can use that as a reference.

x-on:dragstart.self="
dragging = true;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', event.target.id);
"

Now the drop event we bound to the target container has access to this information. Instead of using the setData() method, we can use the getData() method from the dataTransfer object. This will return the id of the item we are dropping. We want to append our item to the target. Using standard JavaScript, we can do this;

x-on:drop.prevent="
const id = event.dataTransfer.getData('text/plain');
const target = event.target.closest('ul');
const element = document.getElementById(id);
target.appendChild(element);
"

The code permalink

We now have all the individual parts to build out drag and drop component. Putting it all together it should look like this;

<div class="drag-and-drop" x-data="{ adding: false, removing: false }">
<div class="drag-and-drop__container drag-and-drop__container--from">
<h3 class="drag-and-drop__title">From</h3>
<ul
class="drag-and-drop__items"
:class="{ 'drag-and-drop__items--removing': removing }"
x-on:drop="removing = false"
x-on:drop.prevent="
const id = event.dataTransfer.getData('text/plain');
const target = event.target.closest('ul');
const element = document.getElementById(id);
target.appendChild(element);
"

x-on:dragover.prevent="removing = true"
x-on:dragleave.prevent="removing = false">

<!-- loop through the items -->
<li
id="item-1"
class="drag-and-drop__item"
:class="{ 'drag-and-drop__item--dragging': dragging }"
x-on:dragstart.self="
dragging = true;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', event.target.id);
"

x-on:dragend="dragging = false"
x-data="{ dragging: false }"
draggable="true">

Your Item #1
</li>
</ul>
</div>
<div class="drag-and-drop__divider"></div>
<div class="drag-and-drop__container drag-and-drop__container--to">
<h3 class="drag-and-drop__title">To</h3>
<ul
class="drag-and-drop__items"
:class="{ 'drag-and-drop__items--adding': adding }"
x-on:drop="adding = false"
x-on:drop.prevent="
const id = event.dataTransfer.getData('text/plain');
const target = event.target.closest('ul');
const element = document.getElementById(id);
target.appendChild(element);
"

x-on:dragover.prevent="adding = true"
x-on:dragleave.prevent="adding = false">

<!-- loop through the already selected items -->
</ul>
</div>
</div>

Summary permalink

I hope you found this tutorial useful, especially if you're new to the world of Alpine.js.

This is a very basic drag and drop implementation with minimal JavaScript thanks to Alpine.js. It stills need improving, such as to support re-ordering within each list. But it feels like a good starting point.

Personally, I would now begin to move the JavaScript to a separate file. I would keep the event bindings and class switching in the HTML, but the longer JavaScript blocks would be more maintainable in a specific JavaScript file.

Preview permalink

You can view this code in action on Codepen demo.


Read more… permalink