9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirDesktopで作るブログアプリ モバイル ナビゲーション作成

Last updated at Posted at 2025-12-25

はじめに

この記事はElixirアドベントカレンダー2025シリーズ2の22日目の記事です。

今回はUIを調整していきます

ヘッダーメニューの作成

最初に既存のヘッダーメニューを消します

lib/blog_app_web/components/layouts/root.html.heex
  ...
  <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
をそれぞれ設定します

lib/blog_app_web/components/layouts.ex
  @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

一覧画面はこのようになります

スクリーンショット 2025-12-25 23.47.12.png

各所に適応させていきます

lib/blog_app_web/live/post_live/form.ex
  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

スクリーンショット 2025-12-25 23.48.00.png

edit

スクリーンショット 2025-12-25 23.48.23.png

詳細画面を適応させます、元々のheaderのactionsを新headerにいれます

lib/blog_app_web/live/post_live/show.ex
  @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

スクリーンショット 2025-12-25 23.56.38.png

ボトムタブ

下のタブナビゲーションを作ります

使うコンポーネントはこちら

タブのリンクをリストにしてforで回します、表示するタブ名と同じだったらactive状態にします
use_bottomはの表示を管理するフラグになります

lib/blog_app_web/components/layouts.ex
  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はの表示を管理するフラグになります

lib/blog_app_web/components/layouts.ex
+ # 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を設定します

lib/blog_app_web/live/user_live/settings.ex
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

スクリーンショット 2025-12-26 1.08.56.png

Setting

スクリーンショット 2025-12-26 1.09.21.png

これでボトムタブができました

ユーザー登録画面でボトムタブは表示したくないの非表示にします

lib/blog_app_web/live/user_live/registration.ex
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

スクリーンショット 2025-12-26 3.10.05.png

最後に

今回はヘッダーナビゲーションとボトムタブナビゲーションを実装しました

次は削除モーダルとユーザー削除を実装します

本記事は以上になります、ありがとうございました

9
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?