Skip to content

Filters

Every collection of items needs to be filtered at some point. Unless you're building a photo gallery or an ecommerce site this might be when presenting posts, users, pages, etc in the admin area. Filtering allows you to specify which columns can be filtered and then pass your model to a view cell that takes care of handling the display of the filters for you. This ensures a consistent look and functionality in the UI while providing the least amount of work when you need to create a UI in a hurry.

Specifying Filterable Columns

You specify the available filters within your Model, using a class property named filters.

protected $filters = [
    'column_name' => [
        'title' => 'Column Title',
        'type' => 'radio',
        'options' => ['one' => 'Option 1', 'two' => 'Option 2']
    ],
    'column_two' => [
        'title' => 'Column Title',
        'options' => 'methodName'
    ]
];

The key of each array element is the name of the column that you wish to filter on. The column must be within the table for the model (see below for other options). It is then described by an array with the keys title, options, and optional key type. Title is the display title for that group of options. The options value is typically an array of keys and their display values that the column should be filtered by. The keys must match valid values within the database. Key type signifies if the filter should be composed of checkboxes (default) or radio's. If it is omitted, the default value checkbox would be assumed. In case type => radio is specified, the last option in the options array will be pre-selected in the filter UI, so it should probably contain value representing all entries.

Sometimes you need to pull values from a config file, or another table, etc, in order to provide the list of options. In this case, you can use the string methodName where methodName is a method within the model to call that returns the list of options.

Implementing the Filters

If all the columns match directly to columns within the model's database table, then you can make use of the Filterable trait, which implements a basic filter() method for you.

Since model's can get crowded with test data, filter data, custom methods, events, and more, you can split out the filter functionality into a new class that extends the model, placing your custom filter logic within that class.

use App\UserModel;
use Bonfire\Traits\Filterable;

class UserFilter extends UserModel
{
    use Filterable;

    public $filters = [
        ...
    ];

    /**
     * A computed options function
     */
    protected function computeFilterA()
    {
        //
    }
}

Complex Filter Columns

There are many times that you'll need to show options that don't directly relate to the model at hand. In this case you must override the filter() method on the model. Within that method you must handle all of the cases for the options you provided manually, returning the query object.

public function filter(array $params)
{
    $query = $this->builder();

    if (isset($params['foo']) && ! empty($params['foo'])) {
        $query->whereIn('foo', $params['foo']);
    }

    return $query;
}

Displaying Filters in the Admin

There are 4 parts that are needed to get a working, filterable list of resources in the admin area.

The Route

The main route for your resource list page must support both GET and POST methods.

$routes->match(['GET', 'POST'], 'users', 'UserController::list', ['as' => 'user-list']);

The controller should then choose between sending either a full HTML page for a regular GET request, or just sending back the table for ajax requests issued via HTMX. You might do it something like:

public function list()
{
    $userModel = model('UserFilter');

    // Performs the actual filtering of the results.
    $userModel->filter($this->request->getPost('filters'));

    $view = $this->request->hasHeader('HX-Request')
        ? $this->viewPrefix .'_table'
        : $this->viewPrefix .'list';

    return $this->render($view, [
        'headers' => [
            'email' => 'Email',
            'name' => 'Name',
            'groups' => 'Groups',
            'last_login' => 'Last Login'
        ],
        'showSelectAll' => true,
        'users' => $userModel->paginate(setting('Site.perPage')),
        'pager' => $userModel->pager,
    ]);
}

The View

Within your view there are 3 things to do.

First, ensure that the table with your resource list is wrapped in a div that uses some Alpine.js to attach a filtered status of false to the div. This creates a variable that determines whether the filters list is shown or not.

Second, insert the x-filter-link component into the top of that div. This provides the link that will show/hide the list of filters. This component toggles the filtered value above between true and false.

Finally, call the renderList view cell which inserts the HTML for the list of options, all wrapped in a form that calls your controller method via AJAX and updates the list of items without a page refresh, using some htmx magic. When calling this, you must supply the ID of the div that surrounds the table of results as the target.

Here's how this is used for the list of users within Bonfire.

<div x-data="{filtered: false}">
    <x-filter-link />

    <div class="row">
        <!-- List Users -->
        <div class="col order-2 order-md-1" id="content-list">
            <?= $this->include('Bonfire\Users\Views\_table') ?>
        </div>

        <!-- Filters -->
        <div class="col-auto order-1 order-md-2" x-show="filtered" x-transition.duration.240ms>
            <?= view_cell('Bonfire\Libraries\Cells\Filters::renderList 'model=UserFilter target=#content-list') ?>
        </div>
    </div>
</div>

Note that the id of the eleent that contains the included table should be with id="content-list" as it this id is the default target for content replacements by the pager and the filter.