defmodule PhoenixKitWeb.Components.LayoutWrapper do
@moduledoc """
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:
## Configuration
Configure parent layout in config.exs:
config :phoenix_kit,
layout: {MyAppWeb.Layouts, :app}
"""
use Phoenix.Component
use PhoenixKitWeb, :verified_routes
import PhoenixKitWeb.CoreComponents, only: [flash_group: 1]
alias PhoenixKit.Users.Auth.Scope
alias PhoenixKit.Utils.PhoenixVersion
alias PhoenixKit.Utils.Routes
@doc """
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
"""
attr :flash, :map, default: %{}
attr :phoenix_kit_current_scope, :any, default: nil
attr :phoenix_kit_current_user, :any, default: nil
attr :page_title, :string, default: nil
attr :current_path, :string, default: nil
attr :inner_content, :string, default: nil
attr :project_title, :string, default: "PhoenixKit"
slot :inner_block, required: false
def app_layout(assigns) do
# Handle both inner_content (Phoenix 1.7-) and inner_block (Phoenix 1.8+)
assigns = normalize_content_assigns(assigns)
# For admin pages, render simplified layout without parent headers
if admin_page?(assigns) do
render_admin_only_layout(assigns)
else
case get_layout_config() do
{module, function} when is_atom(module) and is_atom(function) ->
render_with_parent_layout(assigns, module, function)
nil ->
render_with_phoenix_kit_layout(assigns)
end
end
end
## Private Implementation
# Normalize content assigns to handle both inner_content and inner_block
defp normalize_content_assigns(assigns) do
# If we have inner_content but no inner_block, create inner_block from inner_content
if assigns[:inner_content] && (!assigns[:inner_block] || assigns[:inner_block] == []) do
inner_content = assigns[:inner_content]
# Create a synthetic inner_block slot
inner_block = [
%{
inner_block: fn _slot_assigns, _index ->
Phoenix.HTML.raw(inner_content)
end
}
]
Map.put(assigns, :inner_block, inner_block)
else
# If we have inner_block but no inner_content, leave as is
assigns
end
end
# Check if current page is an admin page that needs navigation
defp admin_page?(assigns) do
case assigns[:current_path] do
nil -> false
path when is_binary(path) -> String.contains?(path, "/admin/")
_ -> false
end
end
# Wrap inner_block with admin navigation if needed
defp wrap_inner_block_with_admin_nav_if_needed(assigns) do
if admin_page?(assigns) do
# Import AdminNav functions for use in template
import PhoenixKitWeb.AdminNav
# Import Scope for user info
alias PhoenixKit.Users.Auth.Scope
# Create new inner_block slot that wraps original content with admin navigation
original_inner_block = assigns[:inner_block]
new_inner_block = [
%{
inner_block: fn _slot_assigns, _index ->
# Create template assigns with needed values
template_assigns = %{
original_inner_block: original_inner_block,
current_path: assigns[:current_path],
phoenix_kit_current_scope: assigns[:phoenix_kit_current_scope],
project_title: assigns[:project_title] || "PhoenixKit"
}
assigns = template_assigns
~H"""
{@project_title} Admin
<.admin_theme_controller mobile={true} />
{render_slot(@original_inner_block)}
"""
end
}
]
# Return assigns with new inner_block
assign(assigns, :inner_block, new_inner_block)
else
# Not an admin page, return assigns unchanged
assigns
end
end
# Render with parent application layout (Phoenix v1.8+ function component approach)
defp render_with_parent_layout(assigns, module, function) do
# Prepare assigns for parent layout compatibility
assigns = prepare_parent_layout_assigns(assigns)
# Dynamically call the parent layout function based on Phoenix version
case PhoenixVersion.get_strategy() do
:modern ->
render_modern_parent_layout(assigns, module, function)
:legacy ->
render_legacy_parent_layout(assigns, module, function)
end
end
# Phoenix v1.8+ approach - function components
defp render_modern_parent_layout(assigns, module, function) do
# Wrap inner content with admin navigation if needed
assigns = wrap_inner_block_with_admin_nav_if_needed(assigns)
# Use apply/3 to dynamically call the parent layout function
apply(module, function, [assigns])
rescue
UndefinedFunctionError ->
# Fallback to PhoenixKit layout if parent function doesn't exist
render_with_phoenix_kit_layout(assigns)
end
# Phoenix v1.7- approach - templates (legacy support)
defp render_legacy_parent_layout(assigns, _module, _function) do
# For legacy Phoenix, layouts are handled at router level
# Wrap inner content with admin navigation if needed
assigns = wrap_inner_block_with_admin_nav_if_needed(assigns)
# Just render content without wrapper - layout comes from router
~H"""
{render_slot(@inner_block)}
"""
end
# Render admin pages with simplified layout (no parent headers)
defp render_admin_only_layout(assigns) do
# Wrap inner content with admin navigation
assigns = wrap_inner_block_with_admin_nav_if_needed(assigns)
~H"""
<.live_title default={"#{assigns[:project_title] || "PhoenixKit"} Admin"}>
{assigns[:page_title] || "Admin"}
<.flash_group flash={@flash} />
{render_slot(@inner_block)}
"""
end
# Fallback to PhoenixKit's own layout
defp render_with_phoenix_kit_layout(assigns) do
# Wrap inner content with admin navigation if needed
assigns = wrap_inner_block_with_admin_nav_if_needed(assigns)
~H"""
{render_slot(@inner_block)}
"""
end
# Prepare assigns for parent layout compatibility
defp prepare_parent_layout_assigns(assigns) do
assigns
|> Map.put_new(:current_user, get_current_user_for_parent(assigns))
|> Map.put_new(:phoenix_kit_integrated, true)
|> Map.put_new(:phoenix_kit_version, get_phoenix_kit_version())
|> Map.put_new(:phoenix_version_info, PhoenixVersion.get_version_info())
end
# Prepare assigns specifically for PhoenixKit layout
defp prepare_phoenix_kit_assigns(assigns) do
assigns
|> Map.put_new(:phoenix_kit_standalone, true)
end
# Extract current user from scope for parent layout compatibility
defp get_current_user_for_parent(assigns) do
case assigns[:phoenix_kit_current_scope] do
nil -> assigns[:phoenix_kit_current_user]
scope -> Scope.user(scope)
end
end
# Get layout configuration from application environment with Phoenix version compatibility
defp get_layout_config do
case Application.get_env(:phoenix_kit, :phoenix_version_strategy) do
:modern ->
# Phoenix v1.8+ - get layouts_module and assume :app function
case Application.get_env(:phoenix_kit, :layouts_module) do
nil -> nil
module -> {module, :app}
end
:legacy ->
# Phoenix v1.7- - use legacy layout config
Application.get_env(:phoenix_kit, :layout)
nil ->
# Fallback - check for legacy layout config first
Application.get_env(:phoenix_kit, :layout)
end
end
# Get PhoenixKit version
defp get_phoenix_kit_version do
case Application.spec(:phoenix_kit) do
nil ->
"unknown"
spec ->
spec
|> Keyword.get(:vsn, "unknown")
|> to_string()
end
end
end