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
endStyling
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
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
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).
The hook responds with listbox_value_response and/or dispatches listbox-value depending on :respond_to.