# `PhiaUi.Components.FilterBuilder`
[🔗](https://github.com/charlenopires/PhiaUI/blob/v0.1.17/lib/phia_ui/components/data/filter_builder.ex#L1)

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

| Component         | Element | Purpose                                         |
|-------------------|---------|-------------------------------------------------|
| `filter_builder/1`| `div`   | Root container: rule list + "Add filter" button |
| `filter_rule/1`   | `div`   | One rule row: field / operator / value / remove |

## Field schema

Each entry in the `fields` list is a map with:

| Key        | Type                       | Required | Description                          |
|------------|----------------------------|----------|--------------------------------------|
| `:name`    | `String.t()`               | yes      | Unique field identifier              |
| `:label`   | `String.t()`               | yes      | Display 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 type | Available operators                                    |
|------------|--------------------------------------------------------|
| `text`     | contains, equals, starts with, ends with, is empty     |
| `select`   | equals, not equals                                     |
| `date`     | equals, before, after, between                         |
| `number`   | equals, 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

# `filter_builder`

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`

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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
