Skip to content

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.

php
/**
 * 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;
}
yaml
columns:
    name:
        label: Category
        searchable: true

    slug:
        label: Slug
        searchable: true

    posts_count:
        label: Posts
        type: number
        relation: posts
        relationCount: true
php
/**
 * 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.

php
/**
 * 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.

php
/**
 * 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,
    );
}
yaml
columns:
    name:
        label: Name

    email:
        label: Email

    sort_order:
        label: Order
        type: number
php
/**
 * 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 the NestedTree or SimpleTree trait. Any other model throws an error that suggests showTree: 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 parent relation gets its new parent, and a sortable model gets its new order through setSortableOrder(). 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 the Sortable trait, or a plain Eloquent model with a sort_order column and its own setSortableOrder(), 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.

OptionDescription
showTreeshows the parent and child records as a tree. Default: true.
treeExpandedstarts with every branch open. The state of each branch is kept in the session. Default: true.
showReordershows a drag handle on each row. Default: true.
maxDepththe most levels a row can be nested by dragging. Default: no limit, or 1 with showTree: false.
dragRowlets the whole row drag, not only its handle. Default: true.
permissionsgate abilities needed to reorder, as a string or an array where any one is enough. Without them the handles are hidden.
sortOrderColumnthe column that orders a flat list while no header sorts it. Default: sort_order.
includeSortOrdersposts sort_orders with every move, which a sortable model always does. Default: false.
includeReferencePoolshares 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.
expandLastColumnwhile the list is flat, stretches the last column over the empty cell kept for the handles. Default: false.