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.
Top comments (16)
Didn't even think of using a library for dnd functionality. Thank you for this article!
I'm looking for a solution for multiple virtual lists (infinite load on scroll) which can be filtered by search input and also be able to drag&drop items between lists. Is there a way for achieve this?
the virtual list aspect is not covered by the lib but i don't see why it won't be compatible with that. same goes for the flitering by search
Thank you! Well I have a repl for this (svelte.dev/repl/c4fc230da62f4f8295...)
however, I'm getting getBoundingClientRect of undefined error when I try to move the for the first time an item, but not sure if the problem is with my code, or should I open an issue for this? :)
sure. feel free to create an issue. please explain what you are trying to acheive (your consider and finalize handlers seems more complex that i would expect)
Great. I will have to build a small drag and drop app soon and I think I will consider svelte. I did not hear much about svelte before your post but first impression is good. Do you have any beginner reading/tutorial that you suggest me to start learning about svelte?
Hi,
Svelte is quite amazing. The official tutorial is really good and I recommend starting there.
May I ask how to synchronize the items list with a store?
I tried naive approach such as
import { ListStore } from "./ListStore"
let items = $ListStore
const handleSortEnd = e => {
items = e.detail.items
ListStore.set(items)
}
but got a bizarre case of double headings in draggable elements
Update: here is full code
github.com/c0dehamster/drag-and-dr...
You should be able to use the store directly and with no problem. Feel free to create a github issue with a Repl link
Thank you, I figured out how to connect to the store, is was actually very easy. Works as intended now
Awesome library and write up for it!
Every example is with hardcoded data, as soon as data comes from an api drag and drop does not work anymore, can you make an example for this?
what do you mean? of course it works. In my company we are using it with data from all kinds of sources all of the time. Feel free to make an issue with an example in github and I could have a look
Sure here a codesanbox with the example. I need to say that I'm new at Svelte coming from React so I'm sure I'm missing something would be awesome if you could take a look, thanks! codesandbox.io/s/svelte-api-dnd-9wzpe PS. the codesanbox is a bit buggy you need to refresh it in order to show the app.
line 30 should be {#each items as todo(todo.id)}
you are iterating over a variable you are not updating in handleSort
thanks