ElixirDesktopリレーションの構築とコンポーネント改造でフォーム作成

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

GoogleBookAPIを使用して本のデータを取得するのですが、その下準備として本棚に本を登録するための関数とリレーションを作成します

# Books contextの作成

本を保存するコンテキストと画面を作成します
LiveViewのファイルをそのままは使わないのですが、ベースとして使うので一緒に作成します

```
mix phx.gen.live Books Book books title:string author:string thumb:string isbn:string published_date:date shelf_id:references:shelves
```

アプリ用マイグレーションを作成します
`priv/repo/migrations/xxxx_create_books.exs` を
`lib/bookshelf/migrations` にコピーして exs -> exに変更します

Repoのmigrationに追加します

```diff_elixir:lib/bookshelf/repo.ex
  def migration() do
    Ecto.Migrator.up(Bookshelf.Repo, 20_231_207_144_335, Bookshelf.Repo.Migrations.CreateShelves)
+   Ecto.Migrator.up(Bookshelf.Repo, 20_231_210_160_844, Bookshelf.Repo.Migrations.CreateBooks)
  end

```


スキーマファイルにリレーションの設定を行います

```diff_elixir:lib/bookshelf/books/book.ex
defmodule Bookshelf.Books.Book do
  use Ecto.Schema
  import Ecto.Changeset

  schema "books" do
    field :author, :string
    field :isbn, :string
    field :published_date, :date
    field :thumb, :string
    field :title, :string
-   field :shelf_id, :id
+   belongs_to :shelf, Bookshelf.Shelves.Shelf

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(book, attrs) do
    book
-   |> cast(attrs, [:title, :author, :thumb, :isbn, :published_date])
-   |> validate_required([:title, :author, :thumb, :isbn, :published_date])
+   |> cast(attrs, [:title, :author, :thumb, :isbn, :published_date, :shelf_id])
+   |> validate_required([:title, :author, :thumb, :isbn, :published_date, :shelf_id])
  end
end
```

関連先のshelfにhas_manyの設定をします

```diff_elixir:lib/bookshelf/shelves/shelf.ex
defmodule Bookshelf.Shelves.Shelf do
  use Ecto.Schema
  import Ecto.Changeset

  schema "shelves" do
    field :name, :string

+   has_many :books, Bookshelf.Books.Book

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(shelf, attrs) do
    shelf
    |> cast(attrs, [:name])
    |> validate_required([:name])
  end
end
```

# 検索ページの雛形作成


```lib/bookshelf_web/live/search_live/index.ex
defmodule BookshelfWeb.SearchLive.Index do
  use BookshelfWeb, :live_view

  @impl true
  def render(assigns) do
    ~H"""
    <.gheader title="Search" />
    <.bottom_tab title="Searh" />
    """
  end

  @impl true
  def mount(_params, _session, socket) do
    {:ok, socket}
  end
end
```


```diff_elixir:lib/bookshelf_web/components/navigation.ex
  defp links() do
    [
      {"Shelf", "hero-book-open-solid", "/shelves"},
-     {"Search", "hero-magnifying-glass-solid", "/shelves"},
+     {"Search", "hero-magnifying-glass-solid", "/search"},
      {"Setting", "hero-cog-6-tooth-solid", "/shelves"}
    ]
  end
```

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

    get "/", PageController, :home

    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 "/search", SearchLive.Index, :index  
  end
```

# 登録Formの作成

phx.gen.liveでbooksを作成したので、そちらのファイルを使いまわします

関連先の本棚を選択するセレクトボックスを追加

```diff_elixir:lib/bookshelf_web/live/book_live/form_component.ex
defmodule BookshelfWeb.BookLive.FormComponent do
  use BookshelfWeb, :live_component

  alias Bookshelf.Books
+ alias Bookshelf.Shelves

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        <%= @title %>
-       <:subtitle>Use this form to manage book records in your database.</:subtitle>
      </.header>

      <.simple_form
        for={@form}
        id="book-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:title]} type="text" label="Title" />
        <.input field={@form[:author]} type="text" label="Author" />
        <.input field={@form[:thumb]} type="text" label="Thumb" />
        <.input field={@form[:isbn]} type="text" label="Isbn" />
        <.input field={@form[:published_date]} type="date"label="Published date" />
+       <.input field={@form[:shelf_id]} type="select" label="Shelf" options={@shelf_options} />        
        <:actions>
          <.button phx-disable-with="Saving...">Save Book</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

  @impl true
  def update(%{book: book} = assigns, socket) do
    changeset = Books.change_book(book)
+   options = Shelves.list_shelves() |> Enum.map(&{&1.name, &1.id})

    {:ok,
     socket
     |> assign(assigns)
+    |> assign(:shelf_options, options)
     |> assign_form(changeset)}
  end
  ...
end
```

モーダル表示制御を実装

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

  alias Bookshelf.Books.Book

  @impl true
  def render(assigns) do
    ~H"""
    <.gheader title="Search" />
+    <.link patch={~p"/search/new"}>
+      <.button>New Book</.button>
+    </.link>
    <.bottom_tab title="Search" />

+   <.modal :if={@live_action in [:new]} id="book-modal" show on_cancel={JS.patch(~p"/search")}>
+     <.live_component
+       module={BookshelfWeb.BookLive.FormComponent}
+       id={:new}
+       title={@page_title}
+       action={@live_action}
+       book={@book}
+       patch={~p"/search"}
+     />
    </.modal>
    """
  end

  @impl true
  def mount(_params, _session, socket) do
    {:ok, socket}
  end

+ @impl true
+ def handle_params(params, _url, socket) do
+   {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ 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 Search")
+   |> assign(:book, nil)
+ end
end

```
ルーティングを追加

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

    get "/", PageController, :home

    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 "/search", SearchLive.Index, :index
+   live "/search/new", SearchLive.Index, :new
  end
```

new bookをクリックすると以下のフォームが表示されます

![スクリーンショット 2023-12-11 1.58.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/40f357e4-9bfa-ad7f-166e-72cb4aa83358.png)


# 本棚に登録した本を仮表示

登録後は追加でいくつか本を登録するのを考慮して、そのままページ推移は行いません
また、登録した本は各本棚のページから閲覧できるようにします

本棚を取得する時に一緒に本も取得するようにしています
```diff_elixir:lib/bookshelf/shelves.ex
- def get_shelf!(id), do: Repo.get!(Shelf,id)
+ def get_shelf!(id), do: Shelf |> preload(:books) |> Repo.get!(id)
```

streamsにshelf.booksを追加します

※以下は確認用で最終的には消すので、飛ばしても構いません

```diff_elixir:lib/bookshelf_web/live/shelf_live/show.ex
  @impl true
  def handle_params(%{"id" => id}, _, socket) do
+    shelf = Shelves.get_shelf!(id)

    {:noreply,
     socket
     |> assign(:page_title, page_title(socket.assigns.live_action))
+     |> stream(:books, shelf.books)
-    |> assign(:shelf, Shelves.get_shelf!(id))}
+    |> assign(:shelf, shelf)}
  end
```

tableをphx.gen.liveで作成したbook_live/indexからコピーしてアクション周りを削ります

```diff_elixir:lib/bookshelf_web/live/shelf_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>
<.list>
  <:item title="Name"><%= @shelf.name %></:item>
</.list>

+ <.table
+  id="books"
+  rows={@streams.books}
+ >
+  <:col :let={{_id, book}} label="Title"><%= book.title %></:col>
+  <:col :let={{_id, book}} label="Author"><%= book.author %></:col>
+  <:col :let={{_id, book}} label="Thumb"><%= book.thumb %></:col>
+  <:col :let={{_id, book}} label="Isbn"><%= book.isbn %></:col>
+  <:col :let={{_id, book}} label="Published date"><%= book.published_date %></:col>
+ </.table>

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

無事本棚に、本が登録されているのを確認できました

![スクリーンショット 2023-12-11 2.08.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/e41ff479-2111-05b5-2d23-9ab73878ee25.png)

# 最後に
本記事ではリレーションの構築と雛形作成から、phx.gen.liveで作成したコンポーネントをコピー&修正して、本の登録フォームと一覧を作成しました

次回はGoogleBookAPIのデータを使って本を追加します