Orderable Tasks

Build a Kanban-style task board with drag-and-drop ordering. Tasks can be reordered within columns and moved between status columns. Uses EctoOrderable for efficient position management with float-based ordering.

v1.8.3
Level: Medium
16
393
7

Install Elixir deps

Install the following dependency.

Install JS deps

Install the following npm package.

CSS variants

Add Tailwind custom variants for drag-and-drop states. The drag variants ( dragging , drag-item , drag-chosen , drag-ghost ) let you style elements differently during drag operations, such as making the ghost element invisible while showing a placeholder.

JavaScript setup

Import Sortable from the vendor directory along with the SortableHook package. Register the hook with your LiveSocket configuration. The SortableHook function takes the Sortable library as an argument, allowing it to create Sortable instances on elements with the phx-hook="Sortable" attribute.

Vendor Sortable.js

Download Sortable.js from sortablejs.github.io/Sortable and place it in your vendor directory. This library provides the drag-and-drop functionality for reordering tasks and columns.

Diff omitted (vendored file).

Tasks context

The Tasks context provides the public API for working with tasks and statuses. The list_tasks/1 function returns tasks for a given status ordered by their position field. It uses EctoOrderable's first_in_set? and last_in_set? query helpers via select_merge to populate virtual first? and last? fields on each task. These virtual fields indicate whether a task is at the boundary of its list, which the UI uses to conditionally show or hide the move up/down buttons. The list_statuses/0 function returns all statuses ordered by position, representing the columns on the board.

Status schema

The Status schema represents the columns on the Kanban board (Backlog, Todo, In Progress, Done). Rather than using a generated ID, it uses the status enum value itself as the primary key since statuses are a fixed set. The position field is a float that determines the left-to-right order of columns on the board. A helper function status_name/1 converts the atom to a human-readable string for display.

StatusOrder module

This module configures EctoOrderable for the Status schema by using the EctoOrderable.Order behaviour. It specifies the repo, schema, and scope. Since statuses are global and not scoped to any parent entity, the scope option is an empty list. This generates a move/2 function that can reposition statuses relative to each other.

Task schema

The Task schema defines the structure for individual tasks on the board. Each task has a number (for display like "TK-123"), title, optional description, status (which column it belongs to), and position (a float for ordering within that column). The virtual fields first? and last? are not stored in the database but are populated at query time to indicate whether a task is at the top or bottom of its column.

TaskOrder module

This module configures EctoOrderable for the Task schema. Unlike StatusOrder, tasks are scoped by their status field, meaning each column maintains its own independent ordering. When you call TaskOrder.move/2 , it calculates the new position relative to other tasks with the same status. If a task's status changes (moved to a different column), EctoOrderable handles inserting it at the correct position in the new column.

Layout updates

The Layouts.app component is extended with fullscreen and class attributes. When fullscreen is true, the main content area uses flex-1 flex classes instead of the standard centered container, allowing the board to fill the available space and scroll horizontally.

Root layout updates

The root layout is updated to support full-height layouts by adding h-full classes to both the html and body elements, along with scrollbar-gutter: stable to prevent layout shift when scrollbars appear.

Board LiveView

The board LiveView renders a complete Kanban board with draggable columns and tasks. It uses Layouts.app with the fullscreen attribute to fill the viewport. The outer container has a Sortable hook that enables reordering the status columns themselves by dragging their headers. Each column has its own nested Sortable hook with a shared data-group attribute, which allows tasks to be dragged not only within their column but also between columns.

When a drag operation completes, the hook sends an event with the item's ID and its neighboring items' IDs. The handle_event callbacks use StatusOrder.move/2 or TaskOrder.move/2 with the between option to position the item between its new neighbors. For cross-column task moves, the handler first updates the task's status field, then repositions it within the new column.

The LiveView returns layout: false from mount to render the Layouts.app component directly in the template rather than using the default layout wrapper.

Task components

The task/1 component renders an individual task card showing the task number, title, current position (for debugging), and a status icon. Move up/down buttons appear conditionally based on the first? and last? virtual fields, preventing users from trying to move a task beyond its boundaries.

The task_status/1 component renders a circular icon representing the task's current status with appropriate colors: gray for Backlog, sky blue for Todo, amber for In Progress, and indigo for Done. Each status has a corresponding icon from the Heroicons set.

Router

Add a live route for the task board at /board . This example places it as a public route, but you can move it inside an authenticated scope as needed for your application.

Migration

The migration creates two tables. The tasks table uses a UUID primary key and includes fields for number (with a unique index for generating sequential task IDs), title, description, status, position, and timestamps. The statuses table uses the status string itself as the primary key with a position field for ordering.

The position field uses the float type, which is central to EctoOrderable's approach. When inserting an item between two others, it calculates a position value between them (e.g., inserting between positions 1.0 and 2.0 yields 1.5). This avoids expensive reordering of all items in the list.

Seed data

The seeds file populates the database with the four status columns and a set of sample tasks distributed across them. Each status is inserted with an initial position, and tasks are created with sequential numbers and positions within their respective columns. Run the seeds with mix run priv/repo/seeds.exs after running migrations.