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 @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 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] } assigns = template_assigns ~H"""
PhoenixKit 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="PhoenixKit Admin"> {assigns[:page_title] || "Admin"}