ElixirDesktopでスマホ的なUIでアプリを作る〜ナビゲーション〜

# はじめに
本記事は以下の記事で作成したCRUDアプリにスマホなUIを組み込んでいく内容になります

https://qiita.com/the_haigo/items/65160612eff12ebfecc2

前提
- 本棚アプリ
- サーバーとの通信はしないスタンドアローン
- DBはローカルのSQlite3
- 本棚(shelf)のCRUD画面がある
- confirm modal を作成してある

# スマホ的なUIとは？

参考にならないくらいめっちゃオシャレなんですけど、トレンドなUIはこちらから色々見ることができます

https://dribbble.com/shots/popular/mobile

https://www.freepik.com/free-photos-vectors/app-mockup

こちらを例にして要素を抽出しますと

https://dribbble.com/shots/22490417-TCourse-Learning-App

- 現在のページ名、戻る、サブアクションを表示するヘッダー
- グローバルメニューとして使うボトムタブナビゲーション
- コンテンツをカードにして境界をわかりやすくしている

https://dribbble.com/shots/22450023-Travel-Mobile-App

- 最初起動したときにどんなアプリかを軽く説明する Welcome画面
- 詳細に段階的に説明したり、実際に１つやってみましょうとガイドするオンボーディングもある
- コアな機能に素早くアクセスできる Floating Action Button

といったものが多く見られます

これらを踏まえてアプリとして作成する場合に以下が必要になりそうです

- UI部分
    - コンポーネントライブラリ daisyui
    - ヘッダー
    - ボトムタブ
    - フローティングアクションボタン
    - Welcome画面
    - カードUI
- 設定画面
    - 個人設定
    - ライセンス
    - 利用規約
    - お問い合わせ
    - プライバシーポリシー
    - ログアウト/初期化
- API通信
    - Google Books
    - APIキーの保存
- ネイティブ側
    - アイコンを変える
    - スプラッシュ画面を変える
    - アプリ名を変える
    - ネイティブ機能を使う(ブラウザを開く)

# UIコンポーネントライブラリ

PhoenixではデフォルトでCSSフレームワークでTailwindCSSが採用されています
ですが、PhoenixのCoreComponentとTailwindでUIを実装するのは大変なので、
UIコンポーネントライブラリを使用して、ちゃちゃっと作りましょう

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

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

こちらは２種類あります
1. tailwindのユーティリティクラスだけで作成した実装サンプル集
2. applyでcssのクラス的にまとめたコンポーネント集

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

https://sailboatui.com/

https://www.hyperui.dev/

https://tailwindui.com/

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

https://daisyui.com/

今回はこれを使用します

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

一応LiveViewの文脈に沿ってくれるライブラリは提供しているのですが、
重厚そうなので今回は見送ります
ライトに使えて、ライトに改修できる方が楽なので

https://flowbite.com/docs/getting-started/phoenix/#install-flowbite

実装例は結構あるので参考にすると良いかもしれません

https://flowbite.com/blocks/

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

```
cd assets
npm i daisyui
```

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

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

これでDaisyUIが使用可能になりました

# ヘッダー
全体で共通して表示するヘッダーコンポーネントを作成します
最初に不要なデフォルトのヘッダーを削除します

```diff_elixir:lib/ebookworm_web/components/layouts/app.html.heex
- <header class="px-4 sm:px-6 lg:px-8">
-  <div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
-    <div class="flex items-center gap-4">
-      <a href="/">
-        <img src={~p"/images/logo.svg"} width="36" />
-      </a>
-      <p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
-        v<%= Application.spec(:phoenix, :vsn) %>
-      </p>
-    </div>
-    <div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
-      <a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
-        @elixirphoenix
-      </a>
-      <a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
-        GitHub
-      </a>
-      <a
-        href="https://hexdocs.pm/phoenix/overview.html"
-        class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
-      >
-        Get Started <span aria-hidden="true">&rarr;</span>
-      </a>
-    </div>
-  </div>
- </header>
<main class="px-4 py-20 sm:px-6 lg:px-8">
  <div class="mx-auto max-w-2xl">
    <.flash_group flash={@flash} />
    <%= @inner_content %>
  </div>
</main>
```

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

https://daisyui.com/components/navbar/#navbar-with-dropdown-center-logo-and-icon

コンポーネントファイルを作成してそこに記載します

今回作るコンポーネントは Phoenix.Componentを使用して、ステートレスなコンポーネントで与えられた引数の値を表示するだけのコンポーネントになります

状態をもたせる（ステートフル）なコンポーネントは Phoenix.LiveComponentを作成します

通常のComponentは `<.link>...<./link>`といったふうに使用できますが
LiveComponentは <.live_component /> 内でモジュールを指定し、ID属性が必須になります

では　`navigation.ex`というファイルを作成してコンポーネントを実装しましょう

```elixir:lib/ebookworm_web/components/navigation.ex
defmodule EbookwormWeb.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 gheader(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
```

attrとslotの２つの要素が関数の上に記載されています
- attrは呼び出し元で設定できるパラメータです、デフォルト値や必須フラグ、ドキュメントを含めることができます
- slotはslotに指定した名前で `:tag`で囲んだ要素を渡すことによってrender_slotの箇所にレンダリングされます

頻繁に使用するコンポーネントは一々importするのが面倒なので
xx_web.exで他のと一緒に読み込んでもらうようにします

```elixir:lib/ebookworm_web.ex:L82
defmodule EbookwormWeb
...
  defp html_helpers do
    quote do
      # HTML escaping functionality
      import Phoenix.HTML
      # Core UI components and translation
      import EbookwormWeb.CoreComponents
      import EbookwormWeb.Navigation # 追加
      import EbookwormWeb.Gettext

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

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

では実際にindex.exとshow.exで使用してみましょう

```diff_elixir:lib/ebookworm_web/live/shlef_live/index.html.heex
+ <.gheader title="Shleves">
+   <:actions>
+     <.link patch={~p"/shelves/new"}>
+         <.icon name="hero-plus-solid" class="h-6 w-6 mr-4" />
+     </.link>
+   </:actions>
+ </.gheader>

- <.header>
-  Listing Shelves
-  <:actions>
-    <.link patch={~p"/shelves/new"}>
-      <.button>New Shelf</.button>
-    </.link>
-  </:actions>
- </.header>
...
```

もとからある`.header`をけして先程作成した`.gheader`を追加します
右側のアクション領域に新規作成用のボタンを付けます
CoreComponentに`.icon`がありそこからheroiconsを使えるので +(plus)を表示します

https://heroicons.com/

命名規則は `hero-[icon名]-[solid or outline]`となってます


![スクリーンショット 2023-09-09 23.38.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/6a2c9fcc-eca2-58d6-8d5b-c19d0e276395.png)

表示はこんな感じになります

show.exにも追加しましょう

```diff_elixir:lib/ebookworm_web/live/shlef_live/show.html.heex
+ <.gheader title={@shelf.name}>
+  <:back>
+    <.link navigate={~p"/shelves"}>
+      <.icon name="hero-chevron-left-solid" class="h-6 w-6"/>
+    </.link>
+  </:back>

+  <:actions>
+    <.link patch={~p"/shelves/#{@shelf}/show/edit"} phx-click={JS.push_focus()}>
+      <.icon name="hero-pencil-square-solid" class="h-6 w-6 mr-4" />
+    </.link>
+  </:actions>
+ </.gheader>

- <.header>
-  Shelf <%= @shelf.id %>
-  <:subtitle>This is a shelf record from your database.</:subtitle>
-  <:actions>
-    <.link patch={~p"/shelves/#{@shelf}/show/edit"} phx-click={JS.push_focus()}>
-      <.button>Edit shelf</.button>
-    </.link>
-  </:actions>
- </.header>

<.list>
  <:item title="Name"><%= @shelf.name %></:item>
</.list>

- <.back navigate={~p"/shelves"}>Back to shelves</.back>
...
```

戻るボタンは `chevron-left`を設定します

![スクリーンショット 2023-09-09 23.51.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/2c527b16-cc99-be7d-b432-54701c8bda08.png)

こんな感じになります

# ボトムタブナビゲーション
本棚一覧の他に本検索画面と、設定画面を作りたいのでナビゲーションを作ります

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

https://daisyui.com/components/bottom-navigation/#with-title


```lib/ebookworm_web/components/navigation.ex
defmodule EbookwormWeb.Navigation do
  use Phoenix.Component

  import EbookwormWeb.CoreComponents # 追加
  ...
  attr(:title, :string, default: "")

  def bottom_tab(assigns) do
    ~H"""
    <div class="btm-nav">
      <%= for {title, icon ,path} <- links() do %>
        <a href={path} class={if @title == 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
    [
      {"Shelf", "hero-book-open-solid", "/shelves"},
      {"Search", "hero-magnifying-glass-solid", "/search"},
      {"Setting", "hero-cog-6-tooth-solid", "/settings"}
    ]
  end
end
```

linksで定義したリンクでタブを作成し、ページのタイトルと同じタブをアクティブ状態にします


早速使ってみましょう
コンテンツ部分とモーダルの間あたりにいれます、今回は`.table`の下になります


```elixir:lib/ebookworm_web/live/shlef_live/index.html.heex
...
<.table
  id="shelves"
  rows={@streams.shelves}
  row_click={fn {_id, shelf} -> JS.navigate(~p"/shelves/#{shelf}") end}
>
  ... 
</.table>

<.bottom_tab title="Shelf" />
```

![スクリーンショット 2023-09-10 17.26.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/0326decb-b7c6-04c2-a2e6-5724260a5adf.png)


```elixir:lib/ebookworm_web/live/shlef_live/show.html.heex
...
<.list>
  <:item title="Name"><%= @shelf.name %></:item>
</.list>

<.bottom_tab title="Shelf" />
...
```

![スクリーンショット 2023-09-10 17.32.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/b56a9e8e-7bc8-1156-7e32-75282ae4462f.png)


# 最後に
本記事ではスマホっぽいUIのヘッダーとボトムタブナビゲーションを実装しました
次はAPIを叩いてデータを持ってきて保存するところまで行います