Skip to content

List Options ​

The options of Lists::make() decide how a list sorts, pages and links its rows, and whether visitors can check rows or choose the columns. Each example shows the method that builds the list and the YAML of its columns. Every list here except one sets customPageName: false, so the lists on the page do not share a page number. Adding a list to a page is covered in Building a List Page.

Sorting ​

Clicking a header sorts the list by that column, a second click reverses the order, and the list remembers the choice for the session. defaultSort sets the order until then, as a column name, which sorts ascending, or as an array with a column and a direction. A list without defaultSort uses the column marked sortableDefault, if there is one.

A column with sortable: false has a plain header, like the excerpt here. showSorting: false removes the sort links from every header, and the default order still applies.

php
/**
 * makeSortingList starts with the most viewed posts until a header is clicked.
 */
protected function makeSortingList(): Lists
{
    return Lists::make(
        alias: 'sortingList',
        model: new Post,
        columns: '~/resources/ui/reference/lists/options/sorting.yaml',
        recordsPerPage: 5,
        defaultSort: ['column' => 'views', 'direction' => 'desc'],
        customPageName: false,
    );
}
yaml
columns:
    title:
        label: Title

    views:
        label: Views
        type: number

    published_at:
        label: Published
        type: timetense

    excerpt:
        label: Excerpt
        type: summary
        sortable: false

Pagination ​

recordsPerPage splits the list into pages, with the record count and a link for each page. The dots between the links open a box to type a page number. A list without recordsPerPage shows every record on one page.

customPageName names the page in the address, here ?postsPage=2, so a reload or a shared link opens the same page. It is page by default, which suits a page with one list. false keeps the page out of the address and posts it with each request instead, so lists on the same page each keep their own. A new search or filter goes back to the first page.

php
/**
 * makePagedList shows five posts a page, with the page number in the address.
 */
protected function makePagedList(): Lists
{
    return Lists::make(
        alias: 'pagedList',
        model: new Post,
        columns: '~/resources/ui/reference/lists/options/posts.yaml',
        recordsPerPage: 5,
        customPageName: 'postsPage',
    );
}
yaml
columns:
    title:
        label: Title

    category:
        label: Category
        relation: category
        select: name

    status:
        label: Status
        type: selectable

    published_at:
        label: Published
        type: timetense

Simple pagination ​

showPageNumbers: false leaves out the record count and the page links, keeping only first, previous and next, so the list never counts its records. It suits large tables, where counting is slow.

showPagination turns the pager off altogether. It is auto by default, which shows the pager whenever recordsPerPage is set, and false shows every record.

php
/**
 * makeSimplePagedList pages without counting the records.
 */
protected function makeSimplePagedList(): Lists
{
    return Lists::make(
        alias: 'simplePagedList',
        model: new Post,
        columns: '~/resources/ui/reference/lists/options/posts.yaml',
        recordsPerPage: 5,
        showPageNumbers: false,
        customPageName: false,
    );
}

List setup ​

showSetup lets the visitor choose the columns, their order and the page size, and the list keeps the choice. The setup opens from the icon a toolbar adds with setupHandler, given the list's own onLoadSetup handler through getEventHandler().

In the setup, drag the columns into a new order, check the ones to show and pick a page size from perPageOptions, which is 20, 40, 80, 100 and 120 by default, with recordsPerPage added when it is missing. Reset to Default forgets the choice. Columns marked invisible start unchecked, while a hidden column is never shown, not even in the setup.

The choice is kept in the session. To keep it for good, such as per user in your database, extend the widget and override getPreferenceStorage() to return your own store: an object with get($key, $default), set($key, $value) and reset($key) methods.

php
/**
 * makeSetupList lets the visitor pick the columns and the page size.
 */
protected function makeSetupList(): Lists
{
    return Lists::make(
        alias: 'setupList',
        model: new Post,
        columns: '~/resources/ui/reference/lists/options/setup.yaml',
        recordsPerPage: 5,
        perPageOptions: [5, 10, 20],
        showSetup: true,
        customPageName: false,
    );
}

/**
 * makeSetupToolbar shows the list setup icon, which opens the setup of the given list.
 */
protected function makeSetupToolbar(Lists $list): Toolbar
{
    return Toolbar::make(
        alias: 'setupToolbar',
        setupHandler: $list->getEventHandler('onLoadSetup'),
    );
}
yaml
columns:
    id:
        label: ID
        hidden: true

    title:
        label: Title

    status:
        label: Status
        type: selectable

    views:
        label: Views
        type: number

    excerpt:
        label: Excerpt
        type: summary
        invisible: true

    review_on:
        label: Review Date
        type: date
        invisible: true

Checkboxes ​

showCheckboxes adds a checkbox to each row, and one in the header that checks or clears them all. Hold Shift to check a range of rows. Rows stay checked when the list moves to another page and back.

A toolbar bound to the list with bindToListWidget() acts on the checked rows. Its button with checked: true stays disabled until a row is checked, then sends the ids of the rows checked on the page to its handler as checked. Checked-state buttons covers the same for buttons of your own markup.

php
/**
 * makeCheckboxList adds a checkbox to each row.
 */
protected function makeCheckboxList(): Lists
{
    return Lists::make(
        alias: 'checkboxList',
        model: new Post,
        columns: '~/resources/ui/reference/lists/options/posts.yaml',
        recordsPerPage: 5,
        showCheckboxes: true,
        customPageName: false,
    );
}

/**
 * makeCheckboxToolbar has a button that sends the checked rows of the given list.
 */
protected function makeCheckboxToolbar(Lists $list): Toolbar
{
    $toolbar = Toolbar::make(
        alias: 'checkboxToolbar',
        buttons: [
            'show' => [
                'label' => 'Show checked',
                'icon' => 'ph ph-list-checks',
                'handler' => 'onShowCheckedPosts',
                'checked' => true,
                'attributes' => ['data-request-flash' => true],
            ],
        ],
    );

    $toolbar->bindToListWidget($list);

    return $toolbar;
}
php
/**
 * onShowCheckedPosts names the posts checked in the list, as sent by the toolbar button.
 */
public function onShowCheckedPosts(): AjaxResponse
{
    $titles = Post::whereIn('id', (array) input('checked'))->orderBy('title')->pluck('title');

    return ajax()->flash('info', $titles->count().' '.Str::plural('post', $titles->count()).' checked: '.$titles->implode(', ').'.');
}

Searchable columns and no records ​

A search box bound to the list looks in the columns marked searchable, including a column read from a relation and the invisible excerpt, which is searched while it is hidden. Search for "nested" to match a title, "releases" to match a category, or a word that no post has to see noRecordsMessage, which is No records found. by default. Search covers the search box itself.

php
/**
 * makeSearchableList searches the columns marked searchable.
 */
protected function makeSearchableList(): Lists
{
    return Lists::make(
        alias: 'searchableList',
        model: new Post,
        columns: '~/resources/ui/reference/lists/options/searchable.yaml',
        recordsPerPage: 5,
        noRecordsMessage: 'No posts match the search.',
        customPageName: false,
    );
}

/**
 * makeSearchableToolbar has the search box that searches the given list.
 */
protected function makeSearchableToolbar(Lists $list): Toolbar
{
    $toolbar = Toolbar::make(
        alias: 'searchableToolbar',
        search: ['prompt' => 'Search posts...'],
    );

    $toolbar->bindToListWidget($list);

    return $toolbar;
}
yaml
columns:
    title:
        label: Title
        searchable: true

    category:
        label: Category
        relation: category
        select: name
        searchable: true

    excerpt:
        label: Excerpt
        type: summary
        searchable: true
        invisible: true

    status:
        label: Status
        type: selectable

Record URL ​

recordUrl links every row to an address, where :id and any other :attribute take the record's values. The path becomes an address through Ui::url(). The whole row follows the link, except cells with clickable: false, here the Live column, and linkage cells, which keep their own link. Ctrl or Cmd and click, or a middle click, opens the row in a new tab.

php
/**
 * makeRecordUrlList links each row to the post's edit page.
 */
protected function makeRecordUrlList(): Lists
{
    return Lists::make(
        alias: 'recordUrlList',
        model: new Post,
        columns: '~/resources/ui/reference/lists/options/record-url.yaml',
        recordsPerPage: 5,
        recordUrl: 'posts/:id/edit',
        customPageName: false,
    );
}
yaml
columns:
    title:
        label: Title

    category:
        label: Category
        type: linkage
        relation: category
        select: name
        linkUrl: categories/:category_id/edit

    views:
        label: Views
        type: number

    published:
        label: Live
        type: switch
        clickable: false

Record click and popups ​

recordOnClick runs JavaScript when a row is clicked, with :id and other attributes replaced, and takes the place of recordUrl. The popup shorthand opens the controller's onLoadPopupForm handler in a popup, posting the record id as form_record_id, and popup@context also posts the context as form_context. The handler returns the popup's markup: a header, a body and a footer, often around a form. A handler in the popup that saves the record can return the list's onRefresh() to update the rows.

The shorthand calls jax.listOnLoadForm(), which the browser bundle provides. A Vite app that imports jax should also assign it to window.jax.

This list sets no recordsPerPage, so it shows every category on one page.

php
/**
 * makePopupList opens each category in a popup, showing every category on one page.
 */
protected function makePopupList(): Lists
{
    return Lists::make(
        alias: 'popupList',
        model: new Category,
        columns: '~/resources/ui/reference/lists/options/popup.yaml',
        recordOnClick: 'popup',
    );
}
php
/**
 * onLoadPopupForm shows the category of the clicked row, as the handler the popup record click opens.
 */
public function onLoadPopupForm(): string
{
    return view('reference.lists.options._category_popup', [
        'category' => Category::with('posts')->findOrFail(input('form_record_id')),
    ])->render();
}
yaml
columns:
    name:
        label: Category

    slug:
        label: Slug

    posts_count:
        label: Posts
        type: number
        relation: posts
        relationCount: true
blade
<div class="dialog-header">
    <h4 class="dialog-title">
        {{ $category->name }}
    </h4>
    <button type="button" class="btn-close" data-dismiss="popup" aria-label="Close"></button>
</div>

<div class="dialog-body">
    <p>
        {{ $category->description }}
    </p>

    @if ($category->posts->isNotEmpty())
        <ul class="mb-0">
            @foreach ($category->posts as $post)
                <li>
                    {{ $post->title }}
                </li>
            @endforeach
        </ul>
    @else
        <p class="fg-3 mb-0">
            No posts in this category.
        </p>
    @endif
</div>

<div class="dialog-footer">
    <button type="button" class="btn-solid theme-secondary" data-dismiss="popup">
        Close
    </button>
</div>

Rows by record ​

List events change single rows. Bind a listener with bindEvent() on the list, as here, or override the matching method in a subclass of the widget, such as eventInjectRowClass().

  • list.injectRowClass receives the record and returns classes for its row, here fading the archived posts.
  • list.overrideRecordAction receives the record, its address and its onclick script, and returns a new address as a string, or an array with a new url or onclick, or clickable: false to leave the row unlinked.
php
/**
 * makeRowEventsList fades the archived posts and leaves them out of the row link.
 */
protected function makeRowEventsList(): Lists
{
    $list = Lists::make(
        alias: 'rowEventsList',
        model: new Post,
        columns: '~/resources/ui/reference/lists/options/posts.yaml',
        recordsPerPage: 10,
        recordUrl: 'posts/:id/edit',
        customPageName: false,
    );

    $list->bindEvent('list.injectRowClass', function (Post $post) {
        return $post->status === 'archived' ? 'opacity-50' : null;
    });

    $list->bindEvent('list.overrideRecordAction', function (Post $post) {
        return $post->status === 'archived' ? ['clickable' => false] : null;
    });

    return $list;
}

The rowlink control, which lists use for their record links, works on any table with data-control="rowlink". Each row follows the first link inside it, whether the link goes to a page, opens a popup or runs an AJAX request, and a cell or link with the nolink class is left out. A linked row can also be reached with Tab and followed with Space.

data-target picks the links by another selector than a, data-exclude-class names another class in place of nolink, and data-linked-class sets the class added to each linked row, rowlink by default.

blade
<div class="control-list">
    <table class="table data" data-control="rowlink">
        <thead>
            <tr>
                <th>
                    Row
                </th>
                <th>
                    Clicking it
                </th>
                <th>
                    Excluded cell
                </th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <a href="{{ route('reference.lists.columns') }}">
                        Columns page
                    </a>
                </td>
                <td>
                    Follows the link
                </td>
                <td class="nolink">
                    Not linked
                </td>
            </tr>
            <tr>
                <td>
                    <a href="javascript:;" data-control="popup" data-handler="onLoadRowDialog" data-size="small">
                        Row dialog
                    </a>
                </td>
                <td>
                    Opens a popup
                </td>
                <td class="nolink">
                    Not linked
                </td>
            </tr>
            <tr>
                <td>
                    <a href="javascript:;" data-request="onRowAction" data-request-flash>
                        AJAX handler
                    </a>
                </td>
                <td>
                    Runs a request
                </td>
                <td class="nolink">
                    Not linked
                </td>
            </tr>
        </tbody>
    </table>
</div>
php
/**
 * onLoadRowDialog shows the dialog opened by the popup row of the row link table.
 */
public function onLoadRowDialog(): string
{
    return view('reference.lists.options._row_dialog')->render();
}

/**
 * onRowAction handles the row of the row link table that runs an AJAX request.
 */
public function onRowAction(): AjaxResponse
{
    return ajax()->flash('info', 'The row ran an AJAX handler.');
}

Options ​

Lists::make() takes these options as named arguments.

OptionDescription
modelthe model whose records the list shows.
columnsthe column definitions, as a YAML file path or an array. See Columns.
aliasthe widget's name, unique on the page, which prefixes its handlers, as in sortingList::onSort. Default: list.
recordsPerPagethe rows on each page, which turns pagination on. Default: every record on one page.
showPaginationshows the pager. auto shows it when recordsPerPage is set. Default: auto.
showPageNumbersshows the record count and a link for each page. false keeps first, previous and next, and skips the count. Default: true.
customPageNamethe query parameter for the page number. false keeps the page out of the address. Default: page.
defaultSortthe starting order, as a column name or an array with column and direction.
showSortinglets the visitor sort by clicking a header. Default: true.
showCheckboxesadds a checkbox to each row. Default: false.
showSetuplets the visitor choose the columns, their order and the page size. Default: false.
perPageOptionsthe page sizes offered in the list setup. Default: [20, 40, 80, 100, 120].
recordUrlthe address each row links to, with :id and other attributes replaced.
recordOnClickJavaScript run when a row is clicked, with the same replacements, or popup and popup@context to open the record in a popup.
noRecordsMessageshown when the list has no records. Default: No records found.
customViewPatha view path searched first for the list's partials, so files of your own replace them.
pivotModereads the row id and the :attribute replacements from each record's pivot, for a list of related records. Default: false.