はじめに
この記事はElixirアドベントカレンダー2025シリーズ2の22日目の記事です。
今回はUIを調整していきます
ヘッダーメニューの作成
最初に既存のヘッダーメニューを消します
...
<body>
- <ul class="menu menu-horizontal w-full relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
- <%= if @current_scope do %>
- <li>
- {@current_scope.user.email}
- </li>
- <li>
- <.link href={~p"/users/settings"}>Settings</.link>
- </li>
- <li>
- <.link href={~p"/users/log-out"} method="delete">Log out</.link>
- </li>
- <% else %>
- <li>
- <.link href={~p"/users/register"}>Register</.link>
- </li>
- <li>
- <.link href={~p"/users/log-in"}>Log in</.link>
- </li>
- <% end %>
- </ul>
{@inner_content}
</body>
</html>
コンポーネントはモバイルのよくあるヘッダーなこちらを使います
既存のLayouts.appコンポーネントにヘッダーをつけます
左側に戻るボタンを入れる back slot
真ん中はページタイトルを入れる title_page attr
右は編集などアクションボタンを入れる actions slot
をそれぞれ設定します
@doc """
Renders your app layout.
This function is typically invoked from every template,
and it often contains your application menu, sidebar,
or similar.
## Examples
<Layouts.app flash={@flash}>
<h1>Content</h1>
</Layouts.app>
"""
attr :flash, :map, required: true, doc: "the map of flash messages"
attr :current_scope, :map,
default: nil,
doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
slot :inner_block, required: true
+ # header
+ attr :page_title, :string, default: ""
+ attr :header, :boolean, default: true
+ slot :back, doc: "add back navigation within .link component"
+ slot :actions, doc: "add action navigation such as add, edit and etc... within .link component"
def app(assigns) do
~H"""
+ <header class="navbar bg-primary text-white shadow-sm">
+ <div class="navbar-start">
+ <span :if={@back != []} class="normal-case text-md ml-1">
+ {render_slot(@back)}
+ </span>
+ </div>
+ <div class="navbar-center text-2xl">
+ <%= if @page_title == "" do %>
+ <h1>BlogApp</h1>
+ <% else %>
+ <h1>{@page_title}</h1>
+ <% end %>
+ </div>
+ <div class="navbar-end">
+ <span :if={@actions != []} class="normal-case text-xl mr-1">
+ {render_slot(@actions)}
+ </span>
+ </div>
+ </header>
<main class="p-4 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl space-y-4">
{render_slot(@inner_block)}
</div>
</main>
<.flash_group flash={@flash} />
"""
end
一覧画面はこのようになります
各所に適応させていきます
def render(assigns) do
~H"""
- <Layouts.app flash={@flash} current_scope={@current_scope}>
+ <Layouts.app page_title={@page_title} flash={@flash} current_scope={@current_scope}>
+ <:back>
+ <.link navigate={return_path(@current_scope, @return_to, @post)}>Back</.link>
+ </:back>
- <.header>
- {@page_title}
- <:subtitle>Use this form to manage post records in your database.</:subtitle>
- </.header>
<.form for={@form} id="post-form" phx-change="validate" phx-submit="save">
<.input field={@form[:text]} type="text" label="Text" />
<footer>
<.button phx-disable-with="Saving..." variant="primary">Save Post</.button>
<.button navigate={return_path(@current_scope, @return_to, @post)}>Cancel</.button>
</footer>
</.form>
</Layouts.app>
"""
end
new
edit
詳細画面を適応させます、元々のheaderのactionsを新headerにいれます
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
+ <:back>
+ <.link navigate={~p"/posts"}>Back</.link>
+ </:back>
+ <:actions>
+ <.button navigate={~p"/posts/#{@post}/edit?return_to=show"}>
+ <.icon name="hero-pencil-square" /> Edit
+ </.button>
+ </:actions>
<.header>
Post {@post.id}
<:subtitle>This is a post record from your database.</:subtitle>
- <:actions>
- <.button navigate={~p"/posts"}>
- <.icon name="hero-arrow-left" />
- </.button>
- <.button variant="primary" navigate={~p"/posts/#{@post}/edit?return_to=show"}>
- <.icon name="hero-pencil-square" /> Edit post
- </.button>
- </:actions>
</.header>
<.list>
<:item title="Text">{@post.text}</:item>
</.list>
</Layouts.app>
"""
end
ボトムタブ
下のタブナビゲーションを作ります
使うコンポーネントはこちら
タブのリンクをリストにしてforで回します、表示するタブ名と同じだったらactive状態にします
use_bottomはの表示を管理するフラグになります
attr :name, :atom, default: :home
def bottom_tab(assigns) do
~H"""
<div class="dock dock-lg">
<%= for {name, title, link, icon} <- links() do %>
<.link navigate={link} class={if name == @name, do: "dock-active", else: ""}>
<.icon name={icon} class="w-6 h-6" />
<span class="text-xs mt-1">
{title}
</span>
</.link>
<% end %>
</div>
"""
end
defp links() do
[
{:home, "ホーム", "/posts", "hero-home"},
{:setting, "設定", "/settings", "hero-cog-6-tooth"}
]
end
作ったらLayouts.appの下の方に追加します
use_bottomはの表示を管理するフラグになります
+ # bottom
+ attr :name, :atom, default: :home
+ attr :use_bottom, :boolean, default: true
def app(assigns) do
~H"""
<header class="navbar bg-primary text-white shadow-sm">
...
</header>
<main class="p-4 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl space-y-4">
{render_slot(@inner_block)}
</div>
</main>
+ <.bottom_tab :if={@use_bottom} name={@name} />
<.flash_group flash={@flash} />
"""
end
settings.exを以下のように変更します、パスワードは使わないのでごっそり削ります
Layouts.appにname,page_titleを設定します
defmodule BlogAppWeb.UserLive.Settings do
use BlogAppWeb, :live_view
alias BlogApp.Accounts
@impl true
def render(assigns) do
~H"""
<Layouts.app
name={:setting}
page_title="Settings"
flash={@flash}
current_scope={@current_scope}
>
<.form for={@email_form} id="email_form" phx-submit="update_email" phx-change="validate_email">
<.input
field={@email_form[:email]}
type="email"
label="Email"
autocomplete="username"
required
/>
<.button variant="primary" phx-disable-with="Changing...">Change Email</.button>
</.form>
<div class="divider" />
</Layouts.app>
"""
end
@impl true
def mount(%{"token" => token}, _session, socket) do
socket =
case Accounts.update_user_email(socket.assigns.current_scope.user, token) do
{:ok, _user} ->
put_flash(socket, :info, "Email changed successfully.")
{:error, _} ->
put_flash(socket, :error, "Email change link is invalid or it has expired.")
end
{:ok, push_navigate(socket, to: ~p"/users/settings")}
end
def mount(_params, _session, socket) do
user = socket.assigns.current_scope.user
email_changeset = Accounts.change_user_email(user, %{}, validate_unique: false)
socket =
socket
|> assign(:current_email, user.email)
|> assign(:email_form, to_form(email_changeset))
|> assign(:trigger_submit, false)
{:ok, socket}
end
@impl true
def handle_event("validate_email", params, socket) do
%{"user" => user_params} = params
email_form =
socket.assigns.current_scope.user
|> Accounts.change_user_email(user_params, validate_unique: false)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, email_form: email_form)}
end
def handle_event("update_email", params, socket) do
%{"user" => user_params} = params
user = socket.assigns.current_scope.user
case Accounts.change_user_email(user, user_params) do
%{valid?: true} = _changeset ->
info = "A link to confirm your email change has been sent to the new address."
{:noreply, socket |> put_flash(:info, info)}
changeset ->
{:noreply, assign(socket, :email_form, to_form(changeset, action: :insert))}
end
end
end
Home
Setting
これでボトムタブができました
ユーザー登録画面でボトムタブは表示したくないの非表示にします
defmodule BlogAppWeb.UserLive.Registration do
use BlogAppWeb, :live_view
@impl true
def render(assigns) do
~H"""
- <Layouts.app flash={@flash} current_scope={@current_scope}>
+ <Layouts.app use_bottom={false} flash={@flash} current_scope={@current_scope}>
"""
end
最後に
今回はヘッダーナビゲーションとボトムタブナビゲーションを実装しました
次は削除モーダルとユーザー削除を実装します
本記事は以上になります、ありがとうございました






