Column Filtering
Filtering comes in two kinds. A column filter is applied to one column's cell values; a global filter is one value tested against every searchable column at once. This page is the port of TanStack Table's Column Filtering guide; the other kind is on Global Filtering.
Both kinds are applied by the same filteredRowModel stage of the
pipeline, and a row has to pass both to survive.
filteredRows : Table.State -> Table.RowModel Person
filteredRows state =
Table.filteredRowModel config state (unfilteredRows state)
Column filter state
State.columnFilters is a list, so several columns can be filtered at once.
The order is the order the filters were added, which is what
getFilterIndex reports.
type alias ColumnFilter =
{ id : String
, value : Value
}
-- State.columnFilters : List ColumnFilter
-- Table.initialState.columnFilters == []
The value is a Value, not a String, because a filter
value can be a number, a [min, max] range (Value.List), or a set of choices.
A text input produces Value.String; a range slider produces
Value.List [ Value.Number low, Value.Number high ].
Config options
| Option | Type | Default | Description |
|---|---|---|---|
enableFilters |
Bool |
True |
Both column and global filtering. False switches off both. |
enableColumnFilters |
Bool |
True |
Column filtering for every column. |
filterFromLeafRows |
Bool |
False |
False filters parents first and drops a parent's whole subtree with it. True filters from the leaves up, keeping a parent whose descendant matches. |
maxLeafRowFilterDepth |
Int |
100 |
How deep filtering reaches. 0 filters only root rows and leaves every sub-row alone. |
manualFiltering |
Bool |
False |
True makes Table.filteredRowModel return its input untouched. |
Manual server-side filtering
Set manualFiltering = True when the rows arriving from the server are already
filtered. The filtering stage then passes the row model straight through while
the filter state, the transitions, and the query functions all keep working, so
your filter inputs are unchanged. Read State.columnFilters and send it with
your request. See Client-Side vs Server-Side.
Filtering sub-rows
filterFromLeafRows and maxLeafRowFilterDepth only matter for a table with
sub-rows (see Expanding and Grouping).
By default a parent row that fails a filter takes its children with it, which
is the cheapest option and the right one when the user is searching top-level
rows. filterFromLeafRows = True reverses that: a parent stays as long as one
of its descendants matches.
maxLeafRowFilterDepth = 0 keeps a matching parent's sub-rows even though they
do not match on their own.
leafFilteringConfig : Table.Config Person
leafFilteringConfig =
let
base : Table.Config Person
base =
Table.config columns
in
{ base
| filterFromLeafRows = True
, maxLeafRowFilterDepth = 1
}
Column options
| Builder | Type | Default | Description |
|---|---|---|---|
withFilterFn |
FilterFn -> Column row -> Column row |
Nothing |
The comparison between this column's cell value and the filter value. |
withCustomFilter |
(Row row -> Value -> Bool) -> Column row -> Column row |
Nothing |
A predicate on the whole row, used instead of a FilterFn. |
withEnableColumnFilter |
Bool -> Column row -> Column row |
True |
Column filtering for this column. |
A column with no accessor can never be filtered, whatever these say.
columns : List (Table.Column Person)
columns =
[ Table.column "firstName" (.firstName >> Value.String)
|> Table.withHeader "First name"
|> Table.withFilterFn FilterFn.includesString
, Table.column "department" (.department >> Value.String)
|> Table.withHeader "Department"
|> Table.withFilterFn FilterFn.equalsString
, Table.column "age" (.age >> toFloat >> Value.Number)
|> Table.withHeader "Age"
|> Table.withFilterFn FilterFn.inNumberRange
, Table.column "tags" (.tags >> List.map Value.String >> Value.List)
|> Table.withHeader "Tags"
|> Table.withFilterFn FilterFn.arrIncludesSome
, Table.column "id" (.id >> Value.String)
|> Table.withHeader "ID"
|> Table.withEnableColumnFilter False
]
Note that getCanFilter answering False does not stop an entry that is
already in State.columnFilters from being applied. The filtered row model
applies every entry whose column exists, matching TanStack.
Filter functions
A Table.FilterFn.FilterFn is four things, ported from the record TanStack's
constructFilterFn builds:
| Part | Signature | When it runs |
|---|---|---|
filter |
Value -> Value -> Bool |
Once per row: the cell value against the filter value. |
resolveFilterValue |
Value -> Value |
Once per filter, before any row is tested. This is where a search string is lowercased and a range is normalised, so the work is not repeated per row. |
resolveDataValue |
Value -> Value |
Once per row, on the cell value, before filter sees it. |
autoRemove |
Value -> Bool |
On write: True means the filter value is blank enough that the filter should be dropped from state instead of stored. |
Table.FilterFn.filter, resolveFilterValue, resolveDataValue, and
autoRemove read those four parts back out of a FilterFn. Note that
Table.FilterFn.filter applies resolveDataValue to the cell value but
expects the filter value to already be resolved, because the row model resolves
it once for the whole pass.
These are the built-ins, grouped the way Table.FilterFn groups them. The
column marked filter value is the shape the Value in State.columnFilters
should have.
TanStack filterFns |
Table.FilterFn |
Filter value |
|---|---|---|
| Basic | ||
equals |
equals |
Any Value; compared with Elm's ==. |
weakEquals |
weakEquals |
Any Value; compared with JavaScript's == rules, so Value.String "1" matches Value.Number 1. |
| Strings | ||
includesString |
includesString |
A String, lowercased once. Case-insensitive substring match. |
includesStringSensitive |
includesStringSensitive |
A String. Case-sensitive substring match. |
equalsString |
equalsString |
A String, lowercased once. Case-insensitive whole-string match. |
equalsStringSensitive |
equalsStringSensitive |
A String. Case-sensitive whole-string match. |
startsWith |
startsWith |
A String, lowercased once. |
endsWith |
endsWith |
A String, lowercased once. |
| Blank | ||
empty |
empty |
An on/off flag; the value itself is not compared. Keeps rows whose cell is Null or whitespace only. |
notEmpty |
notEmpty |
An on/off flag. Keeps rows whose cell is not blank. |
| Numbers | ||
greaterThan |
greaterThan |
A Number, or a String when either side does not parse as a number. |
greaterThanOrEqualTo |
greaterThanOrEqualTo |
As greaterThan. |
lessThan |
lessThan |
As greaterThan. |
lessThanOrEqualTo |
lessThanOrEqualTo |
As greaterThan. |
| Ranges | ||
between |
between |
Value.List [ min, max ], exclusive. A blank end is open. |
betweenInclusive |
betweenInclusive |
Value.List [ min, max ], inclusive. A blank end is open. |
inNumberRange |
inNumberRange |
Value.List [ min, max ], inclusive, numbers only. The ends are coerced and swapped when reversed. |
inDateRange |
inDateRange |
Value.List [ min, max ] of Date values, timestamps, or YYYY-MM-DD strings. Inclusive; a blank end is open. |
| Arrays | ||
arrHas |
arrHas |
Value.List of candidates; the cell is a scalar and has to equal one of them. |
arrIncludes |
arrIncludes |
Value.List of candidates; a List cell has to contain one, a String cell has to contain one as a substring. |
arrIncludesAll |
arrIncludesAll |
Value.List; a List cell has to contain every one of them. |
arrIncludesSome |
arrIncludesSome |
Value.List; a List cell has to contain at least one. |
A column with no withFilterFn and no withCustomFilter filters with the
automatic choice, TanStack's filterFn: 'auto'. Here Nothing in
Column.filterFn is 'auto'.
getAutoFilterFn picks from the
type of the column's first non-null value: includesString for strings,
inNumberRange for numbers, equals for booleans, arrIncludes for lists,
inDateRange for dates, and weakEquals when every sampled value is Null.
Custom filter functions
Build a FilterFn with Table.FilterFn.custom and add the parts you need.
Resolvers default to identity; autoRemove defaults to dropping Null and
the empty string.
startsWithTrimmed : FilterFn.FilterFn
startsWithTrimmed =
FilterFn.custom
(\dataValue filterValue ->
String.startsWith (Value.toString filterValue) (Value.toString dataValue)
)
|> FilterFn.withResolveFilterValue
(Value.toString >> String.toLower >> String.trim >> Value.String)
|> FilterFn.withResolveDataValue
(Value.toString >> String.toLower >> Value.String)
|> FilterFn.withAutoRemove (\filterValue -> Value.toString filterValue == "")
Table.withCustomFilter is the other route. It takes the whole Row and the
raw filter value, so it can look at fields the column's accessor does not
expose. The filter value it receives is not resolved, because there is no
FilterFn to resolve it with.
activeDepartmentColumn : Table.Column Person
activeDepartmentColumn =
Table.column "department" (.department >> Value.String)
|> Table.withHeader "Department"
|> Table.withCustomFilter
(\row filterValue ->
let
person : Person
person =
Table.rowOriginal row
in
person.active
&& String.contains
(String.toLower (Value.toString filterValue))
(String.toLower person.department)
)
Transitions
| Function | What it does |
|---|---|
setColumnFilter |
Set one column's filter value: replaced in place when it already has one, appended otherwise, removed when the value should auto-remove. |
setColumnFilters |
Replace State.columnFilters wholesale, dropping entries of known columns whose value should auto-remove. |
resetColumnFilters |
Clear every column filter. Useful for a "clear all" button. |
update : Msg -> Table.State -> Table.State
update msg state =
case msg of
FilterTyped columnId typed ->
Table.setColumnFilter config
(unfilteredRows state)
columnId
(Value.String typed)
state
FiltersCleared ->
Table.resetColumnFilters state
setColumnFilter and setColumnFilters take a RowModel because
autoRemove is data-derived: deciding whether a value is blank enough to drop
means knowing which filter function the column uses, and for a column with no
withFilterFn that is the automatic choice, which is sampled from the rows.
Pass the row model you handed to Table.filteredRowModel, the one before
filtering.
unfilteredRows : Table.State -> Table.RowModel Person
unfilteredRows state =
Table.coreRowModelFromList config state people
Queries
| Function | Use it for |
|---|---|
getFilterValue |
The current value of a filter input. Maybe Value, Nothing when the column has no filter. |
getCanFilter |
Showing or disabling a filter input. |
getIsFiltered |
A "this column is filtered" indicator. |
getFilterIndex |
The order the filters were applied in. -1 when the column has no filter. |
getFilterFn |
Showing which filter mode a column is in. Nothing when the column does not exist. |
getAutoFilterFn |
The function the automatic choice would pick. |
shouldAutoRemoveFilter |
Predicting whether a value you are about to write will be stored or dropped. |
viewFilterInput : Table.State -> Table.Column Person -> Html Msg
viewFilterInput state column =
let
id : String
id =
Table.columnId column
current : String
current =
Table.getFilterValue state id
|> Maybe.map Value.toString
|> Maybe.withDefault ""
in
if Table.getCanFilter config id then
input
[ value current
, placeholder ("Filter " ++ Maybe.withDefault id (Table.columnHeader column))
, onInput (FilterTyped id)
]
[]
else
text ""
activeFilterCount : Table.State -> Int
activeFilterCount state =
Table.leafColumns config
|> List.filter (\column -> Table.getIsFiltered state (Table.columnId column))
|> List.length
getFilterFn and getAutoFilterFn take a RowModel to sample for the
automatic choice; pass the core row model. The other five read only the
State or the Config.
What this page does not cover
- Controlled state,
onColumnFiltersChange, and atoms. TheStateis always yours. See Table State. initialState.columnFilters. Build your starting state by callingsetColumnFiltersonTable.initialState.- The
filterFnsregistry. There is no registry, sogetFilterFnreturns aFilterFnrather than a registered name, and a column takes the function itself. columnFiltersMetaandaddMeta. A filter function cannot write per-row metadata back onto the row, so the ranking pattern TanStack's fuzzy guide uses is done differently here. See Fuzzy Filtering.autoResetPageIndex. Filtering does not move the page index for you. CallTable.resetPageIndexin the sameupdatebranch as the filter change.
Example
Column Filters, ported from TanStack's Filters example.