Corex.Listbox (Corex v0.1.0-beta.5)

View Source

Phoenix implementation of Zag.js Listbox.

Pass items={Corex.List.new([...])}. With redirect, use per-item :to and :redirect (:href | :patch | :navigate | false); Zag runs single-select when redirect is true.

Examples

Minimal

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

Grouped

<.listbox
  class="listbox"
  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"}
  ])}
>
  <: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={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"}
  ])}
>
  <:label>
    Country of residence
  </:label>
  <:item :let={%{item: entry}}>
    <Flagpack.flag name={String.to_atom(to_string(entry.value))} />
    {entry.label}
  </:item>
  <:item_indicator>
    <.heroicon name="hero-check" />
  </:item_indicator>
</.listbox>

Custom Grouped

<.listbox
  class="listbox"
  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: entry}}>
    <Flagpack.flag name={String.to_atom(to_string(entry.value))} />
    {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 hook reads data-items and 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.value}")
   |> 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: %{value: entry.value})}
            data-phx-push="remove_item"
            data-phx-push-id={entry.value}
            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", %{"value" => v}, socket) do
  item = Enum.find(socket.assigns.items_list, &(to_string(&1.value) == v))
  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={Corex.List.new([])}>
</.listbox>

Summary

API

Sets listbox selection from the client. Dispatches corex:listbox:set-value on the hook root.

Sets listbox selection from the server via push_event (listbox_set_value).

Requests the listbox's current selected values from the client. See value/2 (socket arity) for :respond_to.

Requests the listbox's current selected values from the client via push_event (listbox_value).

Components

listbox(assigns)

Attributes

  • id (:string) - The id of the listbox.

  • items (:list) (required) - Items from Corex.List.new/1 (or maps with :label and optional :value, 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.

  • 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 selection_mode is multiple.

    Defaults to false.

  • 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)

API

set_value(listbox_id, value)

Sets listbox selection from the client. Dispatches corex:listbox:set-value on the hook root.

set_value(socket, listbox_id, value)

Sets listbox selection from the server via push_event (listbox_set_value).

value(listbox_id)

Requests the listbox's current selected values from the client. See value/2 (socket arity) for :respond_to.

value(listbox_id, opts)

Requests the listbox's current selected values from the client via push_event (listbox_value).

The hook responds with listbox_value_response and/or dispatches listbox-value depending on :respond_to.

Functions

value(socket, listbox_id, opts)