Corex.Listbox (Corex v0.1.0-alpha.31)

View Source

Phoenix implementation of Zag.js Listbox.

Examples

Minimal

<.listbox
  id="my-listbox"
  class="listbox"
  items={[
    %{label: "France", id: "fra", disabled: true},
    %{label: "Belgium", id: "bel"},
    %{label: "Germany", id: "deu"},
    %{label: "Netherlands", id: "nld"},
    %{label: "Switzerland", id: "che"},
    %{label: "Austria", id: "aut"}
  ]}
>
  <:label>Choose a country</:label>
  <:item_indicator>
    <.heroicon name="hero-check" />
  </:item_indicator>
</.listbox>

Grouped

<.listbox
  class="listbox"
  items={[
    %{label: "France", id: "fra", group: "Europe"},
    %{label: "Belgium", id: "bel", group: "Europe"},
    %{label: "Germany", id: "deu", group: "Europe"},
    %{label: "Netherlands", id: "nld", group: "Europe"},
    %{label: "Switzerland", id: "che", group: "Europe"},
    %{label: "Austria", id: "aut", group: "Europe"},
    %{label: "Japan", id: "jpn", group: "Asia"},
    %{label: "China", id: "chn", group: "Asia"},
    %{label: "South Korea", id: "kor", group: "Asia"},
    %{label: "Thailand", id: "tha", group: "Asia"},
    %{label: "USA", id: "usa", group: "North America"},
    %{label: "Canada", id: "can", group: "North America"},
    %{label: "Mexico", id: "mex", group: "North America"}
  ]}
>
  <:label>Choose a country</:label>
  <:item_indicator>
    <.heroicon name="hero-check" />
  </:item_indicator>
</.listbox>

Custom

This example requires the installation of Flagpack. Use the :item slot with :let={%{item: entry}} to access the entry map.

<.listbox
  class="listbox"
  items={[
    %{label: "France", id: "fra"},
    %{label: "Belgium", id: "bel"},
    %{label: "Germany", id: "deu"},
    %{label: "Netherlands", id: "nld"},
    %{label: "Switzerland", id: "che"},
    %{label: "Austria", id: "aut"}
  ]}
>
  <:label>
    Country of residence
  </:label>
  <:item :let={%{item: entry}}>
    <Flagpack.flag name={String.to_atom(entry.id)} />
    {entry.label}
  </:item>
  <:item_indicator>
    <.heroicon name="hero-check" />
  </:item_indicator>
</.listbox>

Custom Grouped

<.listbox
  class="listbox"
  items={[
    %{label: "France", id: "fra", group: "Europe"},
    %{label: "Belgium", id: "bel", group: "Europe"},
    %{label: "Germany", id: "deu", group: "Europe"},
    %{label: "Japan", id: "jpn", group: "Asia"},
    %{label: "China", id: "chn", group: "Asia"},
    %{label: "South Korea", id: "kor", group: "Asia"}
  ]}
>
  <:item :let={%{item: entry}}>
    <Flagpack.flag name={String.to_atom(entry.id)} />
    {entry.label}
  </:item>
  <:item_indicator>
    <.heroicon name="hero-check" />
  </:item_indicator>
</.listbox>

Stream

Use with Phoenix.LiveView.stream/3 to add or remove items dynamically. Keep a list in sync with the stream and pass it as items. The listbox uses phx-update="ignore" and updates via data-items; the JS hook rebuilds the list when items change.

For actions inside the :item slot (e.g. a remove button), use data-phx-push and data-phx-push-id so the listbox hook can delegate clicks to LiveView:

def mount(_params, _session, socket) do
  {:ok,
   socket
   |> stream_configure(:items, dom_id: &"listbox:my-listbox:item:#{&1.id}")
   |> stream(:items, @initial_items)
   |> assign(:items_list, @initial_items)}
end

def render(assigns) do
  ~H"""
  <.listbox id="my-listbox" class="listbox" items={@items_list}>
    <:label>Choose an item</:label>
    <:empty>No items</:empty>
    <:item :let={%{item: entry}}>
      <span class="flex items-center justify-between gap-2 w-full">
        <span class="flex items-center gap-2">
          <.action
            phx-click={JS.push("remove_item", value: %{id: entry.id})}
            data-phx-push="remove_item"
            data-phx-push-id={entry.id}
            class="button button--sm"
          >
            <.heroicon name="hero-trash" />
          </.action>
          <span>{entry.label}</span>
        </span>
      </span>
    </:item>
    <:item_indicator>
      <.heroicon name="hero-check" />
    </:item_indicator>
  </.listbox>
  """
end

def handle_event("remove_item", %{"id" => id}, socket) do
  item = Enum.find(socket.assigns.items_list, &(&1.id == id))
  if item do
    {:noreply,
     socket
     |> stream_delete(:items, item)
     |> assign(:items_list, List.delete(socket.assigns.items_list, item))}
  else
    {:noreply, socket}
  end
end

Styling

Use data attributes to target elements:

[data-scope="listbox"][data-part="root"] {}
[data-scope="listbox"][data-part="content"] {}
[data-scope="listbox"][data-part="item"] {}
[data-scope="listbox"][data-part="item-text"] {}
[data-scope="listbox"][data-part="item-indicator"] {}
[data-scope="listbox"][data-part="item-group"] {}
[data-scope="listbox"][data-part="item-group-label"] {}

If you wish to use the default Corex styling, you can use the class listbox 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/listbox.css";

You can then use modifiers

<.listbox class="listbox listbox--accent listbox--lg" items={[]}>
</.listbox>

Learn more about modifiers and Corex Design

Summary

Components

listbox(assigns)

Attributes

  • id (:string) - The id of the listbox.
  • items (:list) (required) - List of Corex.List.Item or maps with id/value, label, disabled, group.
  • value (:list) - Selected value(s). Defaults to [].
  • controlled (:boolean) - Whether the listbox is controlled. Defaults to false.
  • disabled (:boolean) - Whether the listbox is disabled. Defaults to false.
  • dir (:string) - Text direction. Defaults to nil. Must be one of nil, "ltr", or "rtl".
  • orientation (:string) - Layout orientation of items. Defaults to "vertical". Must be one of "horizontal", or "vertical".
  • loop_focus (:boolean) - Whether to loop focus within the listbox. Defaults to false.
  • selection_mode (:string) - How items can be selected. Defaults to "single". Must be one of "single", "multiple", or "extended".
  • select_on_highlight (:boolean) - Select item when highlighted via keyboard. Defaults to false.
  • deselectable (:boolean) - Whether selection can be cleared. Defaults to false.
  • typeahead (:boolean) - Enable typeahead search. Defaults to false.
  • on_value_change (:string) - Server event name on value change. Defaults to nil.
  • on_value_change_client (:string) - Client event name on value change. Defaults to nil.
  • aria_label (:string) - Accessible name when no label slot is provided. Defaults to nil.
  • Global attributes are accepted.

Slots

  • label - Accepts attributes:
    • class (:string)
  • item - Accepts attributes:
    • class (:string)
  • item_indicator - Accepts attributes:
    • class (:string)
  • empty - Accepts attributes:
    • class (:string)