Filter Options
Scope options set the values scopes start with, replace their default queries with SQL or model scopes, shape the options of a group and link scopes together. The scope properties and scope types list every option in one place.
The filters on this page
Most examples are built like those on Scope Types: a filter of posts with the scopes from its own YAML file, bound to a list of posts. The last two show their own methods.
/**
* makeOptionFilters builds a filter for each example whose options are all in its YAML file, bound to a list of posts.
*
* @return array<string, array{filter: Filter, list: Lists}>
*/
protected function makeOptionFilters(): array
{
$examples = [];
foreach (['defaults', 'conditions', 'group-options', 'date-options', 'names'] as $name) {
$list = $this->makeFilteredPosts(Str::camel($name).'List');
$filter = Filter::make(
alias: Str::camel($name).'Filter',
model: new Post,
scopes: "~/resources/ui/reference/filters/options/{$name}.yaml",
);
$filter->bindToListWidget($list);
$examples[$name] = ['filter' => $filter, 'list' => $list];
}
return $examples;
}
/**
* makeFilteredPosts builds a list of posts for a filter to filter.
*/
protected function makeFilteredPosts(string $alias): Lists
{
return Lists::make(
alias: $alias,
model: new Post,
columns: '~/resources/ui/reference/filters/posts.yaml',
recordsPerPage: 5,
noRecordsMessage: 'No posts match the filter.',
customPageName: false,
);
}Default values
default sets the value a scope starts with, so these posts start without the archived ones and with only the approved status. A change to a scope lasts for the session, and Clear Filters goes back to the defaults.
A checkbox takes 1 to start on, a switch 1 or 2 for its first or second state, a dropdown the value of an option and a group a list of values. The popover types take an array of their values, such as a condition and a value.
scopes:
hide_archived:
label: Hide archived
type: checkbox
default: 1
conditions: status <> 'archived'
status:
label: Status
type: dropdown
emptyOption: Any status
default: approved
options:
draft: Draft
review: In Review
approved: Approved
archived: ArchivedConditions
conditions replaces a scope's default query with SQL.
- A checkbox takes one condition, applied while it is on, and a switch takes a list of two, one for each state.
- A dropdown takes one condition, where
:valueis the picked option, and a group takes one where:valueis the picked values, quoted and separated by commas. - Number, date and text scopes take one condition for each choice they offer, keyed by its name, where
truekeeps the default query. They offer only the named choices, and with a single choice the popover shows it as a label in place of a select, like the Address scope here.
| Choice | Scope types | SQL placeholders |
|---|---|---|
| equals | number, date, text | :value, and :valueDate for a date |
| notEquals | date | :value, :valueDate |
| between | number, date | number: :min, :max. Date: :after, :afterDate, :before, :beforeDate |
| greater | number | :value |
| lesser | number | :value |
| before | date | :value, :valueDate |
| after | date | :value, :valueDate |
| contains | text | :value |
In a date condition, :value, :after and :before hold a date and time in the app timezone, while :valueDate, :afterDate and :beforeDate hold the date alone.
scopes:
popular:
label: Popular
type: dropdown
emptyOption: Any views
conditions: views >= :value
options:
1000: 1,000 views or more
5000: 5,000 views or more
comment_count:
label: Comments
type: number
conditions:
greater: (select count(*) from comments where comments.post_id = posts.id) >= :value
lesser: (select count(*) from comments where comments.post_id = posts.id) <= :value
review_on:
label: Review Date
type: date
conditions:
before: review_on <= :valueDate
after: review_on >= :valueDate
slug:
label: Address
type: text
conditions:
contains: trueEmpty option
emptyOption on a group adds an option for the records with no value, here the posts without tags. Picked with other options, it keeps the records with any of them or with none. It widens only its own scope, so the published switch and the search box still apply. On a dropdown, emptyOption is the option that applies no filter instead.
/**
* makeEmptyOptionFilter filters by tag, offering an option for the untagged posts.
*/
protected function makeEmptyOptionFilter(Lists $list): Filter
{
$filter = Filter::make(
alias: 'emptyOptionFilter',
model: new Post,
scopes: '~/resources/ui/reference/filters/options/empty-option.yaml',
);
$filter->bindToListWidget($list);
return $filter;
}
/**
* makeEmptyOptionToolbar searches the same list as the filter.
*/
protected function makeEmptyOptionToolbar(Lists $list): Toolbar
{
$toolbar = Toolbar::make(
alias: 'emptyOptionToolbar',
search: ['prompt' => 'Search posts...'],
);
$toolbar->bindToListWidget($list);
return $toolbar;
}scopes:
published:
label: Published
type: switch
conditions:
- published = false
- published = true
tags:
label: Tags
type: group
nameFrom: name
emptyOption: UntaggedGroup options
modelClass names the model whose records are a group's options, in place of the model of the relation the scope is named after. optionsScope applies one of that model's scopes to the options, here offering only the categories that hold posts. matchMode: toggle adds a choice between including and excluding the picked options, while matchMode: exclude always excludes them.
scopes:
category:
label: Category
type: group
modelClass: App\Models\Category
nameFrom: name
optionsScope: hasPosts
matchMode: toggle/**
* scopeHasPosts limits the query to categories holding at least one post, used by the posts category filter.
*/
public function scopeHasPosts($query)
{
return $query->has('posts');
}Date picker options
minDate and maxDate limit the dates that can be picked, here to 2026, and yearRange sets how many years either side the calendar's year view offers. firstDay: 1 starts the week on Monday in place of the locale's first day, and showWeekNumber numbers the weeks. useTimezone: false compares the picked days with the stored dates as they are, in place of reading them in the viewer's timezone.
scopes:
published_at:
label: Published
type: date
minDate: '2026-01-01'
maxDate: '2026-12-31'
firstDay: 1
showWeekNumber: true
yearRange: 1
useTimezone: falseNames, labels and order
valueFrom filters a column with a name other than the scope's, here published for the live scope, and shortLabel replaces the label in the bar. Scopes show in the order of the file, which before and after change, so the status scope here comes before the views, and order sets a position as a number. cssClass adds a class to the scope.
scopes:
live:
label: Published posts only
shortLabel: Live
type: checkbox
valueFrom: published
views:
label: Views
type: number
cssClass: fst-italic
status:
label: Status
type: group
before: views
options:
draft: Draft
review: In Review
approved: Approved
archived: ArchivedModel scopes and dependent scopes
modelScope runs a model scope in place of the default query. The scope method receives the query and the filter scope, whose value holds the picked value, here filtering the users by their email domain. modelScope also takes a static method as Class::method, called with the query and the scope.
dependsOn links scopes together: when the named scope changes, this one is cleared and rendered again, so a picked name resets when the domain does. On a group with a modelClass, an optionsMethod given as a method name is called on that model with every scope and its current value, so it can narrow its options by another scope, as in $scopes->domain->value.
/**
* makeFilteredUsers builds a list of users for the dependent scopes to filter.
*/
protected function makeFilteredUsers(): Lists
{
return Lists::make(
alias: 'dependsList',
model: new User,
columns: '~/resources/ui/reference/filters/users.yaml',
recordsPerPage: 5,
noRecordsMessage: 'No users match the filter.',
customPageName: false,
);
}
/**
* makeDependsFilter filters the users by a model scope and a scope that resets when it changes.
*/
protected function makeDependsFilter(Lists $list): Filter
{
$filter = Filter::make(
alias: 'dependsFilter',
model: new User,
scopes: '~/resources/ui/reference/filters/options/depends.yaml',
);
$filter->bindToListWidget($list);
return $filter;
}scopes:
domain:
label: Email Domain
type: dropdown
emptyOption: All domains
modelScope: applyDomain
options:
example.com: example.com
example.org: example.org
example.net: example.net
name:
label: Name
type: group
optionsMethod: App\Models\User::getNameGroupOptions
dependsOn: domaincolumns:
name:
label: Name
email:
label: Email/**
* Filter users by email domain, used by the "Email Domain" scope on the Filter Options reference page.
*/
public function scopeApplyDomain($query, $scope)
{
return $query->where('email', 'like', '%@'.$scope->value);
}/**
* Options for the "Name" group filter scope.
*
* @return array<string, string>
*/
public static function getNameGroupOptions(): array
{
return static::query()->orderBy('name')->pluck('name', 'name')->all();
}Scope values
A model scope reads the scope's value from these properties.
| Scope type | Properties |
|---|---|
| checkbox | value, set while the checkbox is on. |
| switch | value: 1 or 2 for the first or second state. |
| dropdown | value: the picked option. |
| group | value: the picked values. mode: include or exclude. |
| date | condition, with value, or after and before for between. |
| number | condition, with value, or min and max for between. |
| text | condition and value. |