TLDR: this post will show you how you can add amazing drag and drop capabilities to your Svelte app using svelte-dnd-action. If you've always wanted to build a Trello clone using Svelte (just with nicer animations than Trello's) you've come to the right place.
Let's talk about drag and drop for a minute
If you've ever tried implementing an app that has rich (or even basic) drag and drop interactions, you would know that it is surprisingly difficult. Sure, the browser has a built in drag and drop API. There is only a minor issue with it - it falls flat on its face when it comes to look and feel.
Don't believe me? svelte-sortable-list is a library that uses the browser's drag and drop API and goes above and beyond (with the help of Svelte) to add as much animation as possible. Despite the admirable effort (I mean it), it is not something I could ship to production. The dragged element, as well as all the other elements, stay in their original place until a drop event takes place. It feels very static and stale (you can try it for yourself). As Rich Harris says: "we can do better".
React developers have been enjoying the mighty, although heavyweight and complicated, react-beautiful-dnd. Svelte developers (at least yours truly) were left wanting.
svelte-dnd-action is a new library that aims to correct that.
How does svelte-dnd-action work?
As its name suggests, the library uses Svelte's actions mechanism in order to turn any list container to a drag and drop (dnd) zone. It relies on its host (=== your code) to update the list's data when requested to do so (via events). It also relies on its host to help out with some of the animations by using Svelte's built-in flip animation.
Let's look at a simple example;
Say we have the following component that displays a list with 3 items:
<style>
div {
height: 1.5em;
width: 10em;
text-align: center;
border: 1px solid black;
margin: 0.2em;
padding: 0.3em;
}
</style>
<script>
let items = [
{id:1, title: 'I'},
{id:2, title: 'Am'},
{id:3, title: 'Yoda'}
];
</script>
<section>
{#each items as item(item.id)}
<div>
{item.title}
</div>
{/each}
</section>
Now let's say we want to make it re-sortable using drag and drop.
Let's add svelte-dnd-action into the mix:
<style>
div {
height: 1.5em;
width: 10em;
text-align: center;
border: 1px solid black;
margin: 0.2em;
padding: 0.3em;
}
</style>
<script>
import {dndzone} from 'svelte-dnd-action';
function handleSort(e) {
items = e.detail.items;
}
let items = [
{id:1, title: 'I'},
{id:2, title: 'Am'},
{id:3, title: 'Yoda'}
];
</script>
<section use:dndzone={{items}} on:consider={handleSort} on:finalize={handleSort}>
{#each items as item(item.id)}
<div>
{item.title}
</div>
{/each}
</section>
Play with this example in the REPL
Easy, right?
We pass out items into the dndzone
action and update our list when we get a consider
or finalize
event. The difference between the two is that consider
is emitted for intermediary states (as items need to 'make room') and finalize
is emitted when the element is dropped. The distinction between the two can be useful when deciding whether to save the new list in the server for example.
One important thing to notice is that each item in the list has an id
property which we also pass as the key for the #each
block. svelte-dnd-action
relies on the existence of the id
property so make sure you have it.
This is neat and all, but there are no animations yet. In order to make everything animate nicely, we need to add flip
into the mix and pass the flip duration into dndzone
as a parameter:
<style>
div {
height: 1.5em;
width: 10em;
text-align: center;
border: 1px solid black;
margin: 0.2em;
padding: 0.3em;
}
</style>
<script>
import {dndzone} from 'svelte-dnd-action';
import {flip} from 'svelte/animate';
const flipDurationMs = 200;
function handleSort(e) {
items = e.detail.items;
}
let items = [
{id:1, title: 'I'},
{id:2, title: 'Am'},
{id:3, title: 'Yoda'}
];
</script>
<section use:dndzone={{items, flipDurationMs}} on:consider={handleSort} on:finalize={handleSort}>
{#each items as item(item.id)}
<div animate:flip={{duration:flipDurationMs}}>
{item.title}
</div>
{/each}
</section>
Play with this example in the REPL
Viola, it animates!
Dnd zones types
By default, if you use dnd-zone
on multiple list containers, you will be able to grab an element from one list and drop it into another. That's pretty cool but sometimes you want control over what can go where.
In order to address this need svelte-dnd-action
accepts an optional type
parameter.
See it in action in the REPL.
In this example, you can move items between the two lists at the top, that have the type "light". You can't move items between the top lists and the bottom list, which has the type "dark" (luckily, Yoda and Luke are safe). You can still shuffle the item within each list like before.
One useful way to use types is for nested dnd-zones. For example, if you are building a Trello like board, each column can be a dndzone
(so items can be moved from one column to another) and the container that holds the columns can also be a dndzone
of a different type. That way, the columns can be re-ordered independently of the items that they hold.
What else can it do?
There is actually quite a bit more that this library can do.
To see a more complex example that includes horizontal and vertical lists, a board (nested zones as explained above) and demos the auto-scroll feature, please have a look at this REPL.
That's all for today folks. Happy dragging and dropping.
Edit Oct 3 2020: The library is now fully accessible as well. You can read more here.