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
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={[]}>
</.listbox>Learn more about modifiers and Corex Design
Summary
Components
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 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.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)