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>.
| Function | Action | Returns |
|---|---|---|
set_value/2 | Set selection (client) | %Phoenix.LiveView.JS{} |
set_value/3 | Set selection (server) | socket |
value/1 | Read selection (client) | %Phoenix.LiveView.JS{} |
value/2 | Read selection (client, opts) | %Phoenix.LiveView.JS{} |
value/3 | Read selection (server) | socket |
value/4 | Read 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"])}
endvalue
<.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")}
endEvents
Server events
| Event | When | Payload |
|---|---|---|
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)}
endClient events
| Event | When | event.detail |
|---|---|---|
on_value_change_client="listbox-value-changed" | Selection changes | id, 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
endControlled
<.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
Attributes
id(:string) - The id of the listbox.items(:list) (required) - Items fromCorex.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 tofalse.disabled(:boolean) - Whether the listbox is disabled. Defaults tofalse.dir(:string) - Text direction. Defaults tonil. Must be one ofnil,"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 tofalse.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 tofalse.deselectable(:boolean) - Whether selection can be cleared. Defaults tofalse.typeahead(:boolean) - Enable typeahead search. Defaults tofalse.on_value_change(:string) - Server event name on value change. Defaults tonil.on_value_change_client(:string) - Client event name on value change. Defaults tonil.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 ifselection_modeis multiple.Defaults to
false.aria_label(:string) - Accessible name when no label slot is provided. Defaults tonil.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 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 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
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.
| Reply | Payload | |
|---|---|---|
| Server | listbox_value_response | %{"id" => id, "value" => selection} |
| Client | listbox-value on the root | same 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
Read selected values from handle_event (listbox_value). Same replies as value/2.
| Reply | Payload |
|---|---|
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