Sortable
Column Reordering
Drag & drop column header reordering with per-user, per-table database persistence.
Drag & drop column header reordering with per-user, per-table database persistence.
Basic usage
use Livewire\Component;use NyonCode\WireTable\Table;use NyonCode\WireTable\Columns\TextColumn;use NyonCode\WireTable\Concerns\WithTable;use NyonCode\WireSortable\Concerns\WithSortable; class TaskTable extends Component{ use WithTable, WithSortable; public function table(Table $table): Table { return $table ->model(Task::class) ->columnReorderable() ->columns([ TextColumn::make('name', 'Name'), TextColumn::make('status', 'Status'), TextColumn::make('priority', 'Priority'), ]); }}
Users can drag column headers to rearrange them. The body cells reorder automatically to match.
Database persistence
Column order is stored in the reorderable_column_orders table with a unique constraint on (user_id, model_type, table_identifier). This means:
- Each user has their own column arrangement
- The arrangement is tied to both the Eloquent model class and the Livewire component class
- Multiple table components showing the same model get independent column orders
- When a user is deleted, their column orders are cascade-deleted
Storage structure
reorderable_column_orders├── user_id: 1│ ├── model_type: "App\Models\Task", table_identifier: "App\Livewire\TaskListTable"│ │ → ["status", "name", "priority"]│ ├── model_type: "App\Models\Task", table_identifier: "App\Livewire\TaskBoardTable"│ │ → ["priority", "name", "status"]│ └── model_type: "App\Models\User", table_identifier: "App\Livewire\UserTable"│ → ["email", "name", "role"]├── user_id: 2│ └── model_type: "App\Models\Task", table_identifier: "App\Livewire\TaskListTable"│ → ["priority", "status", "name"]
How it loads
On component mount (mountWithSortable), the trait:
- Gets the current user ID via
getReorderableUserId()(defaults toauth()->id()) - Gets the model type via
getReorderableModelType()(the Eloquent model class) - Gets the table identifier via
getReorderableTableIdentifier()(the Livewire component class) - Queries
ReorderableColumnOrder::getOrder($userId, $modelType, $tableIdentifier) - If found, sets
$reorderableColumnOrderwith the saved column names
How it saves
When the user drags a column header:
- Alpine.js reads the new header order from
th[data-sortable-column]attributes - Calls
$wire.reorderColumns(['status', 'name', 'priority']) - The trait validates column names against the table definition (ignores unknown columns)
- Saves via
ReorderableColumnOrder::saveOrder($userId, $modelType, $tableIdentifier, $columnOrder)
Column name validation
The trait filters incoming column names against the table definition. Only names that match a defined column are persisted. This prevents:
- Injection of arbitrary column names from the frontend
- Stale column names from breaking the table after a column is removed from the definition
When loading saved order, columns that no longer exist in the definition are silently skipped. Newly added columns (not present in the saved order) are appended at the end.
Getting ordered columns
Use getReorderableColumns() to get columns in the user's preferred order:
$columns = $this->getReorderableColumns();
This returns all defined columns sorted by the saved order. Columns not present in the saved order are appended at the end.
Resetting column order
Call resetColumnOrder() to restore the default order:
<button wire:click="resetColumnOrder"> Reset Column Order</button>
This clears both the component property and the database entry.
Combining with row reordering
Row and column reordering work independently and can be used together:
return $table ->model(Task::class) ->reorderable('position') ->columnReorderable() ->columns([...]);
Multiple tables over the same model
By default, the table identifier is the Livewire component class (static::class). This means two components like TaskListTable and TaskBoardTable that both query App\Models\Task will have independent column orders without any extra configuration.
If you need a custom identifier (e.g., a single component that renders different table configurations), override getReorderableTableIdentifier():
protected function getReorderableTableIdentifier(): string{ return static::class . ':' . $this->tableVariant;}
Custom user resolution
By default, the trait uses auth()->id() to identify the user. Override getReorderableUserId() for custom logic:
// Multi-guard authenticationprotected function getReorderableUserId(): ?int{ return auth('admin')->id();}
Custom model type
By default, the model type is resolved from $table->getQuery()->getModel(). Override getReorderableModelType() if you need a custom key:
protected function getReorderableModelType(): ?string{ return 'custom-key';}
Guests (unauthenticated users)
When getReorderableUserId() returns null, column reordering is silently disabled:
mountWithSortable()skips loading saved orderreorderColumns()is a no-opresetColumnOrder()clears only the local property
The drag & drop UI still works in the browser (via Alpine.js), but changes are not persisted.
Column Reordering Flow
- The Alpine component calls
initColumnSortable()on the first<thead tr> - Header cells are identified by
wire:click="sortTable('column')"ordata-columnattributes - Each identified cell gets a
data-sortable-columnattribute and agrabcursor - SortableJS enables horizontal dragging of
th[data-sortable-column]elements - On drag end, body
<td>cells are reordered to match the new header order - The new column name sequence is sent to
reorderColumns()via Livewire - The trait validates column names, saves to
reorderable_column_orders, and updates the local property - Non-data columns (selection, actions, drag handle) are excluded from dragging