Corex. Menu
(Corex v0.1.0-rc.1)
View Source
Phoenix implementation of 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>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 button--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 |
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
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 },
})
);
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