Xenia.Layout behaviour (Xenia v0.1.1)

Copy Markdown View Source

A layout in a Xenia app folder: a frame around every page in its folder and below.

Layouts nest by folder, not by declaration. A layout.ex applies to every page in its folder and below, inside the layouts of its ancestor folders. See Xenia.Routes.Discovery for the folder conventions, and Xenia.Page for how a page is rendered inside its layouts.

# app/(auth)/layout.ex
defmodule MyAppWeb.AuthLayout do
  use MyAppWeb, :html
  use Xenia.Layout, on_mount: {MyAppWeb.Auth, :require_user}

  def frame(assigns) do
    ~H"""
    <div class="flex">
      <aside>{@persistent}</aside>
      <main>{@inner_content}</main>
    </div>
    """
  end
end

The frame

frame/1 is re-rendered on every navigation. It receives the page's assigns, plus:

  • @inner_content - the page, already inside the layouts below this one
  • @persistent - this layout's persistent part, or nil; place it where it belongs

The persistent part

A persistent.ex next to the layout.ex is the layout's persistent part: a LiveView (use Xenia.Persistent) that stays mounted, with its state and its JavaScript, while the user moves between pages under this layout. The frame places it with {@persistent}. Persistent parts are rendered side by side, never one inside another, and survive only within one live_session (CLAUDE.md §9, experiment S1).

Options

  • :on_mount - on_mount hooks run when any page under this layout mounts, outermost layout first, like live_session's. This is where a route group gets its authentication: a folder name grants nothing. Takes a module, {module, argument}, or a list of them.
  • :sitemap - false leaves every page under this layout out of Xenia.Sitemap, for example pages behind a sign-in. Default true.

Metadata

A layout can define metadata/1: the title, description and social tags for the pages under it, which they can override (see Xenia.Metadata).

def metadata(_assigns), do: %{title: "Acme", title_template: "%s · Acme"}

Summary

Callbacks

Renders the layout's frame around @inner_content, the page. @persistent is this layout's persistent part, or nil.

Metadata for the pages under this layout (see Xenia.Metadata).

Functions

Returns true if module is a Xenia layout.

Returns the on_mount hooks a layout declares, as {module, argument}.

The DOM id of a layout's persistent part. Stable per layout, so every page under the layout renders "the same" sticky LiveView, and LiveView keeps it across navigation.

Whether pages under the layout belong in the sitemap (its :sitemap option). Raises ArgumentError if module is not a Xenia layout.

Callbacks

frame(assigns)

@callback frame(assigns :: map()) :: Phoenix.LiveView.Rendered.t()

Renders the layout's frame around @inner_content, the page. @persistent is this layout's persistent part, or nil.

metadata(assigns)

(optional)
@callback metadata(assigns :: map()) :: map()

Metadata for the pages under this layout (see Xenia.Metadata).

Functions

layout?(module)

@spec layout?(term()) :: boolean()

Returns true if module is a Xenia layout.

on_mount(module)

@spec on_mount(module()) :: [{module(), term()}]

Returns the on_mount hooks a layout declares, as {module, argument}.

Raises ArgumentError if module is not a Xenia layout.

persistent_id(module)

@spec persistent_id(module()) :: String.t()

The DOM id of a layout's persistent part. Stable per layout, so every page under the layout renders "the same" sticky LiveView, and LiveView keeps it across navigation.

sitemap?(module)

@spec sitemap?(module()) :: boolean()

Whether pages under the layout belong in the sitemap (its :sitemap option). Raises ArgumentError if module is not a Xenia layout.