Phoenixで作るGPS Logging System 3 CRUDの中身 part2

# はじめに
[ひとりLiveView Advent Calendar](https://qiita.com/advent-calendar/2021/liveview_alone) の3日目の記事です

この記事は[Elixir Conf US 2021](https://www.elixirconf.com/)の[発表](https://www.youtube.com/watch?v=vHNqZmLLTj4)したシステムの構築と関連技術の解説を目的とした記事です

今回はphx.gen.liveで作成されたモーダルについて解説します

最初にindex.htmlのモーダルを表示する箇所を見てみましょう

```leex:lib/live_logger_web/live/map_live/index.html.heex
<%= if @live_action in [:new, :edit] do %>
  <%= live_modal LiveLoggerWeb.MapLive.FormComponent,
    id: @map.id || :new,
    title: @page_title,
    action: @live_action,
    map: @map,
    return_to: Routes.map_index_path(@socket, :index) %>
<% end %>
```

@live_actionは前回出ててきたので大丈夫ですね、routingで最後につけるatomで、
:new,:editの場合のみこれを表示します。
次が`live_modal`という関数を`LiveLoggerWeb.MapLive.FormComponent`を第一引数、
それ以降を第２引数のキーワードリストとして実行しています

# live_modal
`live_modal`はどういう関数かというと`phx.gen.live`で作成されたlive_helpers.exにあり

```lib/live_logger_web/live/live_helpers.ex
defmodule LiveLoggerWeb.LiveHelpers do
  import Phoenix.LiveView.Helpers

  def live_modal(component, opts) do
    path = Keyword.fetch!(opts, :return_to)
    modal_opts = [id: :modal, return_to: path, component: component, opts: opts]
    live_component(LiveLoggerWeb.ModalComponent, modal_opts)
  end
end
```

1. optsのreturn_toだけ必須項目として取り出して、なければエラーを吐きます
2. return_toと第１引数のLiveLoggerWeb.MapLive.FormComponentをオプションに設定します
3. [Phoenix.LiveView.Helpers](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Helpers.html#live_component/1) をimportして`LiveLoggerWeb.ModalComponent`を第一引数にしてlive_componentを実行します

# ModalComponent

```lib/live_logger_web/live/modal_component.ex
defmodule LiveLoggerWeb.ModalComponent do
  use LiveLoggerWeb, :live_component

  @impl true
  def render(assigns) do
    ~H"""
    <div
      id={@id}
      class="phx-modal"
      phx-capture-click="close"
      phx-window-keydown="close"
      phx-key="escape"
      phx-target={@myself}
      phx-page-loading>

      <div class="phx-modal-content">
        <%= live_patch raw("&times;"), to: @return_to, class: "phx-modal-close" %>
        <%= live_component @component, @opts %>
      </div>
    </div>
    """
  end

  @impl true
  def handle_event("close", _, socket) do
    {:noreply, push_patch(socket, to: socket.assigns.return_to)}
  end
end

```

１つずつ見ていきましょう

- `use :live_component` => stateを持つ Component、待たない場合はPhoenix.Componentを使います
- `id` => live_componentはid要素が必須です
- `phx-capture-click` => [子要素以外をクリックした時](https://hexdocs.pm/phoenix_live_view/0.16.0/bindings.html#click-events)に実行
- `phx-window-keydown` => phx-keyで指定したキーを押した時に実行
- `phx-target` => どのコンポーネントのhandle_eventを発火させるか、今回は@myselfで自分自身を指定
- `phx-page-loading` -> 公式ドキュメントには以下のように書かれているがよくわからなかった
[Additionally, any phx- event may dispatch page loading events by annotating the DOM element with phx-page-loading](https://hexdocs.pm/phoenix_live_view/0.16.0/js-interop.html#loading-state-and-errors)
- `live_patch raw("&times;")` => index.htmlで指定したreturn_toに戻るボタン

- `live_component @component, @opts` => index.htmlで指定したコンポーネントに@optsを渡して表示する
- `handle_event("close")` => return_toで指定した箇所(:index or :show)に移動します

# FormComponent
　汎用モーダル部分が終わったので個別のフォームのコンポーネントを見ていきましょう


```leex:lib/live_logger_web/live/map_live/form_component.html.heex
<div>
  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="map-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">
  
    <%= label f, :name %>
    <%= text_input f, :name %>
    <%= error_tag f, :name %>
  
    <%= label f, :description %>
    <%= text_input f, :description %>
    <%= error_tag f, :description %>
  
    <%= hidden_input f, :user_id %>
    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>
```
先に表示部分を見ていきます
[.form](https://github.com/phoenixframework/phoenix_live_view/blob/bae735e0998c13aa9bb0518021092524d24479d1/lib/phoenix_live_view/helpers.ex#L875)は[Phoenix.HTML.form_for](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#form_for/3)を拡張したコンポーネントです

- let => fにformをパターンマッチさせています
- for => form_forの第一引数form_dataにchangesetを渡しています
- phx-target => イベントのターゲットを自分自身にしています
- phx-change => フォームの内容が変更されたら validationイベントを発火させます
- phx-submit => submitボタンを押したら saveイベントを発火させます

中のタグは従来どおりのPhoenix.HTMLで問題なく書けます

```lib/live_logger_web/live/map_live/form_component.ex
defmodule LiveLoggerWeb.MapLive.FormComponent do
  use LiveLoggerWeb, :live_component

  alias LiveLogger.Loggers

  @impl true
  def update(%{map: map} = assigns, socket) do
    changeset = Loggers.change_map(map)

    {:ok,
     socket
     |> assign(assigns)
     |> assign(:changeset, changeset)}
  end

  @impl true
  def handle_event("validate", %{"map" => map_params}, socket) do
    changeset =
      socket.assigns.map
      |> Loggers.change_map(map_params)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, :changeset, changeset)}
  end

  def handle_event("save", %{"map" => map_params}, socket) do
    save_map(socket, socket.assigns.action, map_params)
  end

  defp save_map(socket, :edit, map_params) do
    case Loggers.update_map(socket.assigns.map, map_params) do
      {:ok, _map} ->
        {:noreply,
         socket
         |> put_flash(:info, "Map updated successfully")
         |> push_redirect(to: socket.assigns.return_to)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, :changeset, changeset)}
    end
  end

  defp save_map(socket, :new, map_params) do
    case Loggers.create_map(map_params) do
      {:ok, _map} ->
        {:noreply,
         socket
         |> put_flash(:info, "Map created successfully")
         |> push_redirect(to: socket.assigns.return_to)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end
end
```

次にロジック部分を見ていきます

life-cycleとしては`mount -> update -> render`となっていますが
mountはformでは使わないので省略されています

- update => 変更を検知した際の処理が記述されています
- validate => フォームデータの変更を検知してchangesetでvalidationを実行しています
- save => submit時にactionで関数のパターンマッチを行って update, createに振り分けています


# まとめ
モーダル表示は以下の順番で実行されていることがわかりました
index.html ->  live_helpers.live_modal -> modal_component -> form_component

live_component内でlive_component関数を実行することによってネストしたコンポーネントが作成できることがわかりました

# 最後に
　LiveViewやlive_componentはまだ出たばかりで情報や実装例が少ないですが、軽く触っただけでもLiveViewではAPIを通すことなくDBに関するコードを実行し結果を即時反映できるので、react/vueなどを使ったrich frontendにありがちなAPI作成地獄から開放されることができるでしょう！（願望）

CURDの中身は以上になります

次はAPIを作成してGPS Loggerアプリから情報を受け取れるようにしていきます