LiveView Drag and Drop with SortableJS
LiveView hooks provide a powerful way to seamlessly integrate JavaScript libraries into LiveView applications. In this post, we’ll integrate SortableJS to build a list component with draggable items.
Defining a live_component
We begin by defining a :live_component called ListComponent, along with its two main callbacks, render/1 and update/2:
defmodule ComponentsExamplesWeb.ListComponent do
use ComponentsExamplesWeb, :live_component
def render(assigns) do
~H"""
<div class="bg-gray-100 py-4 rounded-lg">
<div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
</div>
</div>
"""
end
def update(assigns, socket) do
{:ok, assign(socket, assigns)}
end
end
Next, we add some elements to our component. The component has three main sections: 1) a header with the list title and an input for adding new elements, 2) the list of draggable elements, and 3) a button for clearing the list.
def render(assigns) do
~H"""
<div class="bg-gray-100 py-4 rounded-lg">
<div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
+ <.header>
+ <%= @list_name %>
+ <.simple_form
+ for={@form}
+ phx-change="validate"
+ phx-submit="save"
+ phx-target={@myself}
+ >
+ <.input field={@form[:name]} type="text" />
+ <:actions>
+ <.button class="align-middle ml-2">
+ <.icon name="hero-plus" />
+ </.button>
+ </:actions>
+ </.simple_form>
+ </.header>
</div>
</div>
"""
end
Now the part we’re interested in today: the items list!
def render(assigns) do
~H"""
<div class="bg-gray-100 py-4 rounded-lg">
<div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
<.header>
...
</.header>
+ <div id={"#{@id}-items"}>
+ <div
+ :for={item <- @list}
+ id={"#{@id}-#{item.id}"}
+ class="..."
+ >
+ <div class="flex">
+ <button type="button" class="w-10">
+ <.icon
+ name="hero-check-circle"
+ class={[
+ "w-7 h-7",
+ if(item.status == :completed, do: "bg-green-600", else: "bg-gray-300")
+ ]}
+ />
+ </button>
+ <div class="flex-auto block text-sm leading-6 text-zinc-900">
+ <%= item.name %>
+ </div>
+ <button type="button" class="w-10 -mt-1 flex-none">
+ <.icon name="hero-x-mark" />
+ </button>
+ </div>
+ </div>
</div>
</div>
</div>
"""
end
Adding SortableJS to our LiveView app
There is an existing JavaScript library called SortableJS that provides drag-and-drop functionality for elements inside an HTML tag. Let’s add SortableJS to our LiveView application!
- Go to the SortableJS source repository and locate the
sortable.jsfile. - Copy the
sortable.jsfile to the/assets/vendor/directory in your Phoenix project.
Next, we need to import the Sortable library. You can do this by adding the following line at the top of the app.js file:
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
+ import Sortable from "../vendor/sortable"
Once the Sortable library is imported, we can use it in our component by defining a Hook in the same file:
let Hooks = {}
Hooks.Sortable = {
mounted(){
let sorter = new Sortable(this.el, {
animation: 150,
delay: 100,
dragClass: "drag-item",
ghostClass: "drag-ghost",
forceFallback: true,
onEnd: e => {
let params = {old: e.oldIndex, new: e.newIndex, ...e.item.dataset}
this.pushEventTo(this.el, "reposition", params)
}
})
}
}
let liveSocket = new LiveSocket("/live",
Socket,
{params: {_csrf_token: csrfToken}, hooks: Hooks}
)
Using SortableJS in LiveView components
We add a couple of lines to our component:
def render(assigns) do
~H"""
<div class="bg-gray-100 py-4 rounded-lg">
<div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
<.header>
...
</.header>
- <div id={"#{@id}-items"}>
+ <div id={"#{@id}-items"} phx-hook="Sortable" data-list_id={@id}>
<div
:for={item <- @list}
id={"#{@id}-#{item.id}"}
class="..."
+ data-id={item.id}
>
...
</div>
</div>
</div>
</div>
"""
end
Formatting draggable list items
Fixing this detail is simple. We’ve already specified the CSS classes to format the drop placeholder and the dragged article. However, instead of defining new CSS classes, we can leverage the ones already defined by Tailwind.
Bonus: multiple lists
What if we want to drag items between different lists? It is an option that Sortable already has, we just have to configure it:
Hooks.Sortable = {
+ let group = this.el.dataset.group
mounted(){
let sorter = new Sortable(this.el, {
+ group: group ? group : undefined,
animation: 150,
delay: 100,
dragClass: "drag-item",
ghostClass: "drag-ghost",
forceFallback: true,
onEnd: e => {
let params = {old: e.oldIndex, new: e.newIndex, ...e.item.dataset}
this.pushEventTo(this.el, "reposition", params)
}
})
}
}
Discussion
In this post, we’ve learned how to set up SortableJS in our LiveView app and use it in our live components. But here’s the thing - we’re not actually doing anything with the data we send from the client to the server yet. We still need to figure out how to persist our elements and the position changes.