はじめに
この記事は Elixirアドベントカレンダーのシリーズ4の12日目の記事です
11日目でGoogleBookAPIのデータ使用して保存したデータの表示とフローティングアクションボタンの実装を行います
ネストルーティング
保存できるようになったので、見れるようにしていきましょう
本棚に紐づいた本を一覧したいので、ネストさせたURLを作成します
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
本棚一覧のリンクを変更します
<.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の中身をを消して、検索のを使いまわします
<.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はしないので削除します
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
書籍詳細画面
詳細画面では、リンクを差し替えて、サムネイルをimgタグで上の方に置きます
- <.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>
フローティングアクションボタン(FAB)
次にFABを実装します
FABは重要な機能に素早くアクセスさせるために使用されることが多いです
開閉制御を JS.hide
とJS.show
で行いますので JSモジュールをaliasでJSで使えるようにします
phx-click
でボタンクリックイベント、phx-click-away
で領域外クリックイベントを発火します
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
タブの下辺りに追加します
...
<.bottom_tab title="Shelf" />
<.fab/>
...
最後に
前回に続き保存した書籍を表示・編集する箇所に加え、フローティングアクションボタンを実装しました
本棚アプリの作成は本記事で終了になります
スタンドアローンで動作するアプリをWebアプリケーションスタックで簡単に実装できそうと思っていただければ幸いです
次からはスマホアプリとして一般的な、Phoenixサーバーからデータを取得するサーバークライアント方式での実装について解説します
本記事は以上になりますありがとうございました