Feature

Sortable lists with sortable JS

Preview

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
<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>
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}
})
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);
    }
  });
};