6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirAdvent Calendar 2024

Day 8

ElixirDesktopで作るスマホアプリ Part 4 UIコンポーネント導入、Welcomeページとオンボーディングの実装

Posted at

はじめに

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

アプリを初回起動時に表示されるWelcomeページとオンボーディングを実装していきます

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

git checkout -b feature/onboarding

UIライブラリの追加

Welcomeページを作る前にUIを実装するにあたって素のTailwindだと辛いのでライブラリを入れていこうと思います

UIコンポーネントライブラリの選定

PhoenixではデフォルトでCSSフレームワークでTailwindCSSが採用されています
ですが、PhoenixのCoreComponentとTailwindでUIを実装するのは大変なので、
UIコンポーネントライブラリを使用して、そのコストを抑えることができます

UIコンポーネントの選定は気をつけることがあって、
ReactやVue,SveltなどJSのフレームワークの上で動かすものが多くヒットするのでTailwind単体で動いて、JavaScriptを使用していないもの、またはLiveViewに対応している物を選ぶ必要があります

JavaScriptを使用していないものを選ぶのは、LiveViewでUI制御を行うときに干渉してしまうため調整が面倒だからです

Tailwind単体で動いて、JSを使用していないもの

こちらは2種類あります

  1. tailwindのユーティリティクラスだけで作成した実装サンプル集
  2. applyでcssのクラス的にまとめたコンポーネント集

大半は1です。実装の時にこのUIどう実装するのかがわかるので、参考にするのにも大変良いです

ですがこれだと、記述がどうしても多くなってしまいますので
いい感じにまとめてくれている2を心情的には使いたいのですが、該当するものはほとんどなく
いい感じに揃っているのはこちらです

今回はこれを使用します

LiveViewに対応している物を選ぶ必要があります

こちらは今年いくつか出てきたので気になった方は試してみてください

daisyuiをインストールします
JSのライブラリをインストールするときは assetsに移動する必要があるので注意が必要です

cd assets
npm i daisyui
cd ..

インストールしたら、pluginsに requireを追加します

assets/tailwind.config.js
module.exports = {
  content: ["./js/**/*.js", "../lib/*_web.ex", "../lib/*_web/**/*.*ex"],
  theme: {...},
  plugins: [
    require("@tailwindcss/forms"),
+   require("daisyui"), 
    ...
  ],
  

UIコンポーネントの導入が終わったのでコミットします

git add .
git commit -m 'add daisyui'

Welcomeページ

次にWelcomeページを作成します

こちらは未ログイン時にアプリを起動した際に表示される画面で
ログインと新規登録の導線を表示させます

Welcomeページに表示するいい感じの写真をpriv/static/imagesに入れます

welcome.png

phx.newで作られた初期ページhome.html.heexを以下のように変更します

画像のパスは~p""シジルで/images/ファイル名とすることで展開してくれます

このコンポーネントを使用して画面を構築します

lib/trarecord_web/controllers/page_html/home.html.heex
<div class="hero h-screen " style={"background-image: url(#{~p"/images/welcome.png"});"}>
  <div class="hero-overlay bg-opacity-0"></div>
  <div class="hero-content text-center text-neutral-content">
    <div class="w-full">
      <div class="-mt-64">
        <h1 class="text-5xl font-bold mb-24">Welcome to Trarecord</h1>
      </div>
      <div class="fixed bottom-12 right-8 flex flex-col gap-y-8">
        <.link navigate={~p"/users/log_in"} class="btn font-bold normal-case w-32">
          Sign In
        </.link>
        <.link navigate={~p"/users/register"} class="btn font-bold normal-case w-32">
          Sign Up
        </.link>
      </div>
    </div>
  </div>
</div>

起動時に未ログインの場合はログインページにリダイレクトしていましたが、Welcomeページをそのまま表示するように変更します

lib/trarecord_web/controllers/page_controller.ex:L19
defmodule TrarecordWeb.PageController do
  use TrarecordWeb, :controller
  alias Trarecord.Accounts.User

- def home(conn, _params) do
-   # The home page is often custom made,
-   # so skip the default app layout.
-   render(conn, :home, layout: false)
- end

  def index(conn, _params) do
    redirect_to(conn, conn.assigns.current_user)
  end

  defp redirect_to(conn, %User{}) do
    redirect(conn, to: ~p"/users/settings")
  end

  defp redirect_to(conn, nil) do
-   redirect(conn, to: ~p"/users/log_in")  
+   render(conn, :home, layout: false)
  end
end

これで起動時にWelcome画面が表示されそこから、ログイン、新規登録の導線ができました

Welcomeページにlogin,registerリンクが出て邪魔なので削除します

lib/trarecord_web/components/layouts/root.html.heex
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="csrf-token" content={get_csrf_token()} />
    <.live_title suffix=" · Phoenix Framework">
      <%= assigns[:page_title] || "Trarecord" %>
    </.live_title>
    <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
    <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
    </script>
  </head>
  <body class="bg-white">
-   <ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
-     <%= if @current_user do %>
-       <li class="text-[0.8125rem] leading-6 text-zinc-900">
-         <%= @current_user.email %>
-       </li>
-       <li>
-         <.link
-           href={~p"/users/settings"}
-           class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
-         >
-           Settings
-         </.link>
-       </li>
-       <li>
-         <.link
-           href={~p"/users/log_out"}
-           method="delete"
-           class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
-         >
-           Log out
-         </.link>
-       </li>
-     <% else %>
-       <li>
-         <.link
-           href={~p"/users/register"}
-           class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
-         >
-           Register
-         </.link>
-        </li>
-       <li>
-         <.link
-           href={~p"/users/log_in"}
-           class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
-         >
-           Log in
-         </.link>
-       </li>
-     <% end %>
-   </ul>
    <%= @inner_content %>
  </body>
</html>

これでWelcomeページが出来たのでコミットします

git add .
git commit -m 'impl welcome page'

オンボーディング

オンボーディングとはアプリやWebサービスを登録した後にどのようなアプリで、どう使うのかをユーザーに伝える機能です

スライドショーだったり、実際に使用するボタンのみ有効にしてユーザーの操作をガイドするなどいくつか種類があります

今回はスライドショーで実装していきます

画像の準備

スライドショーで使用する画像ですが、以下からダウンロードします

この方の画像は無料ですが、クレジット表記をする必要があるので、ダウンロード時に表記するクレジットのhtmlをコピーして控えておきましょう

スクリーンショット 2024-12-06 1.09.06.png

オンボーディングページの作成

素材が揃ったのでオンボーディングページを作成します

スライドショーなのでページ推移はせずに nextボタンを押して次の画像とテキストを表示し、最後の画像ならfinishを押してオンボーディングを完了させます

表示する画像とテキスト、リンクのリストを返すintro/0関数を実装します

データ構造は次の様にしています

  • page -> ページ番号
  • title -> タイトル
  • text -> 本文
  • link -> クレジットのリンク
  • image -> 画像パス
  • bg -> 背景色

画像の切り替えイベントはページ番号が3だったらfinishで完了となりそれ以外は次の画像を表示するようにしています

lib/trarecord_web/live/onboarding_live/index.ex
defmodule TrarecordWeb.OnboardingLive.Index do
  use TrarecordWeb, :live_view

  def render(assigns) do
    ~H"""
    <div
      class="flex justify-center w-screen h-screen bg-local bg-contain bg-center bg-no-repeat"
      style={"background-image: url(#{@page_data.image}); background-color: #{@page_data.bg}"}
    >
      <div class="grid grid-rows-6 mt-12 text-center">
        <h1 class="text-2xl"><%= raw(@page_data.title) %></h1>
        <p class="text-base -mt-12 row-span-4"><%= raw(@page_data.text) %></p>
        <%= if @page == 3 do %>
          <p class="text-xl cursor-default" phx-click="finish">finish</p>
        <% else %>
          <p class="text-xl cursor-default" phx-click="next">next</p>
        <% end %>
      </div>
    </div>
    <p id="freepik" class="absolute bottom-2 right-4 text-sm">
      <a href={@page_data.link} class="underline text-blue-600">
        Image by storyset on Freepik
      </a>
    </p>
    """
  end

  def mount(_params, _session, socket) do
    socket
    |> assign(:page, 1)
    |> assign(:page_data, Enum.find(intro(), &(&1.page == 1)))
    |> then(&{:ok, &1})
  end

  def handle_event("next", _params, socket) do
    page = socket.assigns.page + 1

    socket
    |> assign(:page, page)
    |> assign(:page_data, Enum.find(intro(), &(&1.page == page)))
    |> then(&{:noreply, &1})
  end

  def handle_event("finish", _params, socket) do
    {:noreply, push_navigate(socket, to: ~p"/")}
  end

  def intro() do
    [
      %{
        page: 1,
        title: "Let's gather where we want to go.",
        text: "You can register <br />various spots using Google search.",
        link:
          "https://www.freepik.com/free-vector/flying-around-world-with-airplane-concept-illustration_8426450.htm#fromView=author&page=1&position=12&uuid=dbcac267-0605-4076-ae43-111dfb07a118",
        image: "/images/flying-around-world.svg",
        bg: "#59b2ab"
      },
      %{
        page: 2,
        title: "Let's organize them in folders.",
        text: "If you have more spots, divide them into <br />folders for easier management.",
        link:
          "https://www.freepik.com/free-vector/resume-folder-concept-illustration_5358953.htm#fromView=author&page=1&position=6&uuid=31a01ff6-d2cb-4798-bc51-89a0517d7ad9",
        image: "/images/folder.svg",
        bg: "#febe29"
      },
      %{
        page: 3,
        title: "Record your travel.",
        text:
          "You can record your memories of actually visiting the spot<br />with GPS logging and Checkin.",
        link:
          "https://www.freepik.com/free-vector/post-concept-illustration_5928515.htm#fromView=author&page=1&position=2&uuid=1450568b-5aac-4d60-a3ad-bef059625d94",
        image: "/images/post.svg",
        bg: "#22bcb5"
      }
    ]
  end
end

ルーティングとリダイレクト処理

liveviewのページは作ったのでルーティングを追加します
登録後に表示するので、ログイン後のスコープに追加します

lib/trarecord_web/router.ex:L64
  scope "/", TrarecordWeb do
    pipe_through [:browser, :require_authenticated_user]

    live_session :require_authenticated_user,
      on_mount: [{TrarecordWeb.UserAuth, :ensure_authenticated}] do
      live "/users/settings", UserSettingsLive, :edit
      live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
+     live "/onboarding", OnboardingLive.Index, :index
    end
  end

user_registration_liveのフォームをsubmitするとここを通るので、put_session/3関数でオンボーディングにリダイレクトするように変更します

lib/trarecord_web/controllers/user_session_controller.ex:L7
  def create(conn, %{"_action" => "registered"} = params) do
-   create(conn, params, "Account created successfully!")
+   conn
+   |> put_session(:user_return_to, ~p"/onboarding")
+   |> create(params, "Account created successfully!")
  end

オンボーディング時にヘッダーが邪魔なので以下のように変更します

lib/trarecord_web/components/layouts/app.html.heex
<main>
  <div class="flex flex-col">
    <.flash_group flash={@flash} />
    <%= @inner_content %>
  </div>
</main>

オンボーディングの実装が完了したのでコミットします

git add .
git commit -m 'impl onboarding'

動作確認

実装が完了したので動作確認を行います

bb04af429b108e899e92bc2be0324111.gif

新規登録後オンボーディングが始まり、完了後はログイン後画面にリダイレクトされるのを確認できました

テストの追加・修正

オンボーディング周りで既存の改修を行ったので、こけたテストの修正とオンボーディングのテストを追加していきます

page_controller

未ログインで起動時はWelcomeページを開くようにしたので変更

test/trarecord_web/controllers/page_controller_test.exs
defmodule TrarecordWeb.PageControllerTest do
  use TrarecordWeb.ConnCase

  test "GET /", %{conn: conn} do
    conn = get(conn, ~p"/")
-   assert html_response(conn, 302) =~ "/users/log_in"
+   assert html_response(conn, 200) =~ "Welcome"
  end
end

user_session_controller_test

登録後は onboardingにリダイレクトするようにしたので変更

test/trarecord_web/controllers/user_session_controller_test.exs:L53
    test "login following registration", %{conn: conn, user: user} do
      conn =
        conn
        |> post(~p"/users/log_in", %{
          "_action" => "registered",
          "user" => %{
            "email" => user.email,
            "password" => valid_user_password()
          }
        })

-     assert redirected_to(conn) == ~p"/"
+     assert redirected_to(conn) == ~p"/onboarding"     
      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Account created successfully"
    end

user_login_live_test

ヘッダーを消したので新規登録のリンクは Sing upだけになったので変更

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 =~ "Register"
+     assert html =~ "Sign up"
      assert html =~ "Forgot your password?"
    end

user_registration_live_test

新規登録後はonboardingにリダイレクトするようにしたので変更

test/trarecord_web/live/user_registration_live_test.exs
    test "creates account and logs the user in", %{conn: conn} do
      {:ok, lv, _html} = live(conn, ~p"/users/register")

      email = unique_user_email()
      form = form(lv, "#registration_form", user: params_for(:user_form_data, email: email))
      render_submit(form)
      conn = follow_trigger_action(form, conn)

-     assert redirected_to(conn) == ~p"/"
+     assert redirected_to(conn) == ~p"/onboarding"

      # Now do a logged in request and assert on the menu
      conn = get(conn, "/")
      response = html_response(conn, 302)
      assert response =~ "/users/settings"
    end

onboarding_live_test

ページを開いた時のテストと、オンボーディング完了と完了後のリダイレクトのテストを作成

test/trarecord_web/live/onboarding_live_test.exs
defmodule TrarecordWeb.OnboardingLiveTest do
  use TrarecordWeb.ConnCase, async: true

  import Phoenix.LiveViewTest

  describe "start onbarding" do
    setup %{conn: conn} do
      %{conn: log_in_user(conn, insert(:user))}
    end

    test "render onboarding page", %{conn: conn} do
      {:ok, _lv, html} = live(conn, ~p"/onboarding")

      assert html =~ "Let&#39;s gather where we want to go."
      assert html =~ "next"
    end

    test "finish onboarding and redirect /", %{conn: conn} do
      {:ok, lv, _html} = live(conn, ~p"/onboarding")

      assert lv
             |> element("p", "next")
             |> render_click() =~ "Let&#39;s organize them in folders."

      assert lv
             |> element("p", "next")
             |> render_click() =~ "Record your travel."

      assert lv
             |> element("p", "finish")
             |> render_click()
             |> follow_redirect(conn, "/")
    end
  end
end
git add .
git commit -m 'fix test'
git push origin feature/onbarding

CI check and Merge

CIも無事通ったのでマージして完了です

スクリーンショット 2024-12-06 3.22.02.png

最後に

UIコンポーネントライブラリの追加、Welcompeページとオンボーディングの実装が完了しました
これでアプリらしさが結構出てきたかと思います

次は指定したリンクをブラウザで開く、ネイティブ機能との連携を実装していきます

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

参考ページ

https://sailboatui.com/
https://www.hyperui.dev/
https://tailwindui.com/
https://tailblocks.cc/
https://flowrift.com
https://merakiui.com/
https://daisyui.com/
https://petal.build
https://mishka.tools/chelekom
https://github.com/bluzky/salad_ui
https://github.com/briankariuki/tremorx
https://www.freepik.com/author/stories
https://myajo.net/tips/9500/

6
1
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
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?