15
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LiveView CRUD自動生成が生成する<.form>と<%= text_input %>のハック

Last updated at Posted at 2023-01-12

【本コラムは、10分で読めて、10分くらいでお試しいただけます】
piacereElixirImp/fukuoka.exLiveView JP) です、ご覧いただいてありがとございます :bow:

LiveView CRUD自動生成(Scaffold)である mix phx.gen.live が生成するHTMLの <.form><%= text_input %> が、イマイチどんな挙動するか分からなくて、改造したり、カスタマイズする際に躓く人も多そうなので、中身で何をしているかをハックします

:ocean::ocean: Elixir Advent Calendar: 言語カテゴリ1位 & 全カテゴリ2位! :ocean::ocean:

例年を遥かに超える盛り上がりを見せ、堂々のトップ獲得ッ! :qiita: :tada: :confetti_ball:

https://qiita.com/advent-calendar/2022/elixir
https://qiita.com/advent-calendar/2022/ranking/feedbacks
https://qiita.com/advent-calendar/2022/ranking/feedbacks/categories/programming_languages
image.png

LiveView CRUD自動生成で出力される <input name>

下記のようなLiveView CRUD自動生成を行うと、LiveViewモジュールとHTML(.html.heex)が生成されます

mix phx.gen.live Blogs Blog blogs title:string body:text

HTMLは、以下のように生成されますが、この中の <%= text_input f, ~ %> 等は、どのようなHTMLを生成しているでしょう?

lib/basic_web/live/blog_live/form_component.html.heex
<div>
  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="blog-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">
  
    <%= label f, :title %>
    <%= text_input f, :title %>
    <%= error_tag f, :title %>
  
    <%= label f, :body %>
    <%= textarea f, :body %>
    <%= error_tag f, :body %>
  
    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>

下記のような <input ~> を生成しています
image.png

なお、name="blog[title]" がサブミット等されると、下記データフォーマットで送られてきます

%{
  "blog" => %{
    "body" => "b", 
    "title" => "a"
  }
}

基礎知識として、<.form let={f} ~> は、下記URLにある通り、<%= form_for ~ fn f ->%> と透過なコードを生成するので、f@changeset の情報が渡ります
https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#module-phoenix-liveview-integration
image.png

そして、name="blog[title]"blog の部分は、<.form> に指定された for=~ が元になっているのですが、ココが分かりにくいです

HTML展開後のlib/basic_web/live/blog_live/form_component.html.heex

  <form method="post" id="blog-form" phx-change="validate" phx-submit="save" phx-target="1">

  <label for="blog-form_title">Title</label>
+ <input id="blog-form_title" name="blog[title]" type="text">

  </form>

そこで、試しに下記のように書いてみます

lib/basic_web/live/blog_live/form_component.html.heex

  <.form
    let={f}
    for={:hoge}
    id="blog-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">

すると、下記のように blog の部分が hoge に変わります

HTML展開後のlib/basic_web/live/blog_live/form_component.html.heex

  <input id="blog-form_title" name="hoge[title]" type="text">

  </form>

サブミット等は、下記データフォーマットに変わります

%{
  "hoge" => %{
    "body" => "b", 
    "title" => "a"
  }
}

ここで、@changeset をデバッグしてみましょう

lib/basic_web/live/blog_live/form_component.html.heex

  <h3><%= inspect @changeset %></h3>

  <.form
    let={f}
    for={@changeset}
    id="blog-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">

中身は、こんな感じです … 上記で試したアトムとは、似ても似つかないため、全くピンと来ません
image.png

#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [
    title: {"can't be blank", [validation: :required]},
    body: {"can't be blank", [validation: :required]}
  ],
  data: #Basic.Blogs.Blog<>,
  valid?: false
>

Ecto.Changesetの生成箇所は、LiveViewモジュールの以下の通りですが、これが何故 blog を生成するのか、やはり分かりません…

lib/basic_web/live/blog_live/form_component.ex
defmodule BasicWeb.BlogLive.FormComponent do
  use BasicWeb, :live_component

  def update(%{blog: blog} = assigns, socket) do
+   changeset = Blogs.change_blog(blog)

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

  def handle_event("validate", %{"blog" => blog_params}, socket) do
    changeset =
      socket.assigns.blog
+     |> Blogs.change_blog(blog_params)
      |> Map.put(:action, :validate)

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

<.form> のハック

このままでは埒が明かないので、<.form>、つまり form_for の定義を見にいきます

form_for は、LiveViewのレポジトリでは無く、PhoenixHTMLのレポジトリにあります

Githubを検索すると、以下が見つかります(Elixir OSSの関数の検索は、このパターンで探せます)
image.png

ソースコードを見ていきます
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form.ex

すると、form.ex 冒頭に早速、form_for についての取り扱いが書かれています
image.png

どうやら form_for は、Changeset だけで無く、Conn やアトムを受け付ける仕様があり、アトムはコネクションを持たない場合に使うパターンであることが分かりました
image.png

以前、mix phx.gen.auth で生成される認証のコントローラをハックした際、下記のように @changeset@conn を使い分けているのを見て、「ん?…どういうこと?…」と謎に思っていましたが、その背景が分かりました

lib/basic_web/templates/account_confirmation/new.html.heex
<h1>Register</h1>

<.form let={f} for={@changeset} action={Routes.account_registration_path(@conn, :create)}>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <!--<p>Oops, something went wrong! Please check the errors below.</p>-->
      <p>ユーザ登録時にエラーが発生しました</p>
    </div>
  <% end %>

lib/basic_web/templates/account_confirmation/new.html.heex
<h1>Log in</h1>

<.form let={f} for={@conn} action={Routes.account_session_path(@conn, :create)} as={:account}>
  <%= if @error_message do %>
    <div class="alert alert-danger">
      <p><%= @error_message %></p>
    </div>
  <% end %>

  <%= label f, :email %>
  <%= email_input f, :email, required: true %>

オマケ:<.form> のソースコードのハック

上記内容だけでは、ハックとして拍子抜けなので、form_for のソースコードも追っておきます
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form.ex#L328
image.png

defimpl でアトムと Conn の多相性(polymorphism:ポリモーフィズム)を切り替えており、namebloghoge のような値が入ることになります(それを text_input が受け取って <input name=~ が作られると推測)
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form_data.ex#L51
image.png

アトムと Conn の扱いは、下記のような差があり、アトムのときは <.form for=~> で渡されたアトムを文字列に変更して、<input name=~> に使い、Conn のときは、:as が指定されていればソレを使い、未指定時は <input name=~>hoge[title] では無く、title だけとなります
image.png

:as で指定すると、for での指定を上書きできます

lib/basic_web/live/blog_live/form_component.html.heex
<div>
  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    as="foo"
    id="blog-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">

これは下記にオプションパラメータの記述があります(がここの書きっぷりはイマイチ挙動が見えない記述ね…)

text_inputname を使って構成するコードも追ってみましょう
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form.ex#L625
image.png

input_name は、f である form と、その後の :title 等である field から name を生成していそうです
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form.ex#L821
image.png

ビンゴッ! … input_name は、【name】[【field】] という書式を構成しており、これが blog[title]hoge[title] を生成します
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form.ex#L550
image.png

教訓:ソースコードを読む前にリファレンスを読もう

今回、確認した <.form> の仕様、hexdocに書かれていないか調べるために、上記説明をググったところ、普通に書かれていました :sweat_smile:
https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#module-with-limited-data
image.png

一昔前は、リファレンスを読んでも仕様が分からず、ソースコードを読む必要があったElixirですが、現在のElixirは、よほどマイナーなものでも無い限り、上記のようにリファレンスが準備されています

そのため、

「Elixirはライブラリやフレームワークのソースコードを読まないと分からない」

という古い頭の人(自分も含む)は、

「Elixirはリファレンスを読めば仕様が分かる」

にちゃんと切り替えましょう

15
6
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
15
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?