Reordering
Let users drag rows into a new order. Declare the order column in the Table class and the package stores the new order itself:
public function for(): Builder
{
return Room::query()
->orderBy('sort_order')
->orderBy('name');
}
public function configure(TableBuilder $table): void
{
$table
->column('name')
->reorderable('sort_order');
}
The component puts a drag handle in front of every row. After a drop it posts {id, to}, the keys
of the dragged row and of the row it was dropped on, to a signed package route and reloads the table.
Signature
reorderable(string $column = 'sort_order', ?string $url = null): self
| Parameter | Description |
|---|---|
column |
The integer column that holds the position |
url |
A route of your own to post to instead of the package route, see below |
How the Order Is Stored
The server takes every row of the table's for() query, without the current search and filters,
and puts them in the order the table shows them:
- the order column,
- then the other
orderByclauses of thefor()query, such asnameabove, - then the table's
defaultSort(), when that is another column, - then the primary key.
It moves the dragged row to the position of the target and numbers all rows from 1, rewriting only the rows whose position changed. Pagination does not matter: the keys are global.
Rows that share a position, such as freshly imported rows that are all 0, are therefore numbered in
the order you see them on the first move, and every move after that works on unique positions. Give
the for() query (or defaultSort()) the same secondary order the page shows, otherwise a tie is
broken by the primary key and a row can land next to a different neighbour than the one it was
dropped on.
When the Handles Show
The handles hide while the table is sorted on another column through a column header, because the order you see is then not the stored one. Sorting on the order column itself keeps them.
Filtering and searching keep the handles. A drop then moves the row in front of (or behind) the row it was dropped on, and the rows that are filtered out keep their relative order.
Authorization
The package route rebuilds the Table class from the signed url and calls authorizeReorder(), which
falls back to authorize(). Run the package routes inside your web stack with auth so those hooks
see the logged-in user:
public function authorizeReorder(Request $request): bool
{
return $request->user()->can('update', $this->venue);
}
Only call reorderable() for users who may reorder, so others do not see the handles; the route
answers 404 for a table that is not reorderable. A table with constructor arguments, like the venue
here, tells the package how to rebuild itself with signedParameters() and fromSignedParameters().
See Security.
Using Your Own Route
Point the table at a route of your own when you would rather handle the request in a controller:
$table->reorderable('sort_order', url: route('venues.rooms.reorder', $venue));
use TranquilTools\TableBuilder\Http\Requests\ReorderRequest;
public function reorder(ReorderRequest $request, Venue $venue): RedirectResponse
{
Gate::authorize('update', $venue);
(new RoomTable($request->user(), $venue))->performReorder($request->rowId(), $request->targetId());
return back();
}
ReorderRequest validates id and to. performReorder() uses the same ordering rules as the
package route and throws an InvalidArgumentException for a key outside the table's query; turn it
into a validation error if you like:
try {
$table->performReorder($request->rowId(), $request->targetId());
} catch (InvalidArgumentException $exception) {
throw ValidationException::withMessages(['id' => $exception->getMessage()]);
}
The TranquilTools\TableBuilder\Actions\ReorderRows action does the work and can be called on any
query directly. Ties are broken by the orderBy clauses already on that query:
app(ReorderRows::class)(Room::query()->orderBy('name'), 'sort_order', 'id', $id, $toId);
Reordering Without the Server
Without reorderable() in the Table class you can still show the handles with the reorderable
prop and persist the order yourself. Dropping a row emits reorder with {from, to, row}; from
and to are positions within the rows currently shown and row is the dragged row. The component
does not move anything itself: persist the new order and reload.
<script setup lang="ts">
import {useTemplateRef} from 'vue'
import {router} from '@inertiajs/vue3'
const lines = useTemplateRef('lines')
function move({row, to}: {from: number; to: number; row: any}) {
router.post(`/lines/${row.id}/move`, {position: to}, {
onSuccess: () => lines.value?.reload(),
})
}
</script>
<template>
<TableBuilder ref="lines" :table="table" reorderable @reorder="move"/>
</template>
On a paginated table add the offset of the current page yourself, and keep in mind that a sorted or filtered table shows a different order than the one you store.
reorder is emitted for server-side reordering too, before the request is sent.
Payload
The Table class sends reorder to the component:
interface ReorderConfig {
column: string // the order column
key: string // the row key used for `id` and `to`, see rowKey()
url: string | null // the signed package url or your own
}
