Corex.Menu (Corex v0.1.0-rc.0)

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 via window.location (safe everywhere)
  • :patch - LiveView js().patch(url) (caller asserts: same LV mount + matching live route)
  • :navigate - LiveView js().navigate(url) (caller asserts: another LV in the same live_session)
  • false - disable redirect for this item (e.g. let your on_select server 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
end

API

Requires a stable id on <.menu>.

FunctionActionReturns
set_open/2Set open state (client)%Phoenix.LiveView.JS{}
set_open/3Set 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)}
end

Events

Server events

EventWhenPayload
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}
end

on_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

EventWhenevent.detail
on_select_client="menu-selected"Item selectedid, value
on_open_change_client="menu-open-changed"Open state changesid, 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_open(menu_id, open)

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(socket, menu_id, open)

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