Filters
The data table's typed filter grammar as a standalone - seventeen operators across text, number, date and select columns, composable anywhere.
Nothing filtered yet
With no active filters the bar is one trigger. Open it and you get the field list, pick a field and the same panel swaps to that field's operator and value editor - no round trip, no hook, and Escape backs out with focus returned to the trigger.
No filters applied
Name
Category
Price
In stock
Added
<.filters id="sx-filters-empty" state={%State{}} on_change="table">
<:field field={:name} label="Name" type="text" />
<:field
field={:category}
type="select"
options={[{"Hand tools", "hand"}, {"Power tools", "power"}, {"Finishing", "finishing"}]}
/>
<:field field={:price} label="Price" type="number_range" />
<:field field={:in_stock} label="In stock" type="boolean" />
<:field field={:added_on} label="Added" type="date_range" />
</.filters>
Active filters as chips
Every filter in the state renders as a chip: field, humanised operator, formatted value. between shows both bounds, in shows a truncated list, and a valueless op like is_empty shows no value at all. Click the chip body to reopen its editor pre-filled; the x removes it and moves focus to the next chip.
Active filters: Category is Power tools, Price between 50–150, Name is not empty
In stock
<.filters
id="sx-filters-chips"
state={
%State{
filters: [
%{field: :category, op: :eq, value: "power"},
%{field: :price, op: :between, value: ["50", "150"]},
%{field: :name, op: :is_not_empty, value: true}
]
}
}
on_change="table"
>
<:field field={:name} label="Name" type="text" />
<:field
field={:category}
type="select"
options={[{"Hand tools", "hand"}, {"Power tools", "power"}, {"Finishing", "finishing"}]}
/>
<:field field={:price} label="Price" type="number_range" />
<:field field={:in_stock} label="In stock" type="boolean" />
</.filters>
One State, bar and table
The composition proof. Both components read and write the same State struct, so filtering from the bar updates the table and filtering from a column header adds a chip. This example runs the free in-memory engine at render time in link mode - the whole backend is handle_params plus State.from_params/2.
Properties
| Attribute | Type | Default | Description |
|---|---|---|---|
active_filters_label
|
string |
"Active filters"
|
the chip group's accessible name, localizable |
add_filter_label
|
string |
"Add filter"
|
the add trigger's label and its panel's accessible name, localizable |
all_fields_used_label
|
string |
"Every field is already filtered"
|
shown in the add panel when no field is left to add, localizable |
apply_label
|
string |
"Apply"
|
the value editor's submit label, localizable |
class
|
any |
nil
|
extra classes on the bar's root element |
clear_filters_label
|
string |
"Clear filters"
|
the clear-all affordance's label, localizable |
filter_op_labels
|
map |
%{}
|
overrides for operator display names, e.g. %{contains: "enthält"} - same attr name and shape as data_table |
filter_options_placeholder
|
string |
"Filter options…"
|
the multi editor's option-filter placeholder (shown from 8 options up), localizable |
id*
|
string |
DOM id; every panel and chip id is derived from it | |
no_filters_label
|
string |
"No filters applied"
|
announced by the status region when nothing is filtered, localizable |
on_change
|
string |
nil
|
event mode: the event filter edits push, with the same op-shaped payloads `State.handle_op/3` already accepts (`filter` / `clear_filters`). |
path
|
string |
nil
|
link mode: the base path filter changes patch to, with the state encoded via `State.to_params/1`. Required unless `on_change` is set. |
remove_filter_label
|
string |
"Remove filter"
|
prefix for a chip's remove button accessible name, localizable |
state*
|
{:struct, PetalComponents.DataTable.State} |
the state whose `filters` this bar renders and edits | |
target
|
any |
nil
|
event mode: the phx-target (e.g. @myself) |
:field
slot
|
slot |
the filterable field registry, one entry per field |