PetalComponents.Filters (petal_components v4.15.3)

Copy Markdown View Source

A schema-driven filter bar: the active filters as removable chips, plus an "Add filter" popover that walks field -> operator -> value.

It speaks PetalComponents.DataTable.State, the same struct <.data_table> is driven by, so it drops in next to a table sharing one state - or stands alone above any list, card grid or custom query UI. The operator vocabulary is State.ops/0; this component only ever selects subsets of it, and every mutation goes through State.put_filter/4 or State.handle_op/3.

Two wiring modes, inferred from which attr you pass (passing neither raises, exactly like data_table/1):

  • link mode (path) - every change is a patch URL built from State.to_params/1. handle_params plus State.from_params/2 is the whole backend, and the URL is the state store:

    def handle_params(params, _uri, socket) do
      state = State.from_params(params, fields: [:name, :category, :price])
      {rows, state} = Engine.List.run(all_products(), state)
      {:noreply, assign(socket, rows: rows, table: state)}
    end
    
    <.filters id="f" state={@table} path={~p"/products"}>
      <:field field={:name} label="Name" type="text" />
      <:field field={:category} type="select" options={["tools", "toys"]} />
      <:field field={:price} label="Price" type="number_range" />
    </.filters>
  • event mode (on_change) - chip removal, apply and clear-all push the op-shaped payloads State.handle_op/3 already parses, so a LiveView already wired for an event-mode data_table gains a filter bar without a single new handler clause:

    def handle_event("table", params, socket) do
      state = State.handle_op(socket.assigns.table, params, fields: [:name, :category])
      {rows, state} = Engine.List.run(all_products(), state)
      {:noreply, assign(socket, rows: rows, table: state)}
    end
    
    <.filters id="f" state={@table} on_change="table">
      <:field field={:name} label="Name" type="text" />
      <:field field={:category} type="select" options={["tools", "toys"]} />
    </.filters>

Sharing one State with a data table

Point both at the same struct and the same event (or the same path) and they compose with no glue: filter from the bar and the table updates, filter from a column header and a chip appears.

<.filters id="products-filters" state={@table} on_change="table">
  <:field field={:category} type="select" options={["tools", "toys"]} />
  <:field field={:in_stock} label="In stock" type="boolean" />
</.filters>

<.data_table id="products" rows={@rows} state={@table} on_change="table">
  <:col :let={p} field={:name} sortable>{p.name}</:col>
  <:col :let={p} field={:category} filterable="select" options={["tools", "toys"]}>
    {p.category}
  </:col>
</.data_table>

Field types

The :field slot is the registry. type picks the operator subset (always a subset of State.ops/0) and the value editor:

typeoperatorseditor
textcontains, not_contains, eq, neq, starts_with, is_empty, is_not_emptytext input
selecteq, neq, is_empty, is_not_emptysingle select from options
multiincheckbox list from options
number_rangeeq, neq, gt, gte, lt, lte, between, is_empty, is_not_emptynumber input, two for between
date_rangebefore, on, after, between, is_empty, is_not_emptydate input, two for between
booleaneqyes/no select, no operator picker

Valueless operators (is_empty, is_not_empty) render no value input, per State.valueless_op?/1.

Summary

Functions

Renders the filter bar for state, with one :field entry per filterable field. Pass path for link mode or on_change for event mode.

The operator subset offered per registry type, as %{type => [op]}.

Functions

filters(assigns)

Renders the filter bar for state, with one :field entry per filterable field. Pass path for link mode or on_change for event mode.

Attributes

  • id (:string) (required) - DOM id; every panel and chip id is derived from it.

  • state (PetalComponents.DataTable.State) (required) - the state whose filters this bar renders and edits.

  • path (:string) - link mode: the base path filter changes patch to, with the state encoded via State.to_params/1. Required unless on_change is set.

    Defaults to nil.

  • on_change (:string) - event mode: the event filter edits push, with the same op-shaped payloads State.handle_op/3 already accepts (filter / clear_filters).

    Defaults to nil.

  • target (:any) - event mode: the phx-target (e.g. @myself). Defaults to nil.

  • add_filter_label (:string) - the add trigger's label and its panel's accessible name, localizable. Defaults to "Add filter".

  • clear_filters_label (:string) - the clear-all affordance's label, localizable. Defaults to "Clear filters".

  • apply_label (:string) - the value editor's submit label, localizable. Defaults to "Apply".

  • remove_filter_label (:string) - prefix for a chip's remove button accessible name, localizable. Defaults to "Remove filter".

  • active_filters_label (:string) - the chip group's accessible name, localizable. Defaults to "Active filters".

  • no_filters_label (:string) - announced by the status region when nothing is filtered, localizable. Defaults to "No filters applied".

  • all_fields_used_label (:string) - shown in the add panel when no field is left to add, localizable. Defaults to "Every field is already filtered".

  • filter_op_labels (:map) - overrides for operator display names, e.g. %{contains: "enthält"} - same attr name and shape as data_table. Defaults to %{}.

  • filter_options_placeholder (:string) - the multi editor's option-filter placeholder (shown from 8 options up), localizable. Defaults to "Filter options…".

  • class (:any) - extra classes on the bar's root element. Defaults to nil.

Slots

  • field (required) - the filterable field registry, one entry per field. Accepts attributes:
    • field (:atom) (required) - the state field this entry filters.
    • label (:string) - human name; defaults to the humanized field.
    • type (:string) - picks the operator subset and the value editor; defaults to "text". Must be one of "text", "select", "multi", "date_range", "boolean", or "number_range".
    • options (:list) - select/multi: the pickable values, as strings or {label, value} tuples - same shape as data_table's :col options.

type_operators()

@spec type_operators() :: %{required(String.t()) => [atom()]}

The operator subset offered per registry type, as %{type => [op]}.

Exposed so an app (and this library's own test suite) can assert the vocabulary against State.ops/0 rather than trusting a hand-copied list.