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

Combobox component with search/filter for PhiaUI.

A searchable selection widget that renders as a trigger button + dropdown
panel. Filtering is server-side: the parent LiveView handles the search event
and passes back a filtered `options` list. No client-side JS hook is required
for basic functionality.

## Sub-components

- `combobox/1` — standalone combobox (trigger + dropdown panel)
- `form_combobox/1` — `Phoenix.HTML.FormField`-integrated variant with hidden
  input and changeset error display

## When to use

Use `combobox/1` when a `<select>` element would have too many options and
users benefit from typing to filter — country pickers, user selectors,
product/SKU selectors, time-zone pickers, etc.

For simple short lists (< 10 items), use a native `filter_select/1` instead.

## Three-event model

The parent LiveView must handle exactly three events:

| Event            | When fired                       | Payload                   |
|------------------|----------------------------------|---------------------------|
| `on_toggle`      | Trigger button clicked           | none (params ignored)     |
| `on_search`      | User types in the search input   | `%{"query" => string}`    |
| `on_change`      | User selects an option           | `%{"value" => string}`    |

## Complete example — country selector

    defmodule MyAppWeb.ProfileLive do
      use Phoenix.LiveView

      @countries [
        %{value: "us", label: "United States"},
        %{value: "gb", label: "United Kingdom"},
        %{value: "de", label: "Germany"},
        %{value: "fr", label: "France"},
        # ... etc.
      ]

      def mount(_params, _session, socket) do
        {:ok, assign(socket,
          country: nil,
          country_open: false,
          country_search: ""
        )}
      end

      def handle_event("toggle-country", _params, socket) do
        {:noreply, update(socket, :country_open, &(!&1))}
      end

      def handle_event("search-country", %{"query" => q}, socket) do
        {:noreply, assign(socket, country_search: q)}
      end

      def handle_event("pick-country", %{"value" => v}, socket) do
        {:noreply, assign(socket,
          country: v,
          country_open: false,
          country_search: ""
        )}
      end

      # Filter the options server-side based on search query
      defp filtered_countries(search) do
        query = String.downcase(search)
        Enum.filter(@countries, &String.contains?(String.downcase(&1.label), query))
      end
    end

    <%!-- Template --%>
    <.combobox
      id="country-picker"
      options={filtered_countries(@country_search)}
      value={@country}
      open={@country_open}
      search={@country_search}
      placeholder="Select a country..."
      on_toggle="toggle-country"
      on_search="search-country"
      on_change="pick-country"
    />

## Form-integrated example

    <.form for={@form} phx-submit="save">
      <.form_combobox
        id="timezone-picker"
        field={@form[:timezone]}
        options={filtered_timezones(@timezone_search)}
        value={@selected_timezone}
        open={@timezone_open}
        search={@timezone_search}
        placeholder="Select timezone..."
        on_toggle="toggle-tz"
        on_search="search-tz"
        on_change="pick-tz"
      />
      <.button type="submit">Save</.button>
    </.form>

## Options format

Options can be passed in two formats:

    # Map format (preferred)
    options={[%{value: "us", label: "United States"}, ...]}

    # Tuple format (compatible with Phoenix.HTML.Form select helpers)
    options={[{"United States", "us"}, {"Germany", "de"}, ...]}

Both formats are normalised internally to `%{value: string, label: string}`.

## ARIA

The trigger button has `aria-haspopup="listbox"` and `aria-expanded` toggled
with the `open` assign. The dropdown has `role="listbox"` and each option has
`role="option"` with `aria-selected`. Keyboard navigation (Escape / Enter /
arrow keys) requires the optional `PhiaCombobox` JS hook.

# `combobox`

Renders a combobox with a trigger button and searchable dropdown.

The component is fully server-driven: `open`, `search`, `value`, and
`options` are all controlled by the LiveView. On each re-render, the
`options` list is normalised and filtered by the current `search` query
for cases where all options are passed statically (no server-side filtering).

For large option lists (hundreds of items), handle `on_search` in the LiveView
and pass a pre-filtered `options` list instead of relying on client-side filtering.

## Attributes

* `id` (`:string`) (required) - Unique combobox DOM id.
* `value` (`:string`) - Currently selected value string, or `nil` when nothing is selected. Defaults to `nil`.
* `placeholder` (`:string`) - Trigger button text shown when no value is selected. Defaults to `"Select an option..."`.
* `search_placeholder` (`:string`) - Placeholder text for the search input inside the dropdown. Defaults to `"Search..."`.
* `options` (`:list`) - List of options in either format:
  - `%{value: string, label: string}` maps
  - `{label, value}` 2-tuples (compatible with Phoenix.HTML.Form)

  Pass a pre-filtered subset when the LiveView handles `on_search`.

  Defaults to `[]`.
* `open` (`:boolean`) - Whether the dropdown panel is currently visible. Controlled by the LiveView. Defaults to `false`.
* `search` (`:string`) - Current search query — used for client-side label filtering when `options` is static. Defaults to `""`.
* `on_change` (`:string`) - `phx-click` event name emitted when a user selects an option.
  The LiveView receives `%{"value" => value}`.

  Defaults to `"combobox-change"`.
* `on_search` (`:string`) - `phx-change` event name emitted by the search input.
  The LiveView receives `%{"query" => query}`.
  Use this to filter `options` server-side and re-assign.

  Defaults to `"combobox-search"`.
* `on_toggle` (`:string`) - `phx-click` event name emitted when the trigger button is clicked.
  The LiveView should toggle the `open` assign: `update(socket, :open, &(!&1))`.

  Defaults to `"combobox-toggle"`.
* `class` (`:string`) - Additional CSS classes merged via `cn/1`. Defaults to `nil`.
* Global attributes are accepted. HTML attributes forwarded to the root div.

# `form_combobox`

Renders a combobox integrated with `Phoenix.HTML.FormField`.

Injects a `<input type="hidden">` bound to `field.name` so the selected
value is included in `phx-submit` form payloads. Changeset errors from
`field.errors` are displayed as destructive text below the widget.

## Example

    <.form_combobox
      id="assignee-picker"
      field={@form[:assignee_id]}
      options={@team_members}
      value={@selected_assignee}
      open={@assignee_open}
      search={@assignee_search}
      placeholder="Assign to..."
      on_toggle="toggle-assignee"
      on_search="search-assignee"
      on_change="pick-assignee"
    />

## Attributes

* `field` (`Phoenix.HTML.FormField`) (required) - `Phoenix.HTML.FormField` struct from `@form[:field_name]`.
  Provides `id`, `name`, and `errors` for form integration.

* `id` (`:string`) (required) - Unique combobox DOM id.
* `value` (`:any`) - Currently selected value (string or nil). Defaults to `nil`.
* `placeholder` (`:string`) - Trigger button placeholder text. Defaults to `"Select an option..."`.
* `search_placeholder` (`:string`) - Placeholder text for the search input. Defaults to `"Search..."`.
* `options` (`:list`) - Options list (same format as `combobox/1`). Defaults to `[]`.
* `open` (`:boolean`) - Whether the dropdown is visible. Defaults to `false`.
* `search` (`:string`) - Current search query. Defaults to `""`.
* `on_change` (`:string`) - `phx-click` event for option selection. Defaults to `"combobox-change"`.
* `on_search` (`:string`) - `phx-change` event for search input. Defaults to `"combobox-search"`.
* `on_toggle` (`:string`) - `phx-click` event for the trigger button. Defaults to `"combobox-toggle"`.
* `class` (`:string`) - Additional CSS classes. Defaults to `nil`.

---

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