Corex.Combobox (Corex v0.1.1)

View Source

Phoenix implementation of Zag.js Combobox.

Pass options with Corex.List.new/1. With redirect, use per-item :to, :redirect (:href | :patch | :navigate | false), and :new_tab; Zag runs single-select when redirect is true.

Anatomy

Minimal

<.combobox
      class="combobox"
      translation={%Corex.Combobox.Translation{placeholder: "Select a country", empty: "No results"}}
      items={Corex.List.new([
        %{label: "France", value: "fra"},
        %{label: "Belgium", value: "bel"},
        %{label: "Germany", value: "deu"},
        %{label: "Netherlands", value: "nld"},
        %{label: "Switzerland", value: "che"},
        %{label: "Austria", value: "aut"}
      ])}
    >
      <:trigger>
        <.heroicon name="hero-chevron-down" />
      </:trigger>
    </.combobox>

Grouped

<.combobox
      class="combobox"
      translation={%Corex.Combobox.Translation{placeholder: "Select a country", empty: "No results"}}
      items={Corex.List.new([
        %{label: "France", value: "fra", group: "Europe"},
        %{label: "Belgium", value: "bel", group: "Europe"},
        %{label: "Germany", value: "deu", group: "Europe"},
        %{label: "Netherlands", value: "nld", group: "Europe"},
        %{label: "Switzerland", value: "che", group: "Europe"},
        %{label: "Austria", value: "aut", group: "Europe"},
        %{label: "Japan", value: "jpn", group: "Asia"},
        %{label: "China", value: "chn", group: "Asia"},
        %{label: "South Korea", value: "kor", group: "Asia"},
        %{label: "Thailand", value: "tha", group: "Asia"},
        %{label: "USA", value: "usa", group: "North America"},
        %{label: "Canada", value: "can", group: "North America"},
        %{label: "Mexico", value: "mex", group: "North America"}
      ])}
    >
      <:trigger>
        <.heroicon name="hero-chevron-down" />
      </:trigger>
    </.combobox>

Extended

This example requires the installation of Flagpack to display the use of custom item rendering.

  <.combobox
      class="combobox"
      translation={%Corex.Combobox.Translation{placeholder: "Select a country", empty: "No results"}}
      items={Corex.List.new([
        %{label: "France", value: "fra"},
        %{label: "Belgium", value: "bel"},
        %{label: "Germany", value: "deu"},
        %{label: "Netherlands", value: "nld"},
        %{label: "Switzerland", value: "che"},
        %{label: "Austria", value: "aut"}
      ])}
    >
      <:item :let={item}>
        <Flagpack.flag name={String.to_existing_atom(to_string(item.value))} />
        {item.label}
      </:item>
      <:trigger>
        <.heroicon name="hero-chevron-down" />
      </:trigger>
      <:clear_trigger>
        <.heroicon name="hero-backspace" />
      </:clear_trigger>
      <:item_indicator>
        <.heroicon name="hero-check" />
      </:item_indicator>
    </.combobox>

Extended Grouped

This example requires the installation of Flagpack to display the use of custom item rendering.

<.combobox
      class="combobox"
      translation={%Corex.Combobox.Translation{placeholder: "Select a country", empty: "No results"}}
      items={Corex.List.new([
        %{label: "France", value: "fra", group: "Europe"},
        %{label: "Belgium", value: "bel", group: "Europe"},
        %{label: "Germany", value: "deu", group: "Europe"},
        %{label: "Japan", value: "jpn", group: "Asia"},
        %{label: "China", value: "chn", group: "Asia"},
        %{label: "South Korea", value: "kor", group: "Asia"}
      ])}
    >
      <:item :let={item}>
        <Flagpack.flag name={String.to_existing_atom(to_string(item.value))} />
        {item.label}
      </:item>
      <:trigger>
        <.heroicon name="hero-chevron-down" />
      </:trigger>
      <:clear_trigger>
        <.heroicon name="hero-backspace" />
      </:clear_trigger>
      <:item_indicator>
        <.heroicon name="hero-check" />
      </:item_indicator>
    </.combobox>

API

Requires a stable id on <.combobox>.

FunctionActionReturns
set_value/2Set selection (client)%Phoenix.LiveView.JS{}
set_value/3Set selection (server)socket
<.action phx-click={Corex.Combobox.set_value("combobox-api", ["fra"])} class="button button--sm">France</.action>

Events

Server events

EventWhenPayload
on_value_change="combobox_value_changed"Selection changes%{"id" => id, "value" => values}
on_open_change="combobox_open_changed"Menu open state changes%{"id" => id, "open" => open}
on_input_value_change="combobox_search"Input text changes (server filter)%{"id" => id, "value" => string, "reason" => reason}

Client events

EventWhenevent.detail
on_value_change_client="combobox-value-changed"Selection changesid, value, items
on_open_change_client="combobox-open-changed"Menu open state changesid, open

Patterns

Server-side filtering

Disable client filtering with filter={false} and use on_input_value_change to filter on the server. This example uses a local list; replace with a database query for real apps.

defmodule MyAppWeb.CountryCombobox do
  use MyAppWeb, :live_view

  @items [
    %{value: "fra", label: "France"},
    %{value: "bel", label: "Belgium"},
    %{value: "deu", label: "Germany"},
    %{value: "usa", label: "USA"},
    %{value: "jpn", label: "Japan"}
  ]

  def mount(_params, _session, socket) do
    {:ok, assign(socket, items: [])}
  end

  def handle_event("search", %{"value" => value, "reason" => "input-change"}, socket) do
    filtered =
      if byte_size(value) < 1 do
        []
      else
        term = String.downcase(value)
        Enum.filter(@items, fn item ->
          String.contains?(String.downcase(item.label), term)
        end)
      end

    {:noreply, assign(socket, items: filtered)}
  end

  def render(assigns) do
    ~H"""
    <.combobox
      items={@items}
      filter={false}
      on_input_value_change="search"
    >
      <:trigger><.heroicon name="hero-chevron-down" /></:trigger>
    </.combobox>
    """
  end
end

Style

Target parts with data-scope and data-part:

[data-scope="combobox"][data-part="root"] {}
[data-scope="combobox"][data-part="control"] {}
[data-scope="combobox"][data-part="input"] {}
[data-scope="combobox"][data-part="trigger"] {}
[data-scope="combobox"][data-part="clear-trigger"] {}
[data-scope="combobox"][data-part="content"] {}
[data-scope="combobox"][data-part="empty"] {}
[data-scope="combobox"][data-part="item-group"] {}
[data-scope="combobox"][data-part="item-group-label"] {}
[data-scope="combobox"][data-part="item"] {}
[data-scope="combobox"][data-part="item-text"] {}
[data-scope="combobox"][data-part="item-indicator"] {}

If you wish to use the default Corex styling, you can use the class combobox on the component. This requires to install Mix.Tasks.Corex.Design first and import the component css file.

@import "../corex/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/combobox.css";

You can then use modifiers

<.combobox class="combobox combobox--accent combobox--lg" items={Corex.List.new([])}>
  <:empty>No results</:empty>
  <:trigger>
    <.heroicon name="hero-chevron-down" />
  </:trigger>
</.combobox>

Form

Use field={f[:key]} with a form built from an Ecto changeset. Set the form id in to_form/2 and use <.form for={@form}>. See Select Form for full controller and LiveView examples.

For cross-cutting invalid styling and error presentation, see the Forms guide. Pass invalid={Corex.FormField.invalid?(@form[:field])} when you want alert borders after validation.

Localization

Pass translation={%Corex.Combobox.Translation{}} for partial overrides. See Corex.Combobox.Translation for defaults.

Summary

Components

Renders a combobox component.

API

Set selected value(s) from a control (phx-click). Pass a list, comma-separated string, or single value (normalized like the component).

Set selection from handle_event. Pushes combobox_set_value.

Components

combobox(assigns)

Renders a combobox component.

Attributes

  • id (:string) - The id of the combobox, useful for API to identify the combobox.

  • items (:list) - Items from Corex.List.new/1 (or maps with :label and optional :value). Defaults to [].

  • value (:any) - Initial selected item values (list of strings or a single string); not updated by LiveView after mount. Defaults to nil.

  • on_open_change (:string) - The server event name to trigger on open change. Defaults to nil.

  • on_open_change_client (:string) - The client event name to trigger on open change. Defaults to nil.

  • disabled (:boolean) - Whether the combobox is disabled. Defaults to false.

  • translation (Corex.Combobox.Translation) - Override translatable strings. Defaults to nil.

  • always_submit_on_enter (:boolean) - Whether to always submit on enter. Defaults to false.

  • auto_focus (:boolean) - Whether to auto focus the combobox. Defaults to false.

  • close_on_select (:boolean) - Whether to close the combobox on select. Defaults to true.

  • dir (:string) - The direction of the combobox. When nil, derived from document (html lang + config :rtl_locales). Defaults to nil. Must be one of nil, "ltr", or "rtl".

  • orientation (:string) - Layout orientation for the combobox. Defaults to "vertical". Must be one of "horizontal", or "vertical".

  • input_behavior (:string) - The input behavior of the combobox. Defaults to "autohighlight".

  • loop_focus (:boolean) - Whether to loop focus the combobox. Defaults to false.

  • multiple (:boolean) - Whether to allow multiple selection. Defaults to false.

  • invalid (:boolean) - Whether the combobox is invalid. Defaults to false.

  • name (:string) - The name of the combobox.

  • form (:string) - The id of the form of the combobox.

  • read_only (:boolean) - Whether the combobox is read only. Defaults to false.

  • required (:boolean) - Whether the combobox is required. Defaults to false.

  • filter (:boolean) - When true, filter options client-side by input value. Set to false when using on_input_value_change for server-side filtering. Defaults to true.

  • on_input_value_change (:string) - The server event name to trigger on input value change. Defaults to nil.

  • on_input_value_change_client (:string) - The client event name to trigger on input value change. Defaults to nil.

  • on_value_change (:string) - The server event name to trigger on value change. Defaults to nil.

  • on_value_change_client (:string) - The client event name to trigger on value change. Defaults to nil.

  • redirect (:boolean) - When true, selecting a value triggers redirect-on-select. Each item picks the navigation kind via :redirect (:href (default) | :patch | :navigate | false). Items may also set :to (overrides the destination) and :new_tab (opens in a new tab). When true, the client runs single-select in Zag even if multiple is set on this component.

    Defaults to false.

  • positioning (:map) - The positioning of the combobox. Defaults to %Corex.Positioning{hide_when_detached: true, strategy: "fixed", placement: "bottom", gutter: 8, shift: 0, overflow_padding: 0, arrow_padding: 4, flip: true, slide: true, overlap: false, same_width: true, fit_viewport: true, offset: nil}.

  • field (Phoenix.HTML.FormField) - A form field struct retrieved from the form, for example: @form[:country]. Automatically sets id, name, value, and errors from the form field.

  • errors (:list) - List of error messages to display. Defaults to [].

  • Global attributes are accepted.

Slots

  • label - The label content. Accepts attributes:
    • class (:string)
  • empty - Content when there are no results. When omitted, translation.empty is used. Accepts attributes:
    • class (:string)
  • trigger (required) - The trigger button content. Accepts attributes:
    • class (:string)
  • clear_trigger - The clear button content. Accepts attributes:
    • class (:string)
  • item_indicator - Optional indicator for selected items. Accepts attributes:
    • class (:string)
  • error - Accepts attributes:
    • class (:string)
  • item - Custom content for each item. Receives the item as :let binding. Accepts attributes:
    • class (:string)

API

set_value(combobox_id, value)

Set selected value(s) from a control (phx-click). Pass a list, comma-separated string, or single value (normalized like the component).

<.action phx-click={Corex.Combobox.set_value("my-combobox", "bel")}>Belgium</.action>
<.combobox
  id="my-combobox"
  class="combobox"
  translation={%Corex.Combobox.Translation{placeholder: "Country", empty: "None"}}
  items={Corex.List.new([
    %{label: "Belgium", value: "bel"},
    %{label: "Germany", value: "deu"}
  ])}
>
  <:trigger><.heroicon name="hero-chevron-down" /></:trigger>
</.combobox>
document.getElementById("my-combobox")?.dispatchEvent(
  new CustomEvent("corex:combobox:set-value", {
    bubbles: false,
    detail: { value: ["bel"] },
  })
);

set_value(socket, combobox_id, value)

Set selection from handle_event. Pushes combobox_set_value.

<.action phx-click="pick_bel" phx-value-value="bel">Belgium</.action>
<.combobox
  id="my-combobox"
  class="combobox"
  translation={%Corex.Combobox.Translation{placeholder: "Country", empty: "None"}}
  items={Corex.List.new([
    %{label: "Belgium", value: "bel"},
    %{label: "Germany", value: "deu"}
  ])}
>
  <:trigger><.heroicon name="hero-chevron-down" /></:trigger>
</.combobox>
def handle_event("pick_bel", %{"value" => v}, socket) do
  {:noreply, Corex.Combobox.set_value(socket, "my-combobox", v)}
end