Filter Widgets
A widget specifically made for use in a filter.
With filter widgets you can add new scope types to the backend filters. They provide features that are common to filtering lists. Filter widgets must be registered in the plugin registration file.
Filter Widget classes reside inside the filterwidgets directory of the plugin directory. The inner directory name matches the name of the widget class written in lowercase. Widgets can supply assets and partials. An example form widget directory structure looks like this.
├── filterwidgets
| ├── discount
| | ├── partials
| | | └── _discount.php ← Partial File
| | | └── _discount_form.php
| | └── assets
| | ├── js
| | | └── discount.js ← JavaScript File
| | └── css
| | └── discount.css ← StyleSheet File
| └── Discount.php ← Widget Class
# Class Definition
The create:filterwidget
command generates a backend filter widget, view and basic asset files. The first argument specifies the author and plugin name. The second argument specifies the form widget class name.
php artisan create:filterwidget Acme.Blog Discount
The filter widget classes must extend the Backend\Classes\FilterWidgetBase
class. A registered widget can be used in the backend filter field definition file. Example form widget class definition.
namespace Backend\FilterWidgets;
use Backend\Classes\FilterWidgetBase;
class Discount extends FilterWidgetBase
{
public function render() {}
public function renderForm() {}
}
# Filter Widget Properties
Filter widgets may have properties that can be set using the filter scope configuration. Simply define the configurable properties on the class and then call the fillFromConfig
method to populate them inside the init
method definition.
class Discount extends FormWidgetBase
{
/**
* @var bool allowSearch show the search input in the dropdown
*/
public $allowSearch = false;
/**
* init the widget
*/
public function init()
{
$this->fillFromConfig([
'allowSearch',
]);
}
// ...
}
The property values then become available to set from the filter scope definition when using the widget.
discount:
label: Discount
type: discount
allowSearch: true
# Filter Widget Registration
Plugins should register filter widgets by overriding the registerFilterWidgets
method inside the plugin registration file. The method returns an array containing the widget class in the keys and widget short code as the value. Example:
public function registerFilterWidgets()
{
return [
\Backend\FilterWidgets\Discount::class => 'discount',
];
}
The short code is used when referencing the widget in the filter scope definitions and it should be a unique value to avoid conflicts with other filter fields.
# Displaying the Filter State
The main purpose of the filter widget is to apply a scope to the query of a model, which means capturing values from the user first. The render
method is used to display the initial state of the filter and the filterScope
property will contain active value along with other configured properties.
public function render()
{
$this->vars['scope'] = $this->filterScope;
$this->vars['name'] = $this->getScopeName();
$this->vars['value'] = $this->getLoadValue();
return $this->makePartial('discount');
}
At a basic level the filter widget should show a label and its current state to the user. The contents are also wrapped in an anchor that is used to display the filter form.
<a
href="javascript:;"
class="filter-scope <?= $value ? 'active' : '' ?>"
data-scope-name="<?= $name ?>"
>
<span class="filter-label"><?= e(trans($scope->label)) ?></span>
<?php if ($value): ?>
<span class="filter-setting">1</span>
<?php endif ?>
</a>
# Displaying the Filter Form
When a user clicks on the filter label, a form is displayed so that they may specify how to apply the filter. The renderForm
method is used to display the filter form and should correspond to a _discount_form.php
partial.
public function renderForm()
{
$this->vars['allowSearch'] = $this->allowSearch;
$this->vars['scope'] = $this->filterScope;
$this->vars['name'] = $this->getScopeName();
$this->vars['value'] = $this->getLoadValue();
return $this->makePartial('discount_form');
}
The contents should contain the form values and buttons to apply or clear the filter. A form HTML tag is not required and all inputs should belong to the Filter[]
input array. The most common place to store the filtered value is the value
attribute.
<div class="filter-box">
<div class="filter-facet">
<div class="facet-item is-grow">
<select name="Filter[value]" class="form-control form-control-sm custom-select <?= $allowSearch ? '' : 'select-no-search' ?>">
<option value="1" <?= $scope->value === '1' ? 'selected="selected"' : '' ?>>has a discount</option>
<option value="0" <?= $scope->value === '0' ? 'selected="selected"' : '' ?>>does not have a discount</option>
</select>
</div>
</div>
<div class="filter-buttons">
<button class="btn btn-sm btn-primary" data-filter-action="apply">
Apply
</button>
<div class="flex-grow-1"></div>
<button class="btn btn-sm btn-secondary" data-filter-action="clear">
Clear
</button>
</div>
</div>
The $value
variable will contain an array of the selected values. This array will be merged with the $scope
variable for convenience, so you can access the active value via $scope->value
. In summary, use $value
to check if a scope is applied and $scope
to access the values.
# Capturing the Filter Value
The getActiveValue
method is used to capture the filtered form values and storing them. It should return an array (or null) and use postback data for finding the values. If the clearScope
postback value exists, it means the scope wants to be cleared. You may use the hasPostValue
helper method to check if the value was found and is not an empty string.
public function getActiveValue()
{
if (post('clearScope')) {
return null;
}
if (!$this->hasPostValue('value')) {
return null;
}
return post('Filter');
}
# Applying the Scope to the Query
Once a filter value has been captured it can be applied to the query with the applyScopeToQuery
method. The value can be taken from the filterScope->value
property where the value
name comes from the filter form values.
public function applyScopeToQuery($query)
{
$hasDiscount = $this->filterScope->value;
if ($hasDiscount) {
$query->where('discount', '>', 0);
}
else {
$query->where('discount', 0);
}
}
# Working with Inline Filters
Inline filters are filters that can exist as part of the main filter interface, instead of displaying them as a popover form. Accordingly, inside the filter widget class the renderForm
method is not required and only the render
method is used to display the filter contents.
The example below shows an inline search filter with a search button. It is important to keep in mind that since the filter is inline, the input field names are shared across the main form, so the search input uses the $name
variable, instead of the generic Filter
name.
<?php
$activeValue = $scope->scopeValue !== null ? $scope->value : $scope->default;
?>
<div
class="filter-scope scope-inline"
data-scope-name="<?= $scope->scopeName ?>">
<input
placeholder="<?= e($this->getHeaderValue($scope)) ?>"
name="<?= $name ?>[value]"
value="<?= e($activeValue) ?>"
class="form-control form-control-sm" />
<button
class="btn btn-sm btn-search"
data-filter-action="apply">
<i class="icon-search"></i>
</button>
</div>
The next example shows an inline balloon selector control.
<?php
$activeValue = $scope->scopeValue !== null ? $scope->value : $scope->default;
?>
<div
data-scope-name="<?= $scope->scopeName ?>"
data-control="balloon-selector"
data-selector-allow-empty
class="filter-scope scope-inline control-balloon-selector form-control-sm">
<ul class="list-unstyled m-0">
<?php foreach ((array) $scope->options as $key => $value): ?>
<li
data-value="<?= $key ?>"
class="small <?= $key === $activeValue ? 'active' : '' ?>"
data-filter-action="apply">
<?= $value ?>
</li>
<?php endforeach ?>
</ul>
<!-- Hidden input to store the selected filter value -->
<input type="hidden" name="<?= $name ?>[value]" value="<?= $activeValue ?>">
</div>