ElixirDesktop 保存したデータの一覧とフローティングアクションボタンの実装

# はじめに
この記事は Elixirアドベントカレンダーのシリーズ４の12日目の記事です

11日目でGoogleBookAPIのデータ使用して保存したデータの表示とフローティングアクションボタンの実装を行います


# ネストルーティング

保存できるようになったので、見れるようにしていきましょう
本棚に紐づいた本を一覧したいので、ネストさせたURLを作成します

```diff_elixir:lib/bookshelf_web/router.ex
  scope "/", BookshelfWeb do
    pipe_through :browser

    live "/shelves", ShelfLive.Index, :index
    live "/shelves/new", ShelfLive.Index, :new
    live "/shelves/:id/edit", ShelfLive.Index, :edit
    live "/shelves/:id/delete", ShelfLive.Index, :delete

-    live "/shelves/:id", ShelfLive.Show, :show
-    live "/shelves/:id/show/edit", ShelfLive.Show, :edit
+    live "/shelves/:shelf_id/books", BookLive.Index, :index
+    live "/shelves/:shelf_id/books/:id", BookLive.Show, :show
+    live "/shelves/:shelf_id/books/:id/edit", BookLive.Show, :edit
     live "/search", SearchLive.Index, :index
  end
```

本棚一覧のリンクを変更します

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

<.table
  id="shelves"
  rows={@streams.shelves}
- row_click={fn {_id, shelf} -> JS.navigate(~p"/shelves/#{shelf}") end}
+ row_click={fn {_id, shelf} -> JS.navigate(~p"/shelves/#{shelf}/books") end}
>
  <:col :let={{_id, shelf}} label="Name"><%= shelf.name %></:col>
  <:action :let={{_id, shelf}}>
    <div class="sr-only">
-     <.link navigate={~p"/shelves/#{shelf}"}>Show</.link>
+     <.link navigate={~p"/shelves/#{shelf}/books"}>Show</.link>
    </div>
    <.link patch={~p"/shelves/#{shelf}/edit"}>Edit</.link>
  </:action>
  <:action :let={{_id, shelf}}>
    <.link
      patch={~p"/shelves/#{shelf}/delete"}
    >
      Delete
    </.link>
  </:action>
</.table>

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

# 書籍一覧

index.html.heexの中身をを消して、検索のを使いまわします

```elixir:lib/bookshelf_web/live/book_live/index.html.heex
<.gheader title={@shelf.name}>
  <:back>
    <.link navigate={~p"/shelves"}>
      <.icon name="hero-chevron-left-solid" class="h-6 w-6" />
    </.link>
  </:back>
</.gheader>

<div
  id="books"
  class="flex flex-wrap gap-4 mt-4"
  phx-update={match?(%Phoenix.LiveView.LiveStream{}, @streams.books) && "stream"}
>
  <%= for {id, book} <- @streams.books do %>
    <.link
      id={id}
      class="card bg-base-100 shadow-xl w-44 select-none"
      navigate={~p"/shelves/#{book.shelf_id}/books/#{book}"}
    >
      <figure><img class="mt-1" src={book.thumb} /></figure>
      <div class="p-2">
        <p class="text-xs"><%= book.title %></p>
        <p class="text-xs mt-2"><%= book.author %></p>
      </div>
    </.link>
  <% end %>
</div>
<.bottom_tab title="Shelf" />
```

マウント時にパラメータに本棚IDを必須にし、本棚を取得しassignします
この画面からCreateUpdateDeleteはしないので削除します

```diff_elixir:lib/bookshelf_web/live/book_live/index.ex
defmodule BookshelfWeb.BookLive.Index do
  use BookshelfWeb, :live_view

- alias Bookshelf.Books
- alias Bookshelf.Books.Book
+ alias Bookshelf.Shelf

  @impl true
- def mount(_params, _session, socket) do
-   {:ok, stream(socket, :books, Books.list_books())}
- end
+ def mount(%{"shelf_id" => shelf_id}, _session, socket) do
+   shelf = Shelves.get_shelf!(shelf_id)
+
+   socket
+   |> assign(:shelf, shelf)
+   |> stream(:books, shelf.books)
+   |> then(&{:ok, &1})
+ end
  

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

-  defp apply_action(socket, :edit, %{"id" => id}) do
-    socket
-    |> assign(:page_title, "Edit Book")
-    |> assign(:book, Books.get_book!(id))
-  end

-  defp apply_action(socket, :new, _params) do
-    socket
-    |> assign(:page_title, "New Book")
-    |> assign(:book, %Book{})
-  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Listing Books")
    |> assign(:book, nil)
  end

-  @impl true
-  def handle_info({BookshelfWeb.BookLive.FormComponent, {:saved, book}}, socket) do
-    {:noreply, stream_insert(socket, :books, book)}
-  end

- @impl true
- def handle_event("delete", %{"id" => id}, socket) do
-   book = Books.get_book!(id)
-   {:ok, _} = Books.delete_book(book)
-
-   {:noreply, stream_delete(socket, :books, book)}
- end
end
```

![スクリーンショット 2023-09-13 2.10.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/30be5fb2-b232-39a2-507d-c1a02922742a.png)

# 書籍詳細画面

詳細画面では、リンクを差し替えて、サムネイルをimgタグで上の方に置きます

```diff_elixir:lib/ebookwormer_web/live/book_live/show.html.heex
- <.header>
-  Book <%= @book.id %>
-  <:subtitle>This is a book record from your database.</:subtitle>
-  <:actions>
-    <.link patch={~p"/books/#{@book}/show/edit"} phx-click={JS.push_focus()}>
-      <.button>Edit book</.button>
-    </.link>
-  </:actions>
- </.header>
+ <.gheader title={@book.title}>
+   <:back>
+     <.link navigate={~p"/shelves/#{@book.shelf_id}/books"}>
+       <.icon name="hero-chevron-left-solid" class="h-6 w-6" />
+     </.link>
+   </:back>
+   <:actions>
+     <.link patch={~p"/shelves/#{@book.shelf_id}/books/#{@book}/edit"} phx-click={JS.push_focus()}>
+       <.icon name="hero-pencil-square-solid" class="h-6 w-6 mr-4" />
+     </.link>
+   </:actions>
+ </.gheader>
+ <figure><img class="mt-1" src={@book.thumb} /></figure>

<.list>
  <:item title="Title"><%= @book.title %></:item>
  <:item title="Author"><%= @book.author %></:item>
- <:item title="Thumb"><%= @book.thumb %></:item>
  <:item title="Isbn"><%= @book.isbn %></:item>
  <:item title="Published date"><%= @book.published_date %></:item>
</.list>

- <.back navigate={~p"/books"}>Back to books</.back>
+ <.bottom_tab title="Shelf" />

- <.modal :if={@live_action == :edit} id="book-modal" show on_cancel={JS.patch(~p"/books/#{@book}")}>
+ <.modal :if={@live_action == :edit} id="book-modal" show on_cancel={JS.patch(~p"/shelves/#{@book.shelf_id}/books/#{@book}")}>

  <.live_component
    module={BookshelfWeb.BookLive.FormComponent}
    id={@book.id}
    title={@page_title}
    action={@live_action}
    book={@book}
-   patch={~p"/books/#{@book}"}
+   patch={~p"/shelves/#{@book.shelf_id}/books/#{@book}"} 
  />
</.modal>
```
詳細画面
![スクリーンショット 2023-09-13 2.10.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/54334899-308e-376a-8b07-faf0184670ef.png)
編集画面
![スクリーンショット 2023-12-13 0.05.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/6438c22c-f774-e0ce-9001-717e0cc706d8.png)

# フローティングアクションボタン(FAB)

次にFABを実装します
FABは重要な機能に素早くアクセスさせるために使用されることが多いです

開閉制御を `JS.hide`と`JS.show`で行いますので JSモジュールをaliasでJSで使えるようにします
`phx-click`でボタンクリックイベント、`phx-click-away`で領域外クリックイベントを発火します

```diff_elixir:lib/bookshelf_web/components/navigation.ex
defmodule BookshelfWeb.Navigation do
  use Phoenix.Component

  import BookshelfWeb.CoreComponents
+ alias Phoenix.LiveView.JS
  ...
  # 以下追加
  def fab(assigns) do
    ~H"""
    <div class="fixed z-90 bottom-20 right-8">
      <div class="flex flex-col">
        <div id="fab-menu" class="hidden">
          <.menu />
        </div>
        <label class="btn btn-circle self-end">
          <div phx-click-away={
            JS.show(to: "#open-fab") |> JS.hide(to: "#close-fab") |> JS.hide(to: "#fab-menu")
          }>
            <div
              id="close-fab"
              class="hidden"
              phx-click={
                JS.show(to: "#open-fab") |> JS.hide(to: "#close-fab") |> JS.hide(to: "#fab-menu")
              }
            >
              <.icon name="hero-x-mark-solid" class="current-fill w-8 h-8" />
            </div>
            <div
              id="open-fab"
              phx-click={
                JS.show(to: "#close-fab") |> JS.hide(to: "#open-fab") |> JS.show(to: "#fab-menu")
              }
            >
              <.icon name="hero-plus-solid" class="current-fill w-8 h-8" />
            </div>
          </div>
        </label>
      </div>
    </div>
    """
  end

  def menu(assigns) do
    ~H"""
    <ul class="menu bg-base-200 w-42 p-2 rounded-box mb-4">
      <li>
        <.link navigate="/search">
          <.icon name="hero-magnifying-glass-solid" class="h-8 w-8" /> Search Books
        </.link>
      </li>
      <li>
        <.link navigate="/shelves/new">
          <.icon name="hero-book-open-solid" class="h-8 w-8" /> New Shelf
        </.link>
      </li>
    </ul>
    """
  end
end
```

タブの下辺りに追加します

```heex:lib/ebookworm_web/live/shlef_live/index.html.heex
...
<.bottom_tab title="Shelf" />

<.fab/>
...
```

![スクリーンショット 2023-09-13 2.32.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/d72472bc-9a23-e6e8-a2b0-797b7d969560.png)
![スクリーンショット 2023-09-13 2.33.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/ce3749da-f7e9-c0b9-f044-2fce1350060c.png)

# 最後に
前回に続き保存した書籍を表示・編集する箇所に加え、フローティングアクションボタンを実装しました
本棚アプリの作成は本記事で終了になります
スタンドアローンで動作するアプリをWebアプリケーションスタックで簡単に実装できそうと思っていただければ幸いです

次からはスマホアプリとして一般的な、Phoenixサーバーからデータを取得するサーバークライアント方式での実装について解説します

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