PhoenixKitWeb.Components.LayoutWrapper (phoenix_kit v2.15.0)

Copy Markdown View Source

Dynamic layout wrapper component for Phoenix v1.7- and v1.8+ compatibility.

This component automatically detects the Phoenix version and layout configuration to provide seamless integration with parent applications while maintaining backward compatibility.

Usage

Replace direct layout calls with the wrapper:

<%!-- OLD (Phoenix v1.7-) --%>
<%!-- Templates relied on router-level layout config --%>

<%!-- NEW (Phoenix v1.8+) --%>
<PhoenixKitWeb.Components.LayoutWrapper.app_layout flash={@flash}>
  <%!-- content --%>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>

Configuration

Configure parent layout in config.exs:

config :phoenix_kit,
  layout: {MyAppWeb.Layouts, :app}

Summary

Functions

Styles + pre-paint stamp for the admin sidebar's compact (icon-only) mode.

Renders content with the appropriate layout based on configuration and Phoenix version.

Rendering path for auth pages (login, register, reset, confirm, magic link, QR handoff, and the invite-only referral screen).

Renders a page's page_toolbar{Module, :fun} — with the LiveView's own assigns. Called by layouts/admin.html.heex, which is the only place that holds those assigns; the result goes into the :toolbar slot. Renders nothing when the page set no toolbar — and nothing, with a logged warning, when the pair names no fun/1: a page missing its toolbar beats the whole admin chrome raising on every render (the same policy nav_tabs applies to a dead tab).

Functions

admin_sidebar_compact_bootstrap(assigns)

Styles + pre-paint stamp for the admin sidebar's compact (icon-only) mode.

Render once, immediately BEFORE the sidebar markup. Everything about compact mode lives client-side, and deliberately so:

  • the sidebar is a function component, not a LiveView — there is no handle_event/3 owner for a phx-click, and giving one to every admin page (or bolting a global attach_hook onto the admin on_mount chain) would be a lot of machinery for a display preference;
  • it is a per-browser density choice, exactly like the theme, so it belongs in localStorage next to it rather than in a settings row;
  • a client-side toggle costs no round trip, and nothing for morphdom to fight over — the DOM is identical either way, only <html> changes.

Which makes the first paint the whole problem, and the reason this is a synchronous inline <script> rather than a hook in phoenix_kit.js:

  • it must run before the sidebar is parsed, or a viewer who chose compact gets a frame of the full-width menu on every load. An inline script placed above the markup does exactly that;
  • it must not depend on the host having re-run mix phoenix_kit.update to refresh its vendored phoenix_kit.js. Self-contained markup ships with the feature.

Same reasoning, and the same shape, as PhoenixKitWeb.Components.ThemeBootstrap — including the one-instance guard, since a host layout and the kit's own admin shell can both be on the page.

The CSS hides the label rather than removing it (clip-path, not display: none), so every link keeps its accessible name and the menu still reads correctly to a screen reader while collapsed.

app_layout(assigns)

Renders content with the appropriate layout based on configuration and Phoenix version.

Automatically handles:

  • Phoenix v1.8+ function component layouts
  • Phoenix v1.7- legacy layout configuration
  • Fallback to PhoenixKit layouts when no parent configured
  • Parent layout compatibility with PhoenixKit assigns

Attributes

  • flash - Flash messages (required)
  • phoenix_kit_current_scope - Current authentication scope (optional)
  • phoenix_kit_current_user - Current user (optional, for backwards compatibility)

Inner Block

  • inner_block - Content to render within the layout

Attributes

  • flash (:map) - Defaults to %{}.
  • socket (:any) - Defaults to nil.
  • phoenix_kit_current_scope (:any) - Defaults to nil.
  • phoenix_kit_current_user (:any) - Defaults to nil.
  • page_title (:string) - Defaults to nil.
  • page_subtitle (:string) - Defaults to nil.
  • page_section (:string) - Optional breadcrumb segment rendered between "Admin Panel" and page_title (e.g. "Users" on a user detail page). Desktop only — collapses along with the rest of the breadcrumb prefix on mobile. Defaults to nil.
  • page_section_path (:string) - Prefixed path (via PhoenixKit.Utils.Routes.path/1) the page_section crumb links to. Renders as plain text when omitted. Defaults to nil.
  • page_crumbs (:list) - Extra breadcrumb crumbs rendered between page_section and page_title, for pages nested deeper than one level (e.g. catalogue / category drill trails): [%{label: "Plumbing", path: "/…"}]. path is a push_navigate target; patch is a push_patch target for same-LiveView drill trails. Both are optional — omitted renders plain text. The last crumb stays visible below sm (the trail truncates from the left); earlier crumbs collapse with the section. Defaults to [].
  • page_action (:map) - Optional compact action button rendered right after the breadcrumb title: %{icon: "hero-plus", label: "New template", navigate: path}. Lets a page keep its primary create action without spending an in-content header row. label becomes the tooltip/aria-label; icon defaults to hero-plus. Navigation only — for a phx-click action (or anything needing phx-target), use the :action slot instead. ⚠️ Plugin LiveViews rendered through the admin layout can only use this map: the layout threads it as an assign, and a slot cannot travel that way. Defaults to nil.
  • current_path (:string) - Defaults to nil.
  • inner_content (:string) - Defaults to nil.
  • project_title (:string) - Defaults to nil.
  • show_admin_panel_label (:boolean) - Overrides the show_admin_panel_label setting for this render. nil (the default) reads the setting. Mirrors how project_title overrides Settings.get_project_title/0, and keeps the header renderable without a database. Defaults to nil.
  • current_locale (:string) - Defaults to nil.
  • from_layout (:boolean) - Defaults to false.
  • pk_pending_invitations (:list) - Defaults to [].
  • module_assigns (:map) - Module-supplied host-consumable assigns. Each key in this map is merged into the assigns set passed to the parent layout (Layouts.app), so a host's custom layout can read e.g. assigns[:phoenix_kit_publishing_translations] from publishing, or any other module-defined key. Plain conn.assigns don't reach a function-component layout — only declared attrs do — so this single map attribute is how modules thread arbitrary host-consumable data through the boundary without core having to declare each one explicitly. Defaults to %{}.

Slots

  • action - The same compact action button, for pages whose primary action is not a navigation — a phx-click, a JS command, anything needing phx-target. The map attribute cannot express those and cannot address a LiveComponent.

    Takes render priority over the page_action attribute. Content is wrapped in the same chip shell, and the contract is one compact control: a multi-action toolbar belongs in the page body, not the breadcrumb bar.

    ⚠️ Only reaches views calling app_layout/1 directly. Plugin LiveViews render through layouts/admin.html.heex, which threads page_action as an assign — slots do not travel through assigns — so those keep the map.

    <:action>
      <button phx-click="new_device" phx-target={@myself} title="Add device">
        <.icon name="hero-plus" class="w-4 h-4" />
      </button>
    </:action>
  • toolbar - Controls that belong to the page's identity, rendered in the breadcrumb bar right after the title — a status picker, the page's ⋮ menu. Rendered as given (no chip shell); distinct from page_action / :action, which stay the one compact create chip.

    Two ways in. A view calling app_layout/1 directly passes this slot. A plugin LiveView rendered through layouts/admin.html.heex cannot pass a slot, so it assigns page_toolbar: {Module, :fun} on its socket: the layout calls render_page_toolbar/1 with the LiveView's own assigns (change-tracked) and puts the result in this slot — phx-change / phx-click inside it reach the LiveView as usual, because the layout renders inside it. Embedded mounts have no breadcrumb bar, so a page that is also embeddable renders the same component in its body there.

    # in a plugin LiveView's mount:
    assign(socket, page_toolbar: {__MODULE__, :header_toolbar})
    # `def header_toolbar(assigns)` renders, with ~H, e.g. a
    # `<form id="status" phx-change="change_status">` select and the
    # page's `<.table_row_menu>`; both events land in handle_event/3.
  • inner_block

auth_layout(assigns)

Rendering path for auth pages (login, register, reset, confirm, magic link, QR handoff, and the invite-only referral screen).

Auth pages do not render inside the host's Layouts.app. That layout is where a mix phx.new app keeps its logo, framework version and off-site links, so wrapping sign-in in it put Phoenix Framework branding on the login page of every kit install — reported by more than one host. Admin already works this way (render_admin_with_parent/1 never calls the host's :app); auth was the outlier.

The host's root layout still applies, because PhoenixKitWeb.Integration never calls put_root_layout. That is where the document shell, assets and CSRF come from, and it is unaffected.

A host that genuinely wants its own chrome on sign-in opts back in:

config :phoenix_kit, auth_uses_host_layout: true

⚠️ Two things a host may notice when it does not: anything wired into the app layout rather than root — a cookie-consent banner, analytics, a theme toggle — stops appearing on auth pages only. Root-level wiring is unaffected. Stock phx.new puts assets and CSRF in root, so conventional hosts see only the branding disappear, which is the point.

Attributes

  • flash (:map) (required)
  • phoenix_kit_current_scope (:any) - Defaults to nil.
  • page_title (:string) - Defaults to nil.
  • current_path (:string) - Defaults to nil.
  • pk_pending_invitations (:list) - Defaults to [].

Slots

  • inner_block (required)

get_language_flag(code)

render_page_toolbar(assigns)

@spec render_page_toolbar(map()) :: Phoenix.LiveView.Rendered.t() | nil

Renders a page's page_toolbar{Module, :fun} — with the LiveView's own assigns. Called by layouts/admin.html.heex, which is the only place that holds those assigns; the result goes into the :toolbar slot. Renders nothing when the page set no toolbar — and nothing, with a logged warning, when the pair names no fun/1: a page missing its toolbar beats the whole admin chrome raising on every render (the same policy nav_tabs applies to a dead tab).