はじめに
ひとりLiveView Advent Calendar の3日目の記事です
この記事はElixir Conf US 2021の発表したシステムの構築と関連技術の解説を目的とした記事です
今回はphx.gen.liveで作成されたモーダルについて解説します
最初にindex.htmlのモーダルを表示する箇所を見てみましょう
<%= 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にあり
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
- optsのreturn_toだけ必須項目として取り出して、なければエラーを吐きます
- return_toと第1引数のLiveLoggerWeb.MapLive.FormComponentをオプションに設定します
-
Phoenix.LiveView.Helpers をimportして
LiveLoggerWeb.ModalComponent
を第一引数にしてlive_componentを実行します
ModalComponent
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("×"), 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("×")
=> index.htmlで指定したreturn_toに戻るボタン -
live_component @component, @opts
=> index.htmlで指定したコンポーネントに@optsを渡して表示する -
handle_event("close")
=> return_toで指定した箇所(:index or :show)に移動します
FormComponent
汎用モーダル部分が終わったので個別のフォームのコンポーネントを見ていきましょう
<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はPhoenix.HTML.form_forを拡張したコンポーネントです
- let => fにformをパターンマッチさせています
- for => form_forの第一引数form_dataにchangesetを渡しています
- phx-target => イベントのターゲットを自分自身にしています
- phx-change => フォームの内容が変更されたら validationイベントを発火させます
- phx-submit => submitボタンを押したら saveイベントを発火させます
中のタグは従来どおりのPhoenix.HTMLで問題なく書けます
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アプリから情報を受け取れるようにしていきます