7
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で作るスマホアプリ Part 7 ナビゲーションコンポーネントの作成

Last updated at Posted at 2024-12-19

はじめに

この記事はElixirアドベントカレンダー2024のシリーズ2、11日目の記事です

本記事では以下のことを行います

  • ログイン・新規登録画面のデザイン修正
  • ナビゲーションコンポーネントの実装

今回の作業ブランチを作成します

git checkout -b feature/navigation_component

ログイン・新規登録画面のデザイン修正

不要な機能(Keep me logged in)等が残っているので修正していきます

ログイン状態の場合は以下のコマンドでDBをリセットするかトークンファイルを削除します

mix ecto.reset
rm ~/.config/trarecord/token

新規登録画面

render関数内を以下のように書き換えます

lib/trarecord_web/live/user_registration_live.ex:L7
  def render(assigns) do
    ~H"""
    <div id="register" class="mx-auto pt-12 h-screen w-[80vw]">
      <.header class="text-center">
        Sign Up
      </.header>

      <.simple_form
        for={@form}
        id="registration_form"
        phx-submit="save"
        phx-change="validate"
        phx-trigger-action={@trigger_submit}
        action={~p"/users/log_in?_action=registered"}
        method="post"
      >
        <.input field={@form[:email]} type="email" label="Email" required />
        <.input field={@form[:password]} type="password" label="Password" required />
        <:actions>
          <.link id="signin" navigate={~p"/users/log_in"} class="font-semibold text-sm underline">
            Sign in here
          </.link>
        </:actions>

        <:actions>
          <.button
            phx-disable-with={gettext("Creating account...")}
            class="w-full h-12 disabled:bg-gray-400 disabled:border-gray-400"
          >
            Sign Up
          </.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

変更後は以下のようになります

スクリーンショット 2024-12-19 10.10.32.png

ログイン画面

ログイン画面も変更していきます、テストのときにクリックする対象を楽に絞り込むためにリンクにID属性を付けています

lib/trarecord_web/live/user_login_live.ex
  def render(assigns) do
    ~H"""
    <div id="login" class="m-auto pt-12 h-screen w-[80vw]">
      <.header class="text-center">
        Sign In
      </.header>

      <.simple_form for={@form} id="login_form" action={~p"/users/log_in"} phx-update="ignore">
        <.input field={@form[:email]} type="email" label="Email" required />
        <.input field={@form[:password]} type="password" label="Password" required />

        <:actions>
          <ul>
            <li class="mb-2">
              <.link
                id="forgort-password"
                href={~p"/users/reset_password"}
                class="text-sm font-semibold underline"
              >
                Forgot your password?
              </.link>
            </li>
            <li>
              <.link
                id="signup"
                navigate={~p"/users/register"}
                class="font-semibold text-sm underline"
              >
                Sign up here
              </.link>
            </li>
          </ul>
        </:actions>
        <:actions>
          <.button phx-disable-with="Signing in..." class="w-full">
            Sign In
          </.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

変更後は以下のようになります

スクリーンショット 2024-12-19 10.26.45.png

ナビゲーションコンポーネントの作成

ナビゲーションコンポーネントとは、ボトムタブやサイドメニューなどアプリの各リンクを表示するためのコンポーネントです

今回は以下の2つを作成します

ヘッダーナビゲーション
センターにページタイトル、両サイドに戻るなどのアクションボタンを配置
スクリーンショット 2024-12-19 14.13.38.png

ボトムタブナビゲーション
メインとなる機能へのリンク
スクリーンショット 2024-12-19 14.13.44.png

Phoenix LiveViewにおけるコンポーネントについて

LiveViewでは表示だけを行うステートレスのPhoenix.Componentと状態やイベントを定義するPhoenix.LiveComponentの2つのコンポーネントがあります

最初にPhoenix.Componentについて、phx.gen.liveによって生成された画面で使用されている、CoreComponentを題材に解説します

CoreComponentPhoenixにビルドインされたTailwindLiveView Phoniex Componentで実装されたデフォルトコンポーネント群は以下のようなコンポーネントがあります。

  • modal:CRUD作成時にも使用されるモーダル
  • flash:保存の成功、エラー発生の際右上に表示されるFlash
  • flash_group:上記をまとめたもの
  • simple_forminput部分と保存、キャンセル等のボタンのレイアウトを調整したフォーム
  • button:各丸ボタン
  • input:各種各丸input、正規化やエラーメッセージ表示も対応
  • label:デザイン調整されたラベル
  • errorinput等で使うエラーメッセージ
  • header:タイトル、サブタイトル、アクション含むデザインヘッダー
  • tableLiveStream対応したデザイン調整されたテーブル
  • list:構造体やMapの情報を一覧する
  • back:戻るボタン
  • iconHeroiconを表示するコンポーネント

attr と slot

CoreComponentの元になっているPhoenix.Componentattrslotという値を定義できます。

attrはパラメーターとして渡せる値を定義でき、slotcomponentのタグで挟んだ内容をどの場所で展開するかを定義できます。

実際にbackコンポーネントの実装を見てみましょう。

lib/trarecord_web/components/core_components.ex:L558
@doc """
  Renders a back navigation link.

  ## Examples

      <.back navigate={~p"/posts"}>Back to posts</.back>
  """
  attr :navigate, :any, required: true
  slot :inner_block, required: true

  def back(assigns) do
    ~H"""
    <div class="mt-16">
      <.link
        navigate#{@navigate}
        class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
      >
        <.icon name="hero-arrow-left-solid" class="h-3 w-3" />
        <%= render_slot(@inner_block) %>
      </.link>
    </div>
    """
  end

Examplesに使い方が載っています。attrnavigate/postsを指定し、slotBack to Postsが指定されています。

attrnavigatelinkタグのnavigateにパスとして展開されています。またslotの中身を展開するときはrender_slotを使用します。

<.back>...<./back>で挟んだ真ん中の部分がrender_slotで展開されます

liveComponent

LiveComponentは今回使いませんが軽く解説します

update,handle_eventなど各種コールバックが定義できるステートフルなコンポーネントを作成できます

使用する際は<.live_component />内でモジュールを指定し、ID属性が必須になります。

phx.gen.liveで生成したモーダルで入力フォームを表示する際に使われています

lib/trarecord_web/live/folder_live/index.html.heex:L32
<.modal
 :if={@live_action in [:new, :edit]}
 id="folder-modal"
 show
 on_cancel={JS.patch(~p"/folders")}
>
 <.live_component
   module={TrarecordWeb.FolderLive.FormComponent}
   id={@folder.id || :new}
   title={@page_title}
   action={@live_action}
   folder={@folder}
   user_id={@current_user.id}
   patch={~p"/folders"}
 />
</.modal>

LiveComponentを作る場合は以下のように作ります

  • renderで実際に表示する内容を定義
  • updateで渡された値をもとに行う処理を定義
  • handle_eventでイベントが発火した際のハンドリングを定義
  • イベント内で実行されてDB操作とナビゲーションを行う処理を定義

初期化されたときに1回実行されるmountもあります、詳しくは以下を見てみてください

lib/trarecord_web/live/folder_live/form_component.ex
defmodule TrarecordWeb.FolderLive.FormComponent do
 use TrarecordWeb, :live_component

 alias Trarecord.Folders

 @impl true
 def render(assigns) do
 ...
 end

 @impl true
 def update(%{folder: folder} = assigns, socket) do
 ...
 end

 @impl true
 def handle_event("validate", %{"folder" => folder_params}, socket) do
 ...
 end

 def handle_event("save", %{"folder" => folder_params}, socket) do
 ...
 end

 defp save_folder(socket, :edit, folder_params) do
 ...
 end

 defp save_folder(socket, :new, folder_params) do
 ...
 end

 defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

ヘッダーナビゲーションの作成

では実際にヘッダーナビゲーションを作成していきます

UIコンポーネントはこちらを使います

navigation.exをcomponents配下に作り、そこに各コンポーネントを実装していきます

値としてタイトル、左側のスロットは戻るボタン、右側のスロットはアクションボタンを付けれるようになります。

lib/trarecord_web/components/navigation.ex
defmodule TrarecordWeb.Navigation do
 use Phoenix.Component

 attr(:title, :string, default: "")

 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 header_nav(assigns) do
   ~H"""
   <div class="fixed navbar bg-orange-300 text-white w-full z-10 top-0 left-0">
     <div class="navbar-start">
       <span :if={@back != []} class="normal-case text-xl">
         <%= render_slot(@back) %>
       </span>
     </div>
     <div class="navbar-center">
       <span class="normal-case text-4xl">
         <%= @title %>
       </span>
     </div>
     <div class="navbar-end">
       <span :if={@actions != []} class="normal-case text-xl">
         <%= render_slot(@actions) %>
       </span>
     </div>
   </div>
   """
 end
end

プロジェクト全体で読み込む

プロジェクト全体を通してコンポーネントモジュールを読み込みたい場合はxx_web.exのhtml_helpersマクロにimport追加すると全体で読み込んでもらえます

lib/trarecord_web.ex:L82
  defp html_helpers do
    quote do
      # HTML escaping functionality
      import Phoenix.HTML
      # Core UI components and translation
      import TrarecordWeb.CoreComponents
+     import TrarecordWeb.Navigation
      use Gettext, backend: TrarecordWeb.Gettext

      # Shortcut for generating JS commands
      alias Phoenix.LiveView.JS

      # Routes generation with the ~p sigil
      unquote(verified_routes())
    end
  end

フォルダ一覧に組み込む

実際に組み込んでいきます

元のヘッダーと差し替えて、新規作成ボタンは文字ではなく[+]マークにします
またヘッダーはスクロールしてもずれないようにfixedになっているので上方向のマージンをコンテンツに対して行います

lib/trarecord_web/live/folder_live/index.html.heex
- <.header>
-  Listing Folders
-  <:actions>
-    <.link id="add" patch={~p"/folders/new"}>
-      <.button>New Folder</.button>
-    </.link>
-  </:actions>
- </.header>
+ <.header_nav title="Folder">
+  <:actions>
+    <.link patch={~p"/folders/new"}>
+      <.icon name="hero-plus-solid" class="mr-4 h-8 w-8" />
+    </.link>
+  </:actions>
+ </.header_nav>

+ <div class="mt-12">
<.table
  id="folders"
  rows={@streams.folders}
  row_click={fn {_id, folder} -> JS.navigate(~p"/folders/#{folder}") end}
>
  ...
</.table>
+ </div>

こんな感じになります

8de0d960cb93e55b6354635aab3e8dbb.gif

ボトムタブナビゲーション

ボトムタブナビゲーションを作成していきます

UIコンポーネントはこちらを使います

タブのリストをlinks関数に書き出し{タブ名,アイコン,URL}のタプルのリストをforでレンダリングしていきます

また素の Phoenix.Componentだとhtml_helpersが展開されずCoreComponentが使えないので読み込みます

lib/trarecord_web/components/navigation.ex
defmodule TrarecordWeb.Navigation do
  use Phoenix.Component

  import TrarecordWeb.CoreComponents

  def header_nav(assings)
  ...
  end
  
  attr(:current, :string, default: "", doc: "curruent open tab name")

  def bottom_tab(assigns) do
    ~H"""
    <div class="btm-nav">
      <%= for {title, icon ,path} <- links() do %>
        <a href={path} class={if @current == title, do: "active", else: ""}>
          <button>
            <.icon name={icon} class="w-5 h-5" />
            <p class="btm-nav-label"><%= title %></p>
          </button>
        </a>
      <% end %>
    </div>
    """
  end

  defp links() do
    [
      {"Folder", "hero-book-open-solid", "/folders"},
      {"Setting", "hero-cog-6-tooth-solid", "/users/settings"}
    ]
  end

フォルダ一覧とユーザー設定画面に組み込む

レイアウト的にテーブルの下に配置します

lib/trarecord_web/live/folder_live/index.html.heexL9
<div class="mt-12">
  <.table
    id="folders"
    rows={@streams.folders}
    row_click={fn {_id, folder} -> JS.navigate(~p"/folders/#{folder}") end}
  >
    ...
    </:action>
  </.table>
</div>

+ <.bottom_tab current="Folder" />

設定画面にも追加します

  def render(assigns) do
    ~H"""
-   <.header class="text-center">
-     Account Settings
-     <:subtitle>Manage your account email address and password settings</:subtitle>
-   </.header>
+   <.header_nav title="Setting" />


-    <div class="space-y-12 divide-y">
+    <div class="space-y-12 divide-y mt-12 p-4">
      <div>
        <.simple_form
          for={@email_form}
          id="email_form"
          phx-submit="update_email"
          phx-change="validate_email"
        >
        ...
        </.simple_form>
      </div>
    </div>

+   <.bottom_tab current="Setting" />
    """
  end

こんな感じになります

137d75ec327cfa0f0a9524859062bd23.gif

これでナビゲーションコンポーネントが完成しました

テストの修正

ログイン・新規登録ページの文言を変更していくつかテストがコケるようになったので修正していきます

folder_live_test

新規作成のボタンが変わったので直します

test/trarecord_web/live/folder_live_test.exs:L27
    test "saves new folder", %{conn: conn} do
      {:ok, index_live, _html} = live(conn, ~p"/folders")
-     assert index_live |> element("a", "New Folder") |> render_click() =~
+     assert index_live |> element("#add") |> render_click() =~
               "New Folder"

      assert_patch(index_live, ~p"/folders/new")

      assert index_live
             |> form("#folder-form", folder: @invalid_attrs)
             |> render_change() =~ "can&#39;t be blank"

      assert index_live
             |> form("#folder-form", folder: @create_attrs)
             |> render_submit()

      assert_patch(index_live, ~p"/folders")

      html = render(index_live)
      assert html =~ "Folder created successfully"
      assert html =~ "some name"
    end

user_login_live_test

test/trarecord_web/live/user_login_live_test.exs:L7
   test "renders log in page", %{conn: conn} do
      {:ok, _lv, html} = live(conn, ~p"/users/log_in")

-     assert html =~ "Log in"
+     assert html =~ "Sign In"
      assert html =~ "Sign up"
      assert html =~ "Forgot your password?"
    end
test/trarecord_web/live/user_login_live_test.exs:L60
   test "redirects to registration page when the Register button is clicked", %{conn: conn} do
      {:ok, lv, _html} = live(conn, ~p"/users/log_in")

      {:ok, _login_live, login_html} =
        lv
-       |> element(~s|main a:fl-contains("Sign up")|)
+       |> element(~s|main a:fl-contains("Sign up here")|)
        |> render_click()
        |> follow_redirect(conn, ~p"/users/register")

-     assert login_html =~ "Register"
+     assert login_html =~ "Sign Up"
    end

user_registration_live_test

test/trarecord_web/live/user_registration_live_test.exs:L7
   test "renders registration page", %{conn: conn} do
      {:ok, _lv, html} = live(conn, ~p"/users/register")

-     assert html =~ "Register"
-     assert html =~ "Log in"
+     assert html =~ "Sign Up"
+     assert html =~ "Sign in"
    end
test/trarecord_web/live/user_registration_live_test.exs:L24
    test "renders errors for invalid data", %{conn: conn} do
      {:ok, lv, _html} = live(conn, ~p"/users/register")

      result =
        lv
        |> element("#registration_form")
        |> render_change(user: %{"email" => "with spaces", "password" => "too short"})

-     assert result =~ "Register"
+     assert result =~ "Sign Up"
      assert result =~ "must have the @ sign and no spaces"
      assert result =~ "should be at least 12 character"
    end
test/trarecord_web/live/user_registration_live_test.exs:L71
  describe "registration navigation" do
    test "redirects to login page when the Log in button is clicked", %{conn: conn} do
      {:ok, lv, _html} = live(conn, ~p"/users/register")

      {:ok, _login_live, login_html} =
        lv
-       |> element(~s|main a:fl-contains("Log in")|)
+       |> element(~s|main a:fl-contains("Sign in")|)
        |> render_click()
        |> follow_redirect(conn, ~p"/users/log_in")

-     assert login_html =~ "Log in"
+     assert login_html =~ "Sign In"
    end
  end

CIぱすできたのでマージして完了です

スクリーンショット 2024-12-19 16.53.44.png

最後に

本記事ではユーザーログインと、新規登録画面のデザイン修正とナビゲーションコンポーネントの作成を行いました

次はフォルダ一覧をカードレイアウト化していきます

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

参考サイト

https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html
https://daisyui.com/components/navbar/#navbar-with-dropdown-center-logo-and-icon
https://daisyui.com/components/bottom-navigation/#with-title
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html#callbacks

7
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
7
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?