DEV Community

Cover image for Dnd-Master: Lightweight, Powerful Drag-and-Drop for Svelte
Jim Bridger
Jim Bridger

Posted on

Dnd-Master: Lightweight, Powerful Drag-and-Drop for Svelte

A big thankyou to epicenter for sponsoring my open source work.

Have you ever noticed that when it comes to drag and drop options, the choices seem to be:

  • Use the Native Drag and Drop API (which everyone should do exactly once), or;
  • Use this fully-featured library with a big detailed API to learn, and we hope you want to animate lists

There doesn't really seem to be a middle-ground: something that abstracts over the painful parts of the native experience, but that is also dead simple to use and flexible. Powerful, even.

So I made Dnd-Master; a minimal hooks-based solution that is extendable via middleware. And it's typesafe. In this article, I'll go over the basics of how it works and then show examples of two-way validation and a dynamic ghost element.

This is just meant to be a quick read so you can decide if it's right for you; for the full README, visit the github repo.

You can also find a test playground here.

Dragging

To pass some data, use dnd.draggable to create an attachment, and attach it to an element:

const draggable = dnd.draggable("My data")
Enter fullscreen mode Exit fullscreen mode
<div {@attach draggable}>My data</div>
Enter fullscreen mode Exit fullscreen mode

Creating a dropzone is almost the same, but pass it a callback instead of an object:

const dropzone = dnd.dropzone(data => console.log(data))
Enter fullscreen mode Exit fullscreen mode
<div {@attach dropzone}>Drop here</div>
Enter fullscreen mode Exit fullscreen mode

That's pretty straightforward, but the real fun begins when we start using hooks:

let timesDropped = $state(0)

const draggable = dnd.draggable("My data", {
   drop: () => timesDropped++
})
Enter fullscreen mode Exit fullscreen mode

Notice that here, we're running some drop-aware logic on the data side of things, without needing to connect anything to the drop callback, which might very well be in a different component.

Both the data and drop attachments can implement all the native drag events as hooks, as well as a couple of others, and middlewares can provide more.

Middleware

Dnd-Master comes with two middlewares: Validate and Ghost. Let's quickly look at how to validate data, then we'll create a ghost element, and then we'll finish by implementing two-way validation and a dynamic ghost that responds to different dropzones.

Validation

To validate data, use assertData. This function takes a predicate, and returns a validator function that also has a builder on it that replaces dnd.draggable. Like this:

const isString = dnd.assertData(data => typeof data === "string")

const dropzone = isString.soDrop(data => lastDropped = data)
// this will only run your drop callback if the predicate is true
Enter fullscreen mode Exit fullscreen mode

You can also make your drop callback typesafe by using a type assertion in assertData:

const isString = dnd.assertData(
   (data): data is string => typeof data === "string"
) //     ^^^^^^^^^^^^^^^^

const dropzone = isString.soDrop(data => lastDropped = data)
// now typescript knows data is a string!
Enter fullscreen mode Exit fullscreen mode

I like this approach because it encourages me to use a feature that I always forget exists, without abstracting over it.

Ghost

First, let's create an element to use as our ghost. We'll wrap it in <template> so it doesn't appear in the DOM:

<template>
   <div class="ghost" bind:this={ghostElement}>👻 Ghost</div>
</template>
Enter fullscreen mode Exit fullscreen mode

You could style this however you like. Now here's how to add it to the dragged element:

let ghostElement = $state<HTMLDivElement>()

const draggable = dnd.draggable("My item", {
   dragstart: () => dnd.setGhost(ghostElement)
})
Enter fullscreen mode Exit fullscreen mode

We can just use the dragstart hook, as well as the setGhost function provided by the ghost middleware. Painless.

Advanced Examples

Two-way Validation

We already saw how to validate data. Here's how to validate a dropzone:

const isValidZone = dnd.assertZone(
   element => element.dataset.zone === "valid"
)

const draggable = isValidZone.soGive("My item")

// and let's validate some data again:
const isString = dnd.assertData(
   (data): data is string => typeof data === "string"
)

const dropzone = isString.soDrop(data => lastDropped = data)
Enter fullscreen mode Exit fullscreen mode
<div {@attach draggable}>My item</div>

<div data-zone="valid" {@attach dropzone}>Valid Dropzone</div>
Enter fullscreen mode Exit fullscreen mode

Now both sides of the drag and drop flow will validate before passing the data.

Dynamic Ghost

Here we want a ghost that will update when over different dropzones.

let defaultGhost = $state<HTMLDivElement>()
let validGhost = $state<HTMLDivElement>()
let invalidGhost = $state<HTMLDivElement>()

const isValidZone = dnd.assertZone(element =>
   element.dataset.zone === "valid"
)

const dynamicItem = isValidZone.soGive("Item with Dynamic Ghost", {
   dragstart: () => dnd.setGhost(defaultGhost),
   dragleave: () => dnd.setGhost(defaultGhost),
   dragover: (_event, element) => {
      isValidZone(element)
         ? dnd.setGhost(validGhost)
         : dnd.setGhost(invalidGhost)
   }
})

const dropzone = dnd.dropzone(data => console.log(data))
Enter fullscreen mode Exit fullscreen mode
<div {@attach draggable}>My item</div>

<div data-zone="valid" {@attach dropzone}>Valid Dropzone</div>
<div data-zone="invalid" {@attach dropzone}>Invalid Dropzone</div>

<template>
   <div bind:this={defaultGhost}>👻 Ghost</div>
   <div bind:this={validGhost}>✅ Drop here!</div>
   <div bind:this={invalidGhost}>❌ Can't park here mate</div>
</template>
Enter fullscreen mode Exit fullscreen mode

And that's it! Notice that we didn't need to create two separate dropzones; all our validation is data-based in the element data attributes. You could of course choose a different method, the data attribute approach is nothing to do with the library itself, just an idiomatic way of doing it.


Hopefully that's enough to make you interested in trying out Dnd-Master. You can find it on github and npm. It's open source, MIT Licensed, and ready to be expanded with more middleware!

If you use it, let me know how it goes!

Top comments (0)