Skip to content

Building a List Page ​

This guide builds the index pages of the demo app's Showcase. The Posts page binds a list, a filter and a toolbar together, with bulk actions on the checked rows and a quick post popup. The Groups page edits its rows in a popup, and the Categories page shows a tree that you can reorder by dragging. Every snippet is the demo's own code, so you can run the demo app and try each page as you read.

Rendering a list ​

A list starts with a YAML file of columns. The posts list has a column for each attribute it shows, several of them related records:

yaml
# ===================================
#  Post List Column Definitions
# ===================================
#
# Columns marked invisible start hidden and can be shown from the list setup.

columns:
    id:
        label: ID
        sortable: true
        invisible: true
        width: 70px

    featured_image:
        label: Image
        type: image
        sortable: false

    title:
        label: Title
        searchable: true
        sortable: true

    author:
        label: Author
        type: linkage
        relation: author
        select: name
        linkUrl: authors/:author_id/edit
        sortable: true

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

    tags:
        label: Tags
        type: partial
        path: ~/resources/views/posts/_column_tags.php
        relationWith: tags
        sortable: false
        tooltip:
            title: Up to three tags show here, open the post to see all of them.
            icon: ph ph-info

    status:
        label: Status
        type: selectable
        sortable: true
        width: 140px

    published:
        label: Live
        type: switch
        sortable: true
        width: 80px
        options:
            0: Not published
            1: Published

    published_at:
        label: Published
        type: timetense
        sortable: true

    comments_count:
        label: Comments
        type: number
        relation: comments
        relationCount: true
        sortable: true
        width: 110px

    views:
        label: Views
        type: number
        sortable: true
        invisible: true

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

    created_at:
        label: Created
        type: timesince
        sortable: true
        invisible: true

Each key names an attribute, and its definition sets the header, the column type and what the column allows. sortable adds a sorting link to the header, searchable includes the column in a search, and invisible hides the column until someone shows it from the list setup. The columns page covers every column type.

The posts controller's index action returns view('posts.index', $this->prepareListVars()), and the PostList trait builds the list with Lists::make():

php
/**
 * makeListWidget builds the paginated list of posts, newest first, with a setup popup for columns and page size.
 */
protected function makeListWidget(): Lists
{
    return Lists::make(
        model: new Post,
        columns: '~/resources/ui/post/columns.yaml',
        recordsPerPage: 10,
        perPageOptions: [10, 20, 50],
        showCheckboxes: true,
        showSetup: true,
        recordUrl: 'posts/:id/edit',
        defaultSort: ['column' => 'published_at', 'direction' => 'desc'],
        noRecordsMessage: 'No posts match the search and filters.',
    );
}

model is an empty instance of the model to list, and columns the YAML file (a path starting with ~/ resolves from the application root, and an array of definitions works too). The other options set the page size and the sizes offered in the list setup, add checkboxes and the setup popup, link each row to its post, and sort the newest posts first. Every option is listed under Widget configuration, and the list options page shows each one live.

The view prints the widgets. Widgets are Htmlable, so Blade's renders their markup as it is:

blade
@extends('layouts.app')
@section('title', 'Posts')
@section('content')

<h1 class="mb-3">
    Posts
</h1>
<p class="fg-3 mb-7">
    Archived posts are filtered out by default. Check rows to run a bulk action, press Shift+Q for a quick post, and use the list setup icon beside the search to choose the columns and page size.
    <a href="{{ config('reference.docs_url') }}/guides/lists" target="_blank" rel="noopener">
        How this page is built
    </a>
</p>

{{ $toolbar }}

{{ $filter }}

{{ $list }}

@endsection

Sorting, paging, searching and filtering post back to the page's own address, where the list's handlers (list::onSort, list::onPaginate and so on) answer them. The route must accept POST as well as GET, as shown in Routes.

Binding a toolbar and a filter ​

prepareListVars() builds the three widgets of the page and binds the filter and the toolbar to the list:

php
/**
 * prepareListVars builds the list, filter and toolbar, bound together, for the index view.
 *
 * @return array{list: Lists, filter: Filter, toolbar: Toolbar}
 */
protected function prepareListVars(): array
{
    $list = $this->listWidget = $this->makeListWidget();

    $filter = $this->makeFilterWidget();
    $filter->bindToListWidget($list);

    $toolbar = $this->makeToolbarWidget($list);

    // The top bar search submits here as ?q=
    if (request()->filled('q')) {
        $toolbar->getSearchWidget()->setActiveTerm(request('q'));
    }

    $toolbar->bindToListWidget($list);

    return [
        'list' => $list,
        'filter' => $filter,
        'toolbar' => $toolbar,
    ];
}
  • $filter->bindToListWidget($list) adds the filter's scopes to the list's query and shares the list's page parameter. When a scope changes, the filter returns the list refreshed from page one in the same response.
  • $toolbar->bindToListWidget($list) links the toolbar's buttons to the list's checkboxes, passes the search box's mode and scope to the list, applies the current search term, and refreshes the list when a new term is entered.

The list is built first, since the others bind to it, and kept in the public $listWidget property so the bulk action and popup handlers can refresh it. The demo's top bar has a search box of its own that submits to this page as ?q=, and setActiveTerm() hands that term to the toolbar's search before the binding applies it.

The filter and the toolbar are built in the same trait:

php
/**
 * makeFilterWidget builds the filter scopes for the posts list.
 */
protected function makeFilterWidget(): Filter
{
    return Filter::make(
        model: new Post,
        scopes: '~/resources/ui/post/scopes.yaml',
    );
}

/**
 * makeToolbarWidget builds the toolbar from a partial, with a search that runs through a model scope and the list setup icon.
 */
protected function makeToolbarWidget(Lists $list): Toolbar
{
    return Toolbar::make(
        buttons: '~/resources/views/posts/_list_toolbar.php',
        search: [
            'prompt' => 'Search posts...',
            'scope' => 'applySearch',
        ],
        setupHandler: $list->getEventHandler('onLoadSetup'),
    );
}

Filter::make() takes the model and a YAML file of scopes. Each scope constrains the list's query, and the values persist in the session, so the filter survives a reload. The posts filter has a switch, group scopes for the category, author, tags and status, a checkbox that hides archived posts by default, and a date range:

yaml
# ===================================
#  Post Filter Scope Definitions
# ===================================

scopes:
    published:
        label: Published
        type: switch
        conditions:
            - published = false
            - published = true

    category:
        label: Category
        type: group
        modelClass: App\Models\Category
        nameFrom: name
        optionsScope: hasPosts

    author:
        label: Author
        type: group
        modelClass: App\Models\User
        nameFrom: name
        optionsScope: hasPosts

    tags:
        label: Tags
        type: group
        nameFrom: name
        emptyOption: Untagged

    status:
        label: Status
        type: group
        optionsMethod: 'App\Models\Post::listStatusOptions'

    hide_archived:
        label: Hide archived
        type: checkbox
        default: 1
        conditions: status <> 'archived'

    published_at:
        label: Published
        type: daterange

The scope types and filter options pages cover every scope.

Toolbar::make() takes the buttons, the search box and the list setup icon:

  • buttons is a YAML file of button definitions, or a partial of Ui buttons as here. Either renders inside the toolbar, linked to the list.
  • search adds the search box. prompt is its placeholder, and scope names a model scope that runs the search instead of the searchable columns, here applySearch, which also matches the category, tag and author names (see search with a scope).
  • setupHandler shows the icon that opens the list setup. $list->getEventHandler('onLoadSetup') returns list::onLoadSetup, the list's own handler.

The posts toolbar is a partial, which lets it hold a popup button and a dropdown:

php
<div data-control="toolbar">
    <?= Ui::button(
        label: 'New Post',
        href: route('posts.create'),
        icon: 'ph ph-plus',
        primary: true
    ) ?>

    <?= Ui::popupButton(
        label: 'Quick Post',
        handler: 'onLoadQuickPost',
        icon: 'ph ph-lightning',
        hotkey: 'shift+q'
    ) ?>

    <?php Ui::dropdownButton(
        label: 'Bulk Actions',
        icon: 'ph ph-stack',
        dataListCheckedTrigger: true
    )->slot() ?>
        <?= Ui::dropdownItem(
            label: 'Publish',
            handler: 'onBulkPublish',
            icon: 'ph ph-eye',
            dataListCheckedRequest: true,
            dataRequestFlash: true
        ) ?>
        <?= Ui::dropdownItem(
            label: 'Unpublish',
            handler: 'onBulkUnpublish',
            icon: 'ph ph-eye-slash',
            dataListCheckedRequest: true,
            dataRequestFlash: true
        ) ?>
        <?= Ui::dropdownDivider() ?>
        <?= Ui::dropdownItem(
            label: 'Delete',
            handler: 'onBulkDelete',
            icon: 'ph ph-trash',
            dataListCheckedRequest: true,
            dataRequestFlash: true,
            dataRequestConfirm: 'Delete the checked posts?'
        ) ?>
    <?php Ui::end() ?>

    <?= Ui::button(
        label: 'Categories',
        href: route('categories.index'),
        icon: 'ph ph-folders'
    ) ?>
</div>

The Bulk Actions dropdown is disabled until a row is checked (dataListCheckedTrigger), and each item sends the ids of the checked rows with its request (dataListCheckedRequest). The Authors page declares its buttons in YAML instead, where checked: true does both for its Delete button:

yaml
# ===================================
#  Author Toolbar Button Definitions
# ===================================

buttons:
    create:
        label: New Author
        icon: ph ph-plus
        type: primary
        url: authors/create

    delete:
        label: Delete
        icon: action.delete
        type: danger
        handler: onDelete
        confirm: Delete the selected authors? Their posts stay, without an author.
        checked: true

See buttons from YAML and buttons from a partial for the full set of button options.

Bulk actions on checked rows ​

The bulk action handlers read the checked ids, change those posts, and return the refreshed list with a flash message:

php
/**
 * onBulkPublish publishes the checked posts, stamping a publish date on those without one.
 */
public function onBulkPublish(): AjaxResponse
{
    $posts = Post::whereIn('id', $this->getCheckedPostIds());

    $count = (clone $posts)->update(['published' => true]);
    (clone $posts)->whereNull('published_at')->update(['published_at' => now()]);

    return ajax()
        ->flash('success', $count.' '.Str::plural('post', $count).' published.')
        ->update($this->listWidget->onRefresh());
}

/**
 * onBulkUnpublish takes the checked posts offline and keeps their publish dates.
 */
public function onBulkUnpublish(): AjaxResponse
{
    $count = Post::whereIn('id', $this->getCheckedPostIds())->update(['published' => false]);

    return ajax()
        ->flash('success', $count.' '.Str::plural('post', $count).' unpublished.')
        ->update($this->listWidget->onRefresh());
}

/**
 * onBulkDelete deletes the checked posts one by one, so their comments, tags and images go with them.
 */
public function onBulkDelete(): AjaxResponse
{
    $posts = Post::whereIn('id', $this->getCheckedPostIds())->get();

    $posts->each->delete();

    return ajax()
        ->flash('success', $posts->count().' '.Str::plural('post', $posts->count()).' deleted.')
        ->update($this->listWidget->onRefresh());
}

/**
 * getCheckedPostIds returns the ids of the checked rows posted by a bulk action.
 *
 * @return array<int, int>
 */
protected function getCheckedPostIds(): array
{
    return array_map('intval', array_filter((array) input('checked'), 'is_numeric'));
}

The checked ids arrive as checked. $this->listWidget->getAllCheckedIds() reads them too, adding rows checked on other pages of the list. $this->listWidget->onRefresh() returns the list's markup keyed by its element id, which ajax()->update() sends to the browser. The property holds the list because Larajax runs the index action before the handler, so prepareListVars() has already built it.

Record URLs ​

recordUrl: 'posts/:id/edit' makes each row a link to its post. :id, and any other attribute name after a colon, is replaced with the record's value, and the result becomes an address through Ui::url(), which you can change for your application. Clicking anywhere on a row follows the link, except cells that link on their own. The author and category columns are linkage columns with their own linkUrl, so they open the author or the category instead.

recordOnClick runs JavaScript on a row click in place of a link, with the same :id replacement. Its popup value opens the record in a popup, as the Groups page does next. See record URL and record click.

The Groups page edits a group in a popup when you click its row. The list sets recordOnClick: 'popup':

php
<?php

namespace App\Controllers\GroupsController;

use App\Models\UserGroup;
use Larajax\Ui\Widgets\Lists;
use Larajax\Ui\Widgets\Toolbar;

/**
 * GroupList builds the groups list and its toolbar.
 */
trait GroupList
{
    /**
     * @var Lists|null listWidget is kept on the instance so the popup save handler can refresh it.
     */
    public ?Lists $listWidget = null;

    /**
     * prepareListVars builds the list and its toolbar, bound together, for the index view.
     *
     * @return array{list: Lists, toolbar: Toolbar}
     */
    protected function prepareListVars(): array
    {
        $list = $this->listWidget = $this->makeListWidget();

        $toolbar = $this->makeToolbarWidget();
        $toolbar->bindToListWidget($list);

        return [
            'list' => $list,
            'toolbar' => $toolbar,
        ];
    }

    /**
     * makeListWidget builds the list of groups with their member counts, where clicking a row edits the group in a popup.
     */
    protected function makeListWidget(): Lists
    {
        return Lists::make(
            model: new UserGroup,
            columns: '~/resources/ui/user_group/columns.yaml',
            recordsPerPage: 10,
            recordOnClick: 'popup',
            defaultSort: ['column' => 'name', 'direction' => 'asc'],
        );
    }

    /**
     * makeToolbarWidget builds the list toolbar with its buttons and search.
     */
    protected function makeToolbarWidget(): Toolbar
    {
        return Toolbar::make(
            buttons: '~/resources/views/groups/_list_toolbar.php',
            search: ['prompt' => 'Search groups...'],
        );
    }
}

A row click calls the controller's onLoadPopupForm handler, posting the record's id as form_record_id (and form_context with popup@context). The handler returns the popup's markup, and the popup's form posts back to onSavePopupForm:

php
/**
 * onLoadPopupForm renders a group's form in a popup, as the AJAX handler for the list's popup record click.
 */
public function onLoadPopupForm(): string
{
    $group = UserGroup::findOrFail(input('form_record_id'));

    $this->formWidget = $this->makeFormWidget($group, 'update');

    return view('groups._popup_form', [
        'group' => $group,
        'formWidget' => $this->formWidget,
    ])->render();
}

/**
 * onSavePopupForm saves a group from the popup form and refreshes the list.
 */
public function onSavePopupForm(): AjaxResponse
{
    $group = UserGroup::findOrFail(input('form_record_id'));

    $this->formWidget = $this->makeFormWidget($group, 'update');

    $this->saveForm($group);

    return ajax()
        ->flash('success', 'Group updated.')
        ->update($this->listWidget->onRefresh());
}

Both handlers find the group by the posted id and build the same form. The popup's markup is a form with a dialog header, body and footer around the form widget:

blade
<form data-request="onSavePopupForm" data-request-flash data-popup-load-indicator>
    <input type="hidden" name="form_record_id" value="{{ $group->id }}" />

    <div class="dialog-header">
        <h4 class="dialog-title">
            Edit Group
        </h4>
        <button type="button" class="btn-close" data-dismiss="popup" aria-label="Close"></button>
    </div>

    <div class="dialog-body">
        {{ $formWidget }}
    </div>

    <div class="dialog-footer">
        <button type="submit" class="btn-solid theme-primary">
            Save
        </button>
        <button type="button" class="btn-solid theme-secondary" data-dismiss="popup">
            Cancel
        </button>
    </div>
</form>

The hidden form_record_id input sends the id with the save. data-popup-load-indicator shows a loading state while the request runs and closes the popup once it succeeds, and the handler's $this->listWidget->onRefresh() updates the row behind it. The popup shorthand calls the global jax object that the Larajax bundle provides, so an application that imports jax through Vite should also assign it to window.jax. The popups page covers the dialog markup and the other ways to open a popup.

A quick create popup ​

The Posts toolbar's Quick Post button, or Shift+Q, opens a short form for a new post without leaving the list. Ui::popupButton() in the toolbar partial above names the handler that renders the popup, and the QuickPostForm trait holds it:

php
<?php

namespace App\Controllers\PostsController;

use App\Models\Post;
use Larajax\Classes\AjaxResponse;
use Larajax\Ui\Widgets\Form;

/**
 * QuickPostForm builds the short form shown in the quick post popup and creates a post from it.
 */
trait QuickPostForm
{
    /**
     * onLoadQuickPost renders the quick post form in a popup, as the AJAX handler for the toolbar popup button.
     */
    public function onLoadQuickPost(): string
    {
        $this->formWidget = $this->makeQuickPostFormWidget(new Post);

        return view('posts._quick_post_form', [
            'formWidget' => $this->formWidget,
        ])->render();
    }

    /**
     * onCreateQuickPost creates a post from the popup form and refreshes the list.
     */
    public function onCreateQuickPost(): AjaxResponse
    {
        $post = new Post;

        $this->formWidget = $this->makeQuickPostFormWidget($post);

        $post->fill($this->formWidget->getSaveData())->save();

        return ajax()
            ->flash('success', 'Post created.')
            ->update($this->listWidget->onRefresh());
    }

    /**
     * makeQuickPostFormWidget builds the quick post form, with its own alias so its fields stay apart from the main post form.
     */
    protected function makeQuickPostFormWidget(Post $post): Form
    {
        return Form::make(
            alias: 'quickPostForm',
            context: 'create',
            model: $post,
            fields: '~/resources/ui/post/fields_quick.yaml',
        );
    }
}

The quick form has its own fields:

yaml
# ===================================
#  Quick Post Form Field Definitions
# ===================================

fields:

    title:
        label: Title
        span: full
        required: true
        autoFocus: true

    slug:
        label: Slug
        span: left
        required: true
        preset:
            field: title
            type: slug

    category:
        label: Category
        type: relation
        span: right
        required: true
        emptyOption: Choose a category

    excerpt:
        label: Excerpt
        type: textarea
        size: small

    published:
        label: Publish now
        type: switch
        comment: Publishes the post straight away instead of saving a draft.

and its own popup markup, posting to onCreateQuickPost:

blade
<form data-request="onCreateQuickPost" data-request-flash data-popup-load-indicator>
    <div class="dialog-header">
        <h4 class="dialog-title">
            Quick Post
        </h4>
        <button type="button" class="btn-close" data-dismiss="popup" aria-label="Close"></button>
    </div>

    <div class="dialog-body">
        {{ $formWidget }}
    </div>

    <div class="dialog-footer">
        <button type="submit" class="btn-solid theme-primary">
            Create Post
        </button>
        <button type="button" class="btn-solid theme-secondary" data-dismiss="popup">
            Cancel
        </button>
    </div>
</form>

The form is built with the alias quickPostForm, so its inputs and handlers stay apart from those of the main post form, which uses the default alias form. Creating the post returns the refreshed list, so the new post appears behind the closing popup. The post model's rules validate the quick form the same way as the full one, see validation.

A tree of records ​

The Categories page shows the categories as a nested tree. ListStructure is a list that adds the tree and drag-and-drop reordering, and it takes every list option as well as its own:

php
<?php

namespace App\Controllers\CategoriesController;

use App\Models\Category;
use Larajax\Ui\Widgets\ListStructure;
use Larajax\Ui\Widgets\Toolbar;

/**
 * CategoryList builds the category tree and its toolbar.
 */
trait CategoryList
{
    /**
     * prepareListVars builds the tree and its toolbar, bound together, for the index view.
     *
     * @return array{list: ListStructure, toolbar: Toolbar}
     */
    protected function prepareListVars(): array
    {
        $list = $this->makeListWidget();

        $toolbar = $this->makeToolbarWidget();
        $toolbar->bindToListWidget($list);

        return [
            'list' => $list,
            'toolbar' => $toolbar,
        ];
    }

    /**
     * makeListWidget builds the nested set tree, where dragging a row nests or reorders it three levels deep at most.
     */
    protected function makeListWidget(): ListStructure
    {
        return ListStructure::make(
            model: new Category,
            columns: '~/resources/ui/category/columns.yaml',
            recordUrl: 'categories/:id/edit',
            noRecordsMessage: 'There are no categories yet.',
            showTree: true,
            treeExpanded: true,
            maxDepth: 3,
            dragRow: false,
        );
    }

    /**
     * makeToolbarWidget builds the toolbar with its buttons and search, where searching shows the matches as a flat list.
     */
    protected function makeToolbarWidget(): Toolbar
    {
        return Toolbar::make(
            buttons: '~/resources/ui/category/buttons.yaml',
            search: ['prompt' => 'Search categories...'],
        );
    }
}

showTree draws the tree with arrows to open and close each branch, treeExpanded starts with every branch open, and maxDepth stops a category from nesting more than three levels deep. With dragRow: false, a row moves by its handle only, so a click on the row still opens the category. The category is an October Rain model with the NestedTree trait, which stores the tree and the order, and dragging a row moves the category in place. Searching shows the matches as a flat list until the search is cleared, and so does sorting by a column. The list structure page shows a browsing tree and a flat reorderable list as well.

Widget configuration ​

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

OptionDescription
modelan instance of the model to list.
columnsthe column definitions, as a YAML file path or an array.
recordsPerPagethe number of rows on a page. Pagination turns on when it is set.
perPageOptionsthe page sizes offered in the list setup. Default: [20, 40, 80, 100, 120].
showCheckboxesadds a checkbox to each row and one in the header. Default: false.
showSetuplets visitors choose the columns and the page size in a setup popup, kept in the session. Default: false.
recordUrlthe link for each row, where :id and other :attribute names take the record's values.
recordOnClickJavaScript to run on a row click, with the same replacements. popup (or popup@context) opens the controller's onLoadPopupForm handler in a popup.
defaultSortthe initial sort, as a column name or ['column' => 'name', 'direction' => 'desc'].
showSortingadds sorting links to the column headers. Default: true.
noRecordsMessagethe message shown when no records match. Default: No records found.
showPaginationshows the pagination. Default: auto, on when recordsPerPage is set.
showPageNumbersshows the record count and a link for each page. Turn it off to skip counting the records on a large table. Default: true.
customPageNamethe query string name of the page number. Default: page.
expandLastColumnlets the last column take the spare width. Default: false.
aliasthe name the list's AJAX handlers are addressed by, as alias::onHandler. Default: list. Give each list on a page its own.

ListStructure::make() adds showTree, treeExpanded, showReorder, maxDepth and dragRow, see the list structure page. Filter::make() takes model, scopes, context and alias, and Toolbar::make() takes buttons, search, setupHandler and alias. Options without a named argument pass through make() by name, and Lists::makeWithConfig($config) takes a configuration array put together at runtime.

Extending the list ​

To constrain the query without subclassing, add a callback with addFilter(). It receives the query before the list reads its records, as in $list->addFilter(fn ($query) => $query->where('published', true)). A bound filter adds its scopes the same way.

Every extension point of the list is a method that fires a local event. Bind to the event after building the list, such as $list->bindEvent('list.injectRowClass', function ($record) { ... }) to add a class to some rows (see rows by record), or subclass Larajax\Ui\Widgets\Lists, override the method, and build your class with make() as before. Where a point may return something, a listener's return value is used the same way. The extension points are:

MethodEventCalled
eventExtendColumns()list.extendColumnsonce the columns are defined, to add them with addColumns() or remove them with removeColumn().
eventExtendQueryBefore($query)list.extendQueryBeforebefore the list applies its own constraints to the query.
eventExtendSearchQuery($query)list.extendSearchQueryinside the search conditions, to search more places.
eventExtendSortColumn($query, $column, $direction)list.extendSortColumnafter the sort is applied.
eventExtendQuery($query)list.extendQueryafter the list's constraints, and may return a replacement query.
eventExtendRecords($records)list.extendRecordsonce the records are read, and may return a replacement collection.
eventRefresh($result)list.refreshafter a refresh, with the partial updates. Return it with your own updates added.
eventOverrideRecordAction($record, $url, $onClick)list.overrideRecordActionfor each row, and may return a new URL, or an array with url, onclick or clickable.
eventOverrideHeaderValue($column, $value)list.overrideHeaderValuefor each header, and may return a new label.
eventOverrideColumnValueRaw($record, $column, $value)list.overrideColumnValueRawfor each cell before its column type formats it, and may return a new value.
eventOverrideColumnValue($record, $column, $value)list.overrideColumnValuefor each cell after formatting, and may return new markup.
eventInjectRowClass($record, $value)list.injectRowClassfor each row, and may return CSS classes for it.

A list built without columns asks the model for them instead: a defineListColumns($list) method on the model adds each column with $list->defineColumn('name', 'Label'), which returns a definition to chain the column's options on, such as ->sortable(true).

Extending the filter ​

Model hooks ​

The filter's model can take part in two ways:

  • filterScopes($scopes, $context) runs every time the filter renders and after each scope change, with each scope reachable by name on $scopes. Change a scope's properties there, such as $scopes->category->hidden = true, to adapt the filter to the record or the visitor. $context is the filter's context option.
  • defineFilterScopes($filter) supplies the scopes when the filter is built without a scopes option. Add each one with $filter->defineScope('name', 'Label'), which returns a definition to chain the scope's options on, such as ->displayAs('checkbox')->conditions('is_active = 1'). The scopes then work exactly as if they came from YAML.

Subclassing the filter ​

For changes that belong to the filter rather than the model, subclass Larajax\Ui\Widgets\Filter, override these methods, and build your class with make() as before:

MethodCalled
eventExtendScopesBefore()before the scope definitions are read.
eventExtendScopes()once the scopes are defined, to add them with addScopes() or remove them with removeScope().
eventOverrideHeaderValue($scope, $value)for each scope, and may return a new label for its button.
eventUpdate($result, $params)after a scope changes, with the partial updates. The base class adds the bound list here, so call parent::eventUpdate() and add your own updates.
eventExtendQuery($query, $scope)when a scope reads its options, to constrain the option query.

Next steps ​