Packages

WebSocket-first Elixir framework with auto-generated HTTP MCP APIs

Current section

Files

Jump to
dialup lib layout.ex
Raw

lib/layout.ex

defmodule Dialup.Layout do
@moduledoc """
The behaviour and macro for Dialup layout modules.
A layout wraps one or more pages with shared chrome (navigation, footer, etc.).
Layouts are discovered automatically based on the directory hierarchy:
app/
├── layout.ex # wraps all pages under app/
└── blog/
├── layout.ex # wraps all pages under app/blog/ (nested)
└── [slug]/
└── page.ex
## Usage
defmodule MyApp.App.Layout do
use Dialup.Layout
# Called once when the WebSocket connection is established.
# Use it to set session-scoped data (e.g. the current user).
def mount(session) do
user = Repo.get(User, session[:user_id])
{:ok, Map.put(session, :current_user, user)}
end
def render(assigns) do
~H\"\"\"
<nav>{@current_user.name}</nav>
<main>{raw(@inner_content)}</main>
\"\"\"
end
end
## Callbacks
- `mount/1` — optional; called once when the WebSocket connection is established.
Receives the current session map (empty `%{}` for the root layout, or whatever the
parent layout returned). Return `{:ok, new_session}`.
- `render/1` — renders the layout HTML using HEEx. Use `{raw(@inner_content)}` to
inject the child content.
## Colocation CSS
Place a `layout.css` file in the same directory as `layout.ex`. The styles are
automatically scoped and applied to all pages under that directory at compile time.
## Nesting
Layout `mount/1` calls are chained from outermost to innermost. Each layout receives
the session returned by its parent and may enrich it further.
"""
@callback render(assigns :: map()) :: Phoenix.LiveView.Rendered.t()
# session :: 親レイアウトが設定したsession(rootレイアウトは %{} を受け取る)
@callback mount(session :: map()) :: {:ok, map()}
@optional_callbacks mount: 1
defmacro __using__(_opts) do
quote do
@behaviour Dialup.Layout
import Phoenix.Component
import Phoenix.HTML, only: [raw: 1]
import Dialup.Page, only: [overwrite: 2, set_default: 2]
# デフォルト実装:何もしない(mountを定義しないlayoutはsessionに何も追加しない)
def mount(session), do: {:ok, session}
defoverridable mount: 1
@before_compile Dialup.Layout
end
end
defmacro __before_compile__(env) do
template_path = env.file |> Path.rootname(".ex") |> Kernel.<>(".html.heex")
has_render = Module.defines?(env.module, {:render, 1})
has_template = File.exists?(template_path)
render_quote =
cond do
has_render ->
quote do
end
has_template ->
source = File.read!(template_path)
compiled =
EEx.compile_string(source,
engine: Phoenix.LiveView.TagEngine,
line: 1,
file: template_path,
caller: __CALLER__,
source: source,
tag_handler: Phoenix.LiveView.HTMLEngine
)
quote do
@external_resource unquote(template_path)
def render(assigns) do
_ = assigns
unquote(compiled)
end
end
true ->
raise CompileError,
file: env.file,
line: env.line,
description:
"#{inspect(env.module)} must define render/1 or provide #{Path.basename(template_path)}"
end
css_path = env.file |> Path.rootname(".ex") |> Kernel.<>(".css")
css_quote =
if File.exists?(css_path) do
css_content = File.read!(css_path)
scope_class =
"d-" <>
(env.module
|> Atom.to_string()
|> :erlang.md5()
|> Base.encode16(case: :lower)
|> binary_part(0, 7))
scoped_css = ".#{scope_class} {\n#{css_content}\n}"
quote do
@external_resource unquote(css_path)
def __css__, do: unquote(scoped_css)
def __css_scope__, do: unquote(scope_class)
end
else
quote do
def __css__, do: nil
def __css_scope__, do: nil
end
end
quote do
unquote(render_quote)
unquote(css_quote)
end
end
end