asantibanez/livewire-status-board
A Laravel Livewire package for building interactive status boards and Kanban-style workflows. Display records in customizable columns, drag and drop items between statuses, and persist changes with minimal setup.
asantibanez/livewire-status-board is a thin base Livewire component that renders a kanban-style board: an array of "statuses," each holding a collection of records, with SortableJS wired up so records can be dragged between columns. Each drop calls a method on your component, and persistence is entirely up to you. There is no database layer, no status model, no migration — it is a rendering and drag-event package.
Minimal path:
composer require asantibanez/livewire-status-board
php artisan vendor:publish --tag=livewire-status-board-config
php artisan vendor:publish --tag=livewire-status-board-views # only if you need to customise
Then subclass the base component and implement statuses():
use Asantibanez\LivewireStatusBoard\LivewireStatusBoard;
class TaskBoard extends LivewireStatusBoard
{
public function statuses(): array
{
return [
['id' => 'todo', 'name' => 'To Do', 'model' => Task::class,
'records' => Task::where('status', 'todo')->get()],
['id' => 'doing', 'name' => 'Doing', 'model' => Task::class,
'records' => Task::where('status', 'doing')->get()],
['id' => 'done', 'name' => 'Done', 'model' => Task::class,
'records' => Task::where('status', 'done')->get()],
];
}
public function onStatusChanged($statusId, $recordId)
{
Task::findOrFail($recordId)->update(['status' => $statusId]);
}
}
Drop it in a view with @livewire('task-board').
Where to look first, in this order:
src/LivewireStatusBoard.php) — it is small (a few hundred lines at most). Read it end-to-end before adopting; that is faster than reading the README twice.$statuses, per-status id/name/model/records) and how SortableJS is initialised. This is the file you will actually edit.Check composer.json for the Livewire constraint before committing. The current release line is 2.2.0, whose only changelog entry is "Bumped up dependencies" — i.e. no code changes to the component, but the illuminate/* / livewire/* version floors may have moved. Run composer why livewire/livewire after install and confirm it resolves against your Livewire major version (v2 vs v3 differ in event dispatch and asset directives). If it doesn't, the base class is small enough to vendor into app/ and adapt — that is a realistic fallback, not a crisis.
The first use case worth shipping: a per-project or per-user task board where the status column is a plain string or enum cast. Do not try to make this a general-purpose workflow engine on day one.
Read-only dashboards. Omit the model key entirely (or leave onStatusChanged() as a no-op) and the board becomes a status-grouped overview — useful for support queues, order pipelines, or deployment dashboards. Drag will still fire the event; just don't act on it, or drop the model key, which is what gates draggability in most versions.
Scoped boards. Pass context through mount() and keep it in public properties, then filter inside statuses():
public function mount(Project $project) { $this->projectId = $project->id; }
public function statuses(): array {
$tasks = Task::where('project_id', $this->projectId)->with('assignee')->get()->groupBy('status');
return collect(TaskStatus::cases())->map(fn ($s) => [
'id' => $s->value, 'name' => $s->label(), 'model' => Task::class,
'records' => $tasks->get($s->value, collect()),
])->all();
}
Deriving statuses from an enum keeps the board and the database in lockstep — one source of truth, no drift when you add a status.
Persist, then let the re-render do the work. Since statuses() is re-evaluated on every render, you don't need to manually refresh anything after onStatusChanged(): save the record, and the board repaints with the new groupings. This is the package's whole magic trick, and it's why statuses() must always query the database rather than returning cached state.
Custom cards. Publish the view and edit the card markup. You get the record plus the status context, so you can render avatars, due-date badges, wire:click="$emit('openTask', {{ $record->id }})", or a dropdown menu. Keep the outer wrapper element and its wire:key intact — SortableJS identifies columns by the data attributes the package sets, and removing them silently kills drag-and-drop.
Combine with a create form. Put a small wire:submit form above the board in the same parent view; after creating a task with status = 'todo', the board's next render picks it up. If the form is a separate component, use $refresh or an event listener to force the board to re-render.
Testing. It's a normal Livewire component, so Livewire::test(TaskBoard::class)->call('onStatusChanged', 'done', $task->id)->assertHasNoErrors() plus a database assertion covers the only logic you wrote. There is nothing to mock.
statuses() runs on every render, including every drag. That is N queries per drop. Eager-load relationships, add an index on the status column, and consider ->limit() per column for large tables. This is the most likely production surprise.$recordId arrives as a string from the browser. findOrFail() handles it, but strict comparisons ($record->id === $recordId) will fail silently. Cast explicitly.onStatusChanged is a public, browser-callable method. Anyone can POST an arbitrary $statusId / $recordId pair. Authorise inside the method — $this->authorize('update', $task) and validate that $statusId is a legal transition — rather than trusting the payload.ids are used as DOM identifiers and array keys. Use stable, unique, slug-safe strings. Integer-like IDs get recast by PHP when used as array keys, which produces confusing mismatches between the JS payload and your statuses() array.model key implies polymorphism, but only the record ID crosses the wire. If two models share IDs, prefix them ("task-42") or keep one model per board.position column and handle it. Don't promise users drag-to-reorder without building it.vendor/ after upgrades, or keep your overrides as small as possible.livewire/message request payload after a failed drop. It shows exactly which method was called with which parameters — that answers 90% of "the drag does nothing" reports (usually: model missing, or a view override that dropped the wrapper element)."asantibanez/livewire-status-board": "2.2.0"), run your board's Livewire test after composer update, and re-check the published view still matches vendor/ if you overrode it.app/Livewire/ if Livewire's next major breaks it.How can I help you explore Laravel packages today?