Corex.Listbox (Corex v0.1.0-rc.0)

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.

Anatomy

Minimal

<.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>Choose a country</:label>
</.listbox>

With indicator

<.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>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: "Japan", value: "jpn", group: "Asia"},
    %{label: "China", value: "chn", group: "Asia"},
    %{label: "USA", value: "usa", group: "North America"}
  ])
}>
  <:label>Choose a country</:label>
  <:item_indicator><.heroicon name="hero-check" /></:item_indicator>
</.listbox>

Custom item slot

Requires Flagpack. Use :item with :let={%{item: entry}}.

<.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(entry.value)} />
    {entry.label}
  </:item>
  <:item_indicator><.heroicon name="hero-check" /></:item_indicator>
</.listbox>

API

Requires a stable id on <.listbox>.

FunctionActionReturns
set_value/2Set selection (client)%Phoenix.LiveView.JS{}
set_value/3Set selection (server)socket
value/1Read selection (client)%Phoenix.LiveView.JS{}
value/2Read selection (client, opts)%Phoenix.LiveView.JS{}
value/3Read selection (server)socket
value/4Read selection (server, opts)socket

set_value

<.action phx-click={Corex.Listbox.set_value("listbox-api-sv-client", ["bel"])} class="button button--sm">
  Belgium
</.action>
<.listbox id="listbox-api-sv-client" class="listbox" items={
  Corex.List.new([
    %{label: "France", value: "fra"},
    %{label: "Belgium", value: "bel"},
    %{label: "Germany", value: "deu"}
  ])
}>
  <:label>Choose a country</:label>
  <:item_indicator><.heroicon name="hero-check" /></:item_indicator>
</.listbox>
def handle_event("listbox_api_set_value", _params, socket) do
  {:noreply, Corex.Listbox.set_value(socket, "listbox-api-sv-server", ["bel"])}
end

value

<.action phx-click={Corex.Listbox.value("listbox-api-val-client")} class="button button--sm">
  Read selection
</.action>
def handle_event("listbox_api_value_server", _params, socket) do
  {:noreply, Corex.Listbox.value(socket, "listbox-api-val-server")}
end

Events

Server events

EventWhenPayload
on_value_change="listbox_value_changed"Selection changes%{"id" => id, "value" => values} — list of selected value strings

on_value_change

<.listbox
  class="listbox"
  items={
    Corex.List.new([
      %{label: "France", value: "fra"},
      %{label: "Belgium", value: "bel"},
      %{label: "Germany", value: "deu"}
    ])
  }
  on_value_change="listbox_value_changed"
>
  <:label>Choose a country</:label>
  <:item_indicator><.heroicon name="hero-check" /></:item_indicator>
</.listbox>
def handle_event("listbox_value_changed", %{"id" => id, "value" => value}, socket) do
  {:noreply, assign(socket, :selected, value)}
end

Client events

EventWhenevent.detail
on_value_change_client="listbox-value-changed"Selection changesid, value, items

on_value_change_client

<.listbox
  id="listbox-events-client"
  class="listbox"
  items={
    Corex.List.new([
      %{label: "France", value: "fra"},
      %{label: "Belgium", value: "bel"},
      %{label: "Germany", value: "deu"}
    ])
  }
  on_value_change_client="listbox-value-changed"
>
  <:label>Choose a country</:label>
  <:item_indicator><.heroicon name="hero-check" /></:item_indicator>
</.listbox>
document.getElementById("listbox-events-client")?.addEventListener("listbox-value-changed", (event) => {
  console.log(event.detail);
});

Patterns

Stream

Use Phoenix.LiveView.stream/3 to add or remove items at runtime. Keep a list assign in sync with the stream and pass Corex.List.new(@items_list) as items. Configure dom_id to match each item element id (listbox:stream-listbox:item:#{value}).

defmodule MyAppWeb.ListboxStreamLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    initial = [
      %{value: "1", label: "Apple"},
      %{value: "2", label: "Banana"},
      %{value: "3", label: "Cherry"}
    ]

    {:ok,
     socket
     |> stream_configure(:items, dom_id: &"listbox:stream-listbox:item:#{&1.value}")
     |> stream(:items, initial)
     |> assign(:items_list, initial)
     |> assign(:next_id, 4)}
  end

  def handle_event("add_item", _params, socket) do
    id = to_string(socket.assigns.next_id)
    item = %{value: id, label: "Item " <> id}

    {:noreply,
     socket
     |> stream_insert(:items, item)
     |> assign(:items_list, socket.assigns.items_list ++ [item])
     |> assign(:next_id, socket.assigns.next_id + 1)}
  end

  def render(assigns) do
    ~H"""
    <.listbox id="stream-listbox" class="listbox" items={Corex.List.new(@items_list)}>
      <:label>Choose an item</:label>
      <:empty>No items</:empty>
      <:item_indicator><.heroicon name="hero-check" /></:item_indicator>
    </.listbox>
    """
  end
end

Controlled

<.listbox
  class="listbox"
  controlled
  value={@value}
  on_value_change="listbox_controlled_changed"
  items={
    Corex.List.new([
      %{label: "France", value: "fra"},
      %{label: "Belgium", value: "bel"},
      %{label: "Germany", value: "deu"}
    ])
  }
>
  <:label>Choose a country</:label>
  <:item_indicator><.heroicon name="hero-check" /></:item_indicator>
</.listbox>
def handle_event("listbox_controlled_changed", %{"value" => value}, socket) do
  {:noreply, assign(socket, :value, value)}
end

Summary

API

Set the listbox selection from phx-click. Dispatches corex:listbox:set-value with detail.value (string list, wrapped if a single scalar is passed internally).

Set the listbox selection from handle_event (listbox_set_value).

Same as value/2 with default respond_to:.

Read selected values from phx-click. Dispatches corex:listbox:value. Optional respond_to: :server, :client, or :both.

Read selected values from handle_event (listbox_value). Same replies as value/2.

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)

Set the listbox selection from phx-click. Dispatches corex:listbox:set-value with detail.value (string list, wrapped if a single scalar is passed internally).

<.action phx-click={Corex.Listbox.set_value("my-listbox", ["fra"])}>Choose France</.action>
<.listbox id="my-listbox" class="listbox" items={
  Corex.List.new([
    %{label: "France", value: "fra"},
    %{label: "Belgium", value: "bel"}
  ])
}>
  <:label>Country</:label>
</.listbox>
document.getElementById("my-listbox")?.dispatchEvent(
  new CustomEvent("corex:listbox:set-value", {
    bubbles: false,
    detail: { value: ["fra"] },
  })
);

set_value(socket, listbox_id, value)

Set the listbox selection from handle_event (listbox_set_value).

def handle_event("pick_country", %{"code" => c}, socket) do
  {:noreply, Corex.Listbox.set_value(socket, "my-listbox", [c])}
end

value(listbox_id)

Same as value/2 with default respond_to:.

value(listbox_id, opts)

Read selected values from phx-click. Dispatches corex:listbox:value. Optional respond_to: :server, :client, or :both.

ReplyPayload
Serverlistbox_value_response%{"id" => id, "value" => selection}
Clientlistbox-value on the rootsame fields in detail
<.action phx-click={Corex.Listbox.value("my-listbox")}>Read selection</.action>
<.listbox id="my-listbox" class="listbox" items={Corex.List.new([%{label: "A", value: "a"}])}>
  <:label>Pick</:label>
</.listbox>
def handle_event("listbox_value_response", %{"id" => _, "value" => v}, socket) do
  {:noreply, assign(socket, :picked, v)}
end

value(socket, listbox_id, opts)

Read selected values from handle_event (listbox_value). Same replies as value/2.

ReplyPayload
listbox_value_response%{"id" => id, "value" => selection}
def handle_event("read_listbox", _, socket) do
  {:noreply, Corex.Listbox.value(socket, "my-listbox", respond_to: :server)}
end