PhiaUi.Components.FilterBuilder (phia_ui v0.1.16)

Copy Markdown View Source

FilterBuilder component for PhiaUI.

An advanced query-builder UI for constructing multi-condition filter rules at runtime. Each rule is a row containing:

  1. A field selector — which column/attribute to filter on
  2. An operator selector — how to compare (contains, equals, before, etc.)
  3. A value input — the comparison value (text, date, number, or select)
  4. A remove button — delete this rule

New rules are added via an "Add filter" button that fires a server event. The entire state is owned by the LiveView — no client-side JS is required.

When to use

Use FilterBuilder for power-user search and reporting interfaces where users need to combine multiple conditions — like the filter panels in Notion, Linear, Airtable, or an admin reporting dashboard.

For simple search + status dropdowns, use FilterBar instead.

Anatomy

ComponentElementPurpose
filter_builder/1divRoot container: rule list + "Add filter" button
filter_rule/1divOne rule row: field / operator / value / remove

Field schema

Each entry in the fields list is a map with:

KeyTypeRequiredDescription
:nameString.t()yesUnique field identifier
:labelString.t()yesDisplay name in the field dropdown

| :type | "text" | "select" | "date" | "number" | yes | Controls available operators and value input | | :options | [{label, value}] | only for "select" | Options for the value dropdown |

Operator matrix

Field typeAvailable operators
textcontains, equals, starts with, ends with, is empty
selectequals, not equals
dateequals, before, after, between
numberequals, greater than, less than, between

Complete example — advanced search for a CRM contacts table

defp filter_fields do
  [
    %{name: "name",       label: "Name",       type: "text"},
    %{name: "email",      label: "Email",      type: "text"},
    %{name: "status",     label: "Status",     type: "select",
      options: [{"Active", "active"}, {"Churned", "churned"}, {"Prospect", "prospect"}]},
    %{name: "created_at", label: "Created",    type: "date"},
    %{name: "deal_value", label: "Deal Value", type: "number"}
  ]
end

# In the template:
<.filter_builder
  fields={filter_fields()}
  rules={@filter_rules}
  on_add="add_filter"
  on_remove="remove_filter"
  on_change="update_filter"
/>

LiveView state and handlers

# Default state in mount/3:
def mount(_params, _session, socket) do
  {:ok, assign(socket, filter_rules: [])}
end

# Add a new empty rule (UUID id ensures stable DOM keys):
def handle_event("add_filter", _params, socket) do
  rule = %{id: Ecto.UUID.generate(), field: "name", operator: "contains", value: ""}
  {:noreply, update(socket, :filter_rules, &[&1 | [rule]])}
end

# Remove a specific rule by its id:
def handle_event("remove_filter", %{"id" => id}, socket) do
  {:noreply, update(socket, :filter_rules, &Enum.reject(&1, fn r -> r.id == id end))}
end

# Update a specific rule field when user changes field/operator/value:
def handle_event("update_filter", params, socket) do
  id = params["id"] || params["phx-value-id"]
  rules = Enum.map(socket.assigns.filter_rules, fn rule ->
    if rule.id == id do
      %{rule |
        field:    params["filter"][id]["field"]    || rule.field,
        operator: params["filter"][id]["operator"] || rule.operator,
        value:    params["filter"][id]["value"]    || rule.value
      }
    else
      rule
    end
  end)
  {:noreply, assign(socket, filter_rules: rules)}
end

# Apply rules to a query (example using Ecto):
defp apply_filters(query, rules) do
  Enum.reduce(rules, query, fn
    %{field: "name", operator: "contains", value: v}, q ->
      from(r in q, where: ilike(r.name, ^"%#{v}%"))
    %{field: "status", operator: "equals", value: v}, q ->
      from(r in q, where: r.status == ^v)
    _rule, q -> q
  end)
end

Summary

Functions

Renders the filter builder container.

Renders a single filter rule row.

Functions

filter_builder(assigns)

Renders the filter builder container.

Renders existing rules as filter_rule/1 rows and an "Add filter" button to append new rules. Entirely server-driven — no JS hook required. All interactivity goes through LiveView phx-click / phx-change events.

Attributes

  • fields (:list) (required) - List of field definition maps. Each map must have :name, :label, :type, and optionally :options (for type: "select").

    fields={[
      %{name: "status", label: "Status", type: "select",
        options: [{"Active", "active"}, {"Inactive", "inactive"}]},
      %{name: "created_at", label: "Created", type: "date"},
      %{name: "amount", label: "Amount", type: "number"}
    ]}
  • rules (:list) - List of active rule maps. Each map must have :id, :field, :operator, and :value. The :id must be unique and stable across re-renders (use Ecto.UUID.generate() when creating rules).

    rules={[
      %{id: "abc-123", field: "status", operator: "equals", value: "active"},
      %{id: "def-456", field: "created_at", operator: "after", value: "2026-01-01"}
    ]}

    Defaults to [].

  • on_add (:string) (required) - phx-click event name for the "Add filter" button. The handler should append a new default rule to the rules list.

  • on_remove (:string) (required) - phx-click event name for the remove button on each rule row. The LiveView receives %{"id" => rule_id}.

  • on_change (:string) (required) - phx-change event name fired when any field, operator, or value input changes. The LiveView receives %{"filter" => %{rule_id => %{"field" => ..., "operator" => ..., "value" => ...}}}.

  • class (:string) - Additional CSS classes for the root wrapper. Defaults to nil.

  • Global attributes are accepted. HTML attributes forwarded to the root div.

filter_rule(assigns)

Renders a single filter rule row.

The row contains a field dropdown, an operator dropdown (populated based on the selected field's type), a value input (type-appropriate: text, date, number, or select), and a remove button.

The current_field is resolved at render time from the fields list so the operator list and value input are always consistent with the selected field.

Attributes

  • id (:string) (required) - Unique rule identifier. Passed as phx-value-id on the remove button and embedded in input name attributes so the LiveView can identify which rule was changed: filter[rule_id][field], filter[rule_id][operator], etc.

  • field (:string) (required) - Name of the currently selected field (must match a :name key in the fields list).

  • operator (:string) (required) - Currently selected operator string (e.g. "contains", "before", "equals").

  • value (:string) - Current filter value string. Defaults to "".

  • fields (:list) (required) - Same fields list passed to filter_builder/1 — used to render field options.

  • on_remove (:string) (required) - phx-click event name for the remove button on this row.

  • on_change (:string) (required) - phx-change event name fired when any input on this row changes.

  • class (:string) - Additional CSS classes for the row wrapper. Defaults to nil.

  • Global attributes are accepted. HTML attributes forwarded to the row div.