What's a great modern drag and drop javascript library you recommend?

Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browsers. Modern browsers may have shiny new API’s that are not being leveraged by older plugins.

What’s a great, modern drag and drop plugin you would recommend?

One feature I’m trying to build: I’m trying to replicate the Trello list drag and drop feature where you can drag cards between lists, and drag the lists around as well.

One plugin I found is draggable by Shopify: Draggable JS – JavaScript drag and drop library But it’s not maintained anymore, and has a ton of open PRs.

Another plugin is Sortable: GitHub - SortableJS/Sortable: Reorderable drag-and-drop lists for modern browsers and touch devices. No jQuery or framework required.

Thanks!

Sorry for the non-answer but I use Draglua which is also unmaintained. It works and it’s nice and lightweight. I’m only using it on an internal app, though.

1 Like

This blog uses sortableJS.
I haven’t used it personally, but if fly people are using it, makes me biased already.

8 Likes

Oh right, I should have mentioned Sortable, ha. It’s basically the defacto vanilla JS sorting library. I just like the way the way Dragula “feels” better but I’ve used Sortable and it certailny works well.

I am happy with Sortable JS, I wrote a custom hook with Liveview, similar to the one in the article

4 Likes

Sortable was extremely easy to integrate, and well maintained, and vanilla. Thanks for the suggestions guys!

import { ApplicationSortable } from "./hooks/applicationSortable.js";

let liveSocket = new LiveSocket("/live", Socket, {
  params: { _csrf_token: csrfToken },
  hooks: { ApplicationSortable }
});

export const ApplicationSortable = {
    mounted() {
        let group = this.el.dataset.group;

        let sorter = new Sortable(this.el, {
            group: group ? group : undefined,
            animation: 150,
            dragClass: "drag-item",
            ghostClass: "drag-ghost",
            forceFallback: true,
            onEnd: e => {
                let params = { old: e.oldIndex, new: e.newIndex, ...e.item.dataset }
                console.log(params);
                this.pushEventTo(this.el, "reposition", params)
            }
        })
    }
};
5 Likes

Note that SortableJS is not concerned with accessibility.

4 Likes

I used Sortable and was very pleased with what I was able to do with it. I would share it but it’s in a project I made that never saw the light of day on the open Internet.

The todo_trek showcase app is using sortable.js as well.

1 Like

There are similar issues on both Dragula and Draggable. Do you know of a vanilla JS lib that is accessible? I can’t seem to find one.

1 Like

I didn’t find a maintained accessible JS library either. I guess one way to mitigate this is to use SortableJS, but also offer move-up and move-down buttons that are keyboard-accessible, if you can live with the additional UI elements.

Some general articles about accessible drag and drop:

https://dev.opera.com/articles/accessible-drag-and-drop/

https://medium.com/salesforce-ux/4-major-patterns-for-accessible-drag-and-drop-1d43f64ebf09

5 Likes

I always reach for https://dndkit.com myself.

4 Likes

Shopify’s draggable is pretty good. Documentation is a bit sparse.

Says “built for React” and all examples are React. Does it work without?

I never tried, but it doesn’t look like it

After many hours of research, I have finally found a dnd library that is built for vanillajs first, is actually performant (uses webworkers), has a good auto scroll out of the box, and is used in production applications:

Additionally, it doesn’t use the native drag and drop web api, which means that it doesn’t over-scroll in a glitchy way on mobile, and the dragged element can be constrained to a certain axis since it doesn’t need to follow the cursor.

It’s more of a layout engine, but there are Kanban examples that can easily be pared down to do what scrollablejs does.

9 Likes

Hey y’all. I’m not an Elixer dev but I came across this thread while searching for drag-and-drop libraries.

Pragmatic Drag and Drop by Atlassian is worth a look. It’s a low-level library so you might have to do more setup than, e.g., Dragula, but the documentation is extensive.

Cheers!

1 Like

Elixir**

1 Like

This one looks interesting.
I’m still looking for something like react-beautiful-dnd (also from Atlassian), but not tied to React.
It seems that Pragmatic DnD also uses the web platform’s drag and drop API instead of controlling the dragged items movement which I guess is more performant but it somehow just feels less “smooth”.

There is nothing I’ve seen that feels as pleasant to use (to me) as the standard react-beautifu-dnd example here:
https://react-beautiful-dnd.netlify.app/iframe.html?id=board--simple

I was looking at Muuri today and trying to get the Kanban example working. Examples | Muuri Docs It took me some time to turn the CSS into tailwind and break the HTML into small functions so I am pasting what I did here. Hopefully it helps somebody else get started with Muuri + Phoenix.

defmodule MyAppWeb.MuuriLive.Kanban do
  @moduledoc false
  use MyAppWeb, :html

  def kanban(assigns) do
    ~H"""
    <div id="muuri-kanban-demo" phx-hook="KanbanHook">
      <div class={["drag-container", "fixed top-0 left-0", "z-[1000]"]}></div>
      <div class={["board", "relative"]}>
        <.board_column header="Todo" indexes={1..5} />
        <.board_column header="Working" indexes={6..10} />
        <.board_column header="Done" indexes={11..15} />
      </div>
    </div>
    """
  end

  attr :header, :string
  attr :indexes, :integer

  defp board_column(assigns) do
    ~H"""
    <div class={[
      "board-column done",
      "absolute top-0 left-0",
      "py-0 px-[10px]",
      "w-1/3",
      "z-[1] muuri-item-releasing:z-[2] muuri-item-dragging:z-[3] muuri-item-dragging:cursor-move"
    ]}>
      <div class={[
        "board-column-container",
        "w-full h-full"
      ]}>
        <div class={[
          "board-column-header",
          "relative",
          "h-[50px] leading-[50px]",
          "overflow-hidden",
          "py-0 px-[20px]",
          "text-center",
          "bg-green-500",
          "text-white",
          "rounded-sm",
          "font-bold",
          "tracking-[0.5px]",
          "uppercase"
        ]}>
          <%= @header %>
        </div>
        <div class={[
          "board-column-content-wrapper",
          "relative",
          "p-[8px]",
          "bg-blue-700",
          "h-screen",
          "overflow-y-auto",
          "rounded-sm"
        ]}>
          <div class={["board-column-content", "relative min-h-full"]}>
            <.board_item :for={index <- @indexes} index={index} />
          </div>
        </div>
      </div>
    </div>
    """
  end

  attr :index, :integer

  defp board_item(assigns) do
    ~H"""
    <div class={[
      "board-item",
      "absolute w-full m-[8px]",
      "muuri-item-releasing:z-[9998] muuri-item-dragging:z-[9999]",
      "muuri-item-dragging:cursor-move",
      "muuri-item-hidden:z-[0]"
    ]}>
      <div class={[
        "board-item-content",
        "relative p-[20px]",
        "bg-white",
        "rounded-[4px]",
        "text-[17px]",
        "cursor-pointer"
      ]}>
        <span>Item #</span><%= @index %>
      </div>
    </div>
    """
  end
end

and the hook

import Muuri from "../../vendor/muuri";

const KanbanHook = {
  mounted() {
    var dragContainer = document.querySelector(".drag-container");
    var itemContainers = [].slice.call(
      document.querySelectorAll(".board-column-content"),
    );
    var columnGrids = [];
    var boardGrid;

    // Init the column grids so we can drag those items around.
    itemContainers.forEach(function (container) {
      var grid = new Muuri(container, {
        items: ".board-item",
        dragEnabled: true,
        dragSort: function () {
          return columnGrids;
        },
        dragContainer: dragContainer,
        dragAutoScroll: {
          targets: (item) => {
            return [
              { element: window, priority: 0 },
              { element: item.getGrid().getElement().parentNode, priority: 1 },
            ];
          },
        },
      })
        .on("dragInit", function (item) {
          item.getElement().style.width = item.getWidth() + "px";
          item.getElement().style.height = item.getHeight() + "px";
        })
        .on("dragReleaseEnd", function (item) {
          item.getElement().style.width = "";
          item.getElement().style.height = "";
          item.getGrid().refreshItems([item]);
        })
        .on("layoutStart", function () {
          boardGrid.refreshItems().layout();
        });

      columnGrids.push(grid);
    });

    // Init board grid so we can drag those columns around.
    boardGrid = new Muuri(".board", {
      dragEnabled: true,
      dragHandle: ".board-column-header",
    });
  },
};
export { KanbanHook };

I will be changing the JS to use this.el. For now, it is identical to the JS given in their example.

It works nice. It “feels” better than sortable, which was my go-to for drag-n-drop

The tailwind modifiers I added like this:

    plugin(({ addVariant }) => addVariant("muuri", [".muuri&", ".muuri &"])),
    plugin(({ addVariant }) => addVariant("muuri", [".muuri&", ".muuri &"])),
    plugin(({ addVariant }) =>
      addVariant("muuri-item", [".muuri-item&", ".muuri-item &"]),
    ),
    plugin(({ addVariant }) =>
      addVariant("muuri-item-shown", [
        ".muuri-item-shown&",
        ".muuri-item-shown &",
      ]),
    ),
    plugin(({ addVariant }) =>
      addVariant("muuri-item-hidden", [
        ".muuri-item-hidden&",
        ".muuri-item-hidden &",
      ]),
    ),
    plugin(({ addVariant }) =>
      addVariant("muuri-item-positioning", [
        ".muuri-item-positioning&",
        ".muuri-item-positioning &",
      ]),
    ),
    plugin(({ addVariant }) =>
      addVariant("muuri-item-dragging", [
        ".muuri-item-dragging&",
        ".muuri-item-dragging &",
      ]),
    ),
    plugin(({ addVariant }) =>
      addVariant("muuri-item-releasing", [
        ".muuri-item-releasing&",
        ".muuri-item-releasing &",
      ]),
    ),
    plugin(({ addVariant }) =>
      addVariant("muuri-item-placeholder", [
        ".muuri-item-placeholder&",
        ".muuri-item-placeholder &",
      ]),
    ),

in tailwind config js.

7 Likes