We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Jump to file
- ∟ mix.exs
- ∟ assets/package.json
- ∟ assets/css/app.css
- ∟ assets/js/app.js
- ∟ assets/vendor/Sortable.js
- ∟ lib/my_app/tasks.ex
- ∟ lib/my_app/tasks/status.ex
- ∟ lib/my_app/tasks/status_order.ex
- ∟ lib/my_app/tasks/task.ex
- ∟ lib/my_app/tasks/task_order.ex
- ∟ lib/my_app_web/components/layouts.ex
- ∟ lib/my_app_web/components/layouts/root.html.heex
- ∟ lib/my_app_web/live/task_live/board.ex
- ∟ lib/my_app_web/live/task_live/components.ex
- ∟ lib/my_app_web/router.ex
- ∟ priv/repo/migrations/20250429154243_create_tasks.exs
- ∟ priv/repo/seeds.exs
Install JS deps
Install the following npm package.
- @phx-hook/sortable
- npmjs.com/package/@phx-hook/sortable
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.
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.