Feature
Sortable lists with sortable JS
This feature uses the javascript library sortable js to handle drag and drop sorting. Install it with:
yarn add sortablejs
The javascript is initiated with a LiveView hook. InitSortable
. The idea is that the as soon the the sorting event is triggered, I send a push event to the component with the list of task ids and the sortorder.
In this example, there are also two connected lists and the tasks can be sorted and drag between both lists.
In the markup, I use data attributes like:
data-target-id="#<%= @id %>" <!-- The LiveView Component that I need to send the event to -->
data-list-id="<%= list_id %>" <!-- The is of the current list -->
data-sortable-id="<%= task[:id] %>" <!-- The id of the current task in the list -->
Note that when the tasks are returned from the Tasks contect, they are sorted by the sort_order attribute.
tasks = Tasks.list_tasks()
lib/phoenix_features_web/live/components/sortable_simple.ex
defmodule PhoenixFeaturesWeb.Components.SortableSimple do
use PhoenixFeaturesWeb, :live_component
alias PhoenixFeatures.Tasks
@lists ~w(1 2)
def mount(socket) do
tasks = Tasks.list_tasks()
{:ok,
socket
|> assign(:tasks, tasks)
|> assign(:lists, @lists)
}
end
def handle_event("sort", %{"list" => [_|_] = list}, socket) do
list
|> Enum.each(fn %{"id" => id, "list_id" => list_id, "sort_order" => sort_order} ->
Tasks.get_task(id)
|> Tasks.update_task(%{list_id: list_id, sort_order: sort_order})
end)
{:noreply, socket}
end
end
ib/phoenix_features_web/live/components/sortable_simple.html.leex
<div id="<%= @id %>" style="min-height: 250px;" ><!-- NOTE THE THE COMPONENT NEEDS TO BE TRACKED WITH AN ID -->
<div class="flex flex-wrap -mx-4 overflow-hidden">
<%= for list_id <- @lists do %>
<div class="my-2 px-4 w-1/2 overflow-hidden">
<div class="p-2 bg-gray-200 rounded h-full">
<h3 class="m-2 text-lg font-semibold">List <%= list_id %></h3>
<div phx-hook="InitSortable" data-target-id="#<%= @id %>" data-list-id="<%= list_id %>" >
<%= for task <- Enum.filter(@tasks, &(&1[:list_id] == list_id)) do %>
<div data-sortable-id="<%= task[:id] %>" class="cursor-move bg-white rounded flex p-4 my-1 items-center">
<svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" class="text-indigo-500 w-6 h-6 flex-shrink-0 mr-4" viewBox="0 0 24 24">
<path d="M22 11.08V12a10 10 0 11-5.93-9.14"></path>
<path d="M22 4L12 14.01l-3-3"></path>
</svg>
<span class="title-font font-medium"><%= task[:name] %></span>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
</div>
assets/js/app.js
import {InitSortable} from "./init_sortable"
let Hooks = {}
Hooks.InitSortable = InitSortable
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
hooks: Hooks,
params: {_csrf_token: csrfToken}
})
assets/js/init_sortable.js
import Sortable from "sortablejs";
export const InitSortable = {
mounted() {
const callback = list => {
this.pushEventTo(this.el.dataset.targetId, "sort", { list: list });
};
init(this.el, callback);
}
};
const init = (sortableList, callback) => {
const listId = sortableList.dataset.listId
const sortable = new Sortable(sortableList, {
group: "shared",
handle: ".cursor-move",
dragClass: "shadow-xl",
onSort: evt => {
let ids = [];
const nodeList = sortableList.querySelectorAll("[data-sortable-id]");
for (let i = 0; i < nodeList.length; i++) {
const idObject = { id: nodeList[i].dataset.sortableId, list_id: listId, sort_order: i };
ids.push(idObject);
}
callback(ids);
}
});
};