Corex.Menu (Corex v0.1.0-beta.5)

View Source

Phoenix implementation of Zag.js Menu.

Examples

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>

Use as 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 Control

In order to use the API, you must use an id on the component

Client-side

<button phx-click={Corex.Menu.set_open("my-menu", true)}>
  Open Menu
</button>

Server-side

def handle_event("open_menu", _, socket) do
  {:noreply, Corex.Menu.set_open(socket, "my-menu", true)}
end

Styling

Use data attributes to target elements:

[data-scope="menu"][data-part="root"] {}
[data-scope="menu"][data-part="trigger"] {}
[data-scope="menu"][data-part="positioner"] {}
[data-scope="menu"][data-part="content"] {}
[data-scope="menu"][data-part="item"] {}
[data-scope="menu"][data-part="separator"] {}
[data-scope="menu"][data-part="item-group"] {}
[data-scope="menu"][data-part="item-group-label"] {}

If you wish to use the default Corex styling, you can use the class menu 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/menu.css";

You can then use modifiers

<.menu class="menu menu--accent menu--lg">
</.menu>

Summary

Components

Renders a menu component.

API

Sets the menu open state from client-side. Returns a Phoenix.LiveView.JS command.

Sets the menu open state from server-side. Pushes a LiveView event.

Components

API

set_open(menu_id, open)

Sets the menu open state from client-side. Returns a Phoenix.LiveView.JS command.

Examples

<button phx-click={Corex.Menu.set_open("my-menu", true)}>
  Open Menu
</button>

set_open(socket, menu_id, open)

Sets the menu open state from server-side. Pushes a LiveView event.

Examples

def handle_event("open_menu", _params, socket) do
  socket = Corex.Menu.set_open(socket, "my-menu", true)
  {:noreply, socket}
end