Corex. Menu
(Corex v0.2.0)
View Source
Menu for Phoenix LiveView. Behavior follows Zag.js Menu.
Anatomy
List
You must use Corex.Tree.Item struct for items.
The value for each item is optional in maps passed to Corex.Tree.new/1 (auto-generated when omitted).
You can specify disabled for each item and nested children.
<.menu
class="menu"
items={[
%Corex.Tree.Item{
value: "edit",
label: "Edit"
},
%Corex.Tree.Item{
value: "duplicate",
label: "Duplicate"
},
%Corex.Tree.Item{
value: "delete",
label: "Delete"
}
]}
>
<:trigger>Actions</:trigger>
<:indicator>
<.heroicon name="hero-chevron-down" />
</:indicator>
</.menu>Nested Menu
Use children in Corex.Tree.Item to create nested menus.
<.menu
class="menu"
items={[
%Corex.Tree.Item{
value: "new-tab",
label: "New tab"
},
%Corex.Tree.Item{
value: "share",
label: "Share",
children: [
%Corex.Tree.Item{
value: "messages",
label: "Messages"
},
%Corex.Tree.Item{
value: "airdrop",
label: "Airdrop"
},
%Corex.Tree.Item{
value: "whatsapp",
label: "WhatsApp"
}
]
},
%Corex.Tree.Item{
value: "print",
label: "Print"
}
]}
>
<:trigger>Click me</:trigger>
</.menu>Nested Menu with Custom Indicator
Use the :nested_indicator slot to customize the indicator shown on items with nested menus (defaults to arrow right →).
<.menu
class="menu"
items={[
%Corex.Tree.Item{
value: "share",
label: "Share",
children: [
%Corex.Tree.Item{value: "messages", label: "Messages"}
]
}
]}
>
<:trigger>Click me</:trigger>
<:nested_indicator>
<.heroicon name="hero-arrow-right" />
</:nested_indicator>
</.menu>Custom item
Use the :item slot to customize each menu row (for example icons beside labels).
<.menu
class="menu"
items={[
%Corex.Tree.Item{value: "fra", label: "France"},
%Corex.Tree.Item{value: "bel", label: "Belgium"},
%Corex.Tree.Item{value: "deu", label: "Germany"}
]}
>
<:item :let={item}>
<Flagpack.flag name={String.to_existing_atom(to_string(item.value))} />
{item.label}
</:item>
<:trigger>Actions</:trigger>
<:indicator>
<.heroicon name="hero-chevron-down" />
</:indicator>
</.menu>Grouped Items
Use group in Corex.Tree.Item to group related items. The group value is used as the section label (same as select).
<.menu
class="menu"
items={[
%Corex.Tree.Item{
value: "edit",
label: "Edit",
group: "Actions"
},
%Corex.Tree.Item{
value: "duplicate",
label: "Duplicate",
group: "Actions"
},
%Corex.Tree.Item{
value: "account-1",
label: "Account 1",
group: "Accounts"
},
%Corex.Tree.Item{
value: "account-2",
label: "Account 2",
group: "Accounts"
}
]}
>
<:trigger>Actions</:trigger>
<:indicator>
<.heroicon name="hero-chevron-down" />
</:indicator>
</.menu>Patterns
Navigation
Set redirect on the component so selecting an item navigates to the item's value (e.g. path).
Per item, choose the navigation kind explicitly via the item's :redirect field:
:href(default) - full page redirect viawindow.location(safe everywhere):patch- LiveViewjs().patch(url)(caller asserts: same LV mount + matching live route):navigate- LiveViewjs().navigate(url)(caller asserts: another LV in the samelive_session)false- disable redirect for this item (e.g. let youron_selectserver handler decide)
Set new_tab: true on an item to open its destination in a new tab via window.open.
Breaking change
Earlier versions were no-ops when LiveView was connected (the server
handler was expected to call redirect/2). The hook now performs a hard
:href redirect by default. Opt back into the old behavior by setting
the per-item redirect: false, or opt into LV-aware navigation with
redirect: :patch / redirect: :navigate.
Controller
When not connected to LiveView, the hook always performs a full page redirect via window.location.
<.menu
class="menu"
redirect
items={[
%Corex.Tree.Item{value: "/", label: "Home"},
%Corex.Tree.Item{value: "/docs", label: "Docs"},
%Corex.Tree.Item{value: "https://example.com", label: "External", new_tab: true}
]}
>
<:trigger>Navigate</:trigger>
<:indicator>
<.heroicon name="hero-chevron-down" />
</:indicator>
</.menu>LiveView
When connected to LiveView, use on_select and redirect in the callback. The payload includes value (the item value).
defmodule MyAppWeb.NavMenuLive do
use MyAppWeb, :live_view
def handle_event("handle_select", %{"value" => value}, socket) do
{:noreply, push_navigate(socket, to: value)}
end
def render(assigns) do
~H"""
<.menu
class="menu"
redirect
on_select="handle_select"
items={[
%Corex.Tree.Item{value: "/", label: "Home"},
%Corex.Tree.Item{value: "/docs", label: "Docs"}
]}
>
<:trigger>Navigate</:trigger>
<:indicator>
<.heroicon name="hero-chevron-down" />
</:indicator>
</.menu>
"""
end
endAPI
Requires a stable id on <.menu>.
| Function | Action | Returns |
|---|---|---|
set_open/2 | Set open state (client) | %Phoenix.LiveView.JS{} |
set_open/3 | Set open state (server) | socket |
set_open
<.action phx-click={Corex.Menu.set_open("menu-api", true)} class="button ui-size-sm">
Open Menu
</.action>def handle_event("open_menu", _, socket) do
{:noreply, Corex.Menu.set_open(socket, "menu-api", true)}
endEvents
Server events
| Event | When | Payload |
|---|---|---|
on_select="menu_selected" | Item selected | %{"id" => id, "value" => value} |
on_open_change="menu_open_changed" | Open state changes | %{"id" => id, "open" => open} |
on_select
<.menu
class="menu"
on_select="menu_selected"
items={[
%Corex.Tree.Item{value: "menu", label: "Menu"},
%Corex.Tree.Item{value: "combobox", label: "Combobox"},
%Corex.Tree.Item{value: "select", label: "Select"}
]}
>
<:trigger>Actions</:trigger>
<:indicator><.heroicon name="hero-chevron-down" /></:indicator>
</.menu>def handle_event("menu_selected", %{"value" => value}, socket) do
{:noreply, socket}
endon_open_change
<.menu
class="menu"
on_open_change="menu_open_changed"
items={[
%Corex.Tree.Item{value: "menu", label: "Menu"},
%Corex.Tree.Item{value: "combobox", label: "Combobox"},
%Corex.Tree.Item{value: "select", label: "Select"}
]}
>
<:trigger>Actions</:trigger>
<:indicator><.heroicon name="hero-chevron-down" /></:indicator>
</.menu>Client events
| Event | When | event.detail |
|---|---|---|
on_select_client="menu-selected" | Item selected | id, value |
on_open_change_client="menu-open-changed" | Open state changes | id, open |
Style
Import menu.css and stack modifiers on the host (class on <.menu>).
@import "../corex/corex.css";Axes: Semantic (ui-accent, ui-brand, ui-alert, ui-info, ui-success), Variant (ui-solid), Size (ui-size-sm … ui-size-xl), Radius (ui-rounded-*), Max height (ui-max-height-* on the host; clamps content). See the modifier guide.
Semantic modifiers set palette variables on triggers and selected items. Variant modifiers control trigger and content panel surface treatment. Default is subtle; add menu ui-solid for a filled trigger or panel.
Semantic
| Modifier | Classes |
|---|---|
| Default | menu |
| Accent | menu ui-accent |
| Brand | menu ui-brand |
| Alert | menu ui-alert |
| Info | menu ui-info |
| Success | menu ui-success |
Variant
Visual treatment of the trigger and content panel. Combine with a semantic modifier for palette-driven ink and fill.
| Modifier | Classes |
|---|---|
| Subtle (default) | menu or menu ui-accent |
| Solid | menu ui-accent ui-solid |
Size
| Modifier | Classes |
|---|---|
| SM | menu ui-size-sm |
| MD | menu ui-size-md |
| LG | menu ui-size-lg |
| XL | menu ui-size-xl |
Max height
Opt-in clamp on the menu content. Example: menu ui-max-height-xs.
Summary
Components
Renders a menu component.
API
Set menu open state from a control (phx-click). Targets the root with id menu:<id>.
Set open state from handle_event. Pushes menu_set_open.
Components
API
@spec set_open(String.t(), Corex.Value.coercible()) :: Phoenix.LiveView.JS.t()
Set menu open state from a control (phx-click). Targets the root with id menu:<id>.
<.action phx-click={Corex.Menu.set_open("my-menu", true)}>Open</.action>
<.menu id="my-menu" class="menu" items={[%Corex.Tree.Item{label: "Edit", value: "edit"}]}>
<:trigger>Actions</:trigger>
</.menu>document.querySelector('[id="menu:my-menu"]')?.dispatchEvent(
new CustomEvent("corex:menu:set-open", {
bubbles: false,
detail: { open: true },
})
);
@spec set_open(Phoenix.LiveView.Socket.t(), String.t(), Corex.Value.coercible()) :: Phoenix.LiveView.Socket.t()
Set open state from handle_event. Pushes menu_set_open.
<.action phx-click="open_menu">Open</.action>
<.menu id="my-menu" class="menu" items={[%Corex.Tree.Item{label: "Edit", value: "edit"}]}>
<:trigger>Actions</:trigger>
</.menu>def handle_event("open_menu", _, socket) do
{:noreply, Corex.Menu.set_open(socket, "my-menu", true)}
end