LoginSignup
8
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-03

はじめに

ひとりLiveView Advent Calendar の3日目の記事です

この記事はElixir Conf US 2021発表したシステムの構築と関連技術の解説を目的とした記事です

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

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

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を第一引数、
それ以降を第2引数のキーワードリストとして実行しています

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と第1引数のLiveLoggerWeb.MapLive.FormComponentをオプションに設定します
  3. Phoenix.LiveView.Helpers を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

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

  • use :live_component => stateを持つ Component、待たない場合はPhoenix.Componentを使います
  • id => live_componentはid要素が必須です
  • phx-capture-click => 子要素以外をクリックした時に実行
  • 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
  • live_patch raw("&times;") => index.htmlで指定したreturn_toに戻るボタン

  • live_component @component, @opts => index.htmlで指定したコンポーネントに@optsを渡して表示する

  • handle_event("close") => return_toで指定した箇所(:index or :show)に移動します

FormComponent

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

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>

先に表示部分を見ていきます
.formPhoenix.HTML.form_forを拡張したコンポーネントです

  • 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アプリから情報を受け取れるようにしていきます

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1