List Structure
The ListStructure widget is a list that shows parent and child records as a tree and reorders them by dragging. It takes every list option, adds its own, and saves each move to the model: a nested set moves the node, and a sortable model stores the new order. It shows every record at once, without pages, until a header sorts it.
Tree
The categories here use the NestedTree trait of October Rain, which stores them as a nested set. Drag a row up or down to reorder it, or to the right to nest it under the row above, down to maxDepth levels. The arrows expand and collapse a branch, and the list remembers each branch for the session.
Sorting by a header or searching shows a flat list, and the icon at the end of the header, or a third click on the same header, brings the tree back. The toolbar's search box is bound to the tree the same way as to any list, and looks in its searchable columns.
/**
* makeTreeList shows the categories as a tree that drags three levels deep at most.
*/
protected function makeTreeList(): ListStructure
{
return ListStructure::make(
alias: 'treeList',
model: new Category,
columns: '~/resources/ui/reference/lists/list-structure/categories.yaml',
maxDepth: 3,
);
}
/**
* makeTreeToolbar has the search box that searches the given tree.
*/
protected function makeTreeToolbar(ListStructure $list): Toolbar
{
$toolbar = Toolbar::make(
alias: 'treeToolbar',
search: ['prompt' => 'Search categories...'],
);
$toolbar->bindToListWidget($list);
return $toolbar;
}columns:
name:
label: Category
searchable: true
slug:
label: Slug
searchable: true
posts_count:
label: Posts
type: number
relation: posts
relationCount: true/**
* Category is a post category stored as a nested set tree, so the list structure can display, nest and reorder it.
*/
#[Fillable(['name', 'slug', 'description', 'parent'])]
class Category extends Model
{
/** @use HasFactory<CategoryFactory> */
use HasFactory;
use NestedTree;
use Validation;Browsing tree
treeExpanded: false starts with every branch closed, and showReorder: false removes the drag handles, leaving a tree to browse. permissions does the same for visitors without one of the named gate abilities: they see no handles, and a reorder request from them is refused.
/**
* makeBrowseTree starts with the branches closed and offers no reordering.
*/
protected function makeBrowseTree(): ListStructure
{
return ListStructure::make(
alias: 'browseTree',
model: new Category,
columns: '~/resources/ui/reference/lists/list-structure/categories.yaml',
treeExpanded: false,
showReorder: false,
);
}Reorderable list
showTree: false reorders a flat list, for a model that implements October\Contracts\Database\SortableInterface, like the users here, which are a plain Eloquent model. After a drop, the list posts the ids of every row in their new order as sort_orders, and calls the model's setSortableOrder() with them. Until a header sorts the list, the rows are ordered by the sortOrderColumn, sort_order by default.
dragRow: false drags a row by its handle only, so the rows can still be selected and clicked.
/**
* makeReorderList shows the users as a flat list, dragged by the handle into a new order.
*/
protected function makeReorderList(): ListStructure
{
return ListStructure::make(
alias: 'reorderList',
model: new User,
columns: '~/resources/ui/reference/lists/list-structure/users.yaml',
showTree: false,
dragRow: false,
);
}columns:
name:
label: Name
email:
label: Email
sort_order:
label: Order
type: number/**
* Persist a new record ordering. Called by the ListStructure widget when
* rows are dragged into a new order.
*
* @param array<int, int|string> $itemIds
* @param array<int, int>|null $itemOrders
*/
public function setSortableOrder($itemIds, $itemOrders = null): void
{
if (! is_array($itemIds)) {
return;
}
foreach (array_values($itemIds) as $index => $id) {
static::withoutEvents(function () use ($id, $index) {
static::query()->whereKey($id)->update(['sort_order' => $index + 1]);
});
}
}Model requirements
- A tree, the default, needs a model that implements
October\Contracts\Database\TreeInterface, such as an October Rain model with theNestedTreeorSimpleTreetrait. Any other model throws an error that suggestsshowTree: false. Plain Eloquent models have no tree support of their own, so a tree needs an October Rain model. - A move is saved in the first way the model supports. A nested set moves the node next to its new neighbour or under its new parent. Otherwise, a model with a
parentrelation gets its new parent, and a sortable model gets its new order throughsetSortableOrder(). A model with none of these shows the drag handles but keeps its order. - A flat reorderable list works on any model that implements
SortableInterface: an October Rain model with theSortabletrait, or a plain Eloquent model with asort_ordercolumn and its ownsetSortableOrder(), like the users above.
Structure and sorting
The structure and column sorting do not mix. Clicking a sortable header hides the tree and sorts a flat list, with pages when recordsPerPage is set, and a third click on the same header brings the structure back, as does the icon at the end of the header. Searching also shows flat results until the search is cleared. A list with a defaultSort starts sorted and flat, until the visitor brings the structure back.
Options
ListStructure::make() takes every list option and these.
| Option | Description |
|---|---|
| showTree | shows the parent and child records as a tree. Default: true. |
| treeExpanded | starts with every branch open. The state of each branch is kept in the session. Default: true. |
| showReorder | shows a drag handle on each row. Default: true. |
| maxDepth | the most levels a row can be nested by dragging. Default: no limit, or 1 with showTree: false. |
| dragRow | lets the whole row drag, not only its handle. Default: true. |
| permissions | gate abilities needed to reorder, as a string or an array where any one is enough. Without them the handles are hidden. |
| sortOrderColumn | the column that orders a flat list while no header sorts it. Default: sort_order. |
| includeSortOrders | posts sort_orders with every move, which a sortable model always does. Default: false. |
| includeReferencePool | shares out the current sort orders of the moved rows, in place of numbering them from 1, for reordering a subset such as one page. Default: false. |
| expandLastColumn | while the list is flat, stretches the last column over the empty cell kept for the handles. Default: false. |