【本コラムは、10分で読めて、10分くらいでお試しいただけます】
piacere (ElixirImp/fukuoka.ex、LiveView JP) です、ご覧いただいてありがとございます
LiveView CRUD自動生成(Scaffold)である mix phx.gen.live
が生成するHTMLの <.form>
と <%= text_input %>
が、イマイチどんな挙動するか分からなくて、改造したり、カスタマイズする際に躓く人も多そうなので、中身で何をしているかをハックします

Elixir Advent Calendar: 言語カテゴリ1位 & 全カテゴリ2位! 
例年を遥かに超える盛り上がりを見せ、堂々のトップ獲得ッ!
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
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を生成しているでしょう?
<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>
なお、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
そして、name="blog[title]"
の blog
の部分は、<.form>
に指定された for=~
が元になっているのですが、ココが分かりにくいです
…
<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>
…
そこで、試しに下記のように書いてみます
…
<.form
let={f}
for={:hoge}
id="blog-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save">
…
すると、下記のように blog
の部分が hoge
に変わります
…
<input id="blog-form_title" name="hoge[title]" type="text">
…
</form>
…
サブミット等は、下記データフォーマットに変わります
%{
"hoge" => %{
"body" => "b",
"title" => "a"
}
}
ここで、@changeset
をデバッグしてみましょう
…
<h3><%= inspect @changeset %></h3>
<.form
let={f}
for={@changeset}
id="blog-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save">
…
中身は、こんな感じです … 上記で試したアトムとは、似ても似つかないため、全くピンと来ません
#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
を生成するのか、やはり分かりません…
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の関数の検索は、このパターンで探せます)
ソースコードを見ていきます
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form.ex
すると、form.ex
冒頭に早速、form_for
についての取り扱いが書かれています
どうやら form_for
は、Changeset
だけで無く、Conn
やアトムを受け付ける仕様があり、アトムはコネクションを持たない場合に使うパターンであることが分かりました
以前、mix phx.gen.auth
で生成される認証のコントローラをハックした際、下記のように @changeset
と @conn
を使い分けているのを見て、「ん?…どういうこと?…」と謎に思っていましたが、その背景が分かりました
<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 %>
…
<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
defimpl
でアトムと Conn
の多相性(polymorphism:ポリモーフィズム)を切り替えており、name
に blog
や hoge
のような値が入ることになります(それを text_input
が受け取って <input name=~
が作られると推測)
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form_data.ex#L51
アトムと Conn
の扱いは、下記のような差があり、アトムのときは <.form for=~>
で渡されたアトムを文字列に変更して、<input name=~>
に使い、Conn
のときは、:as
が指定されていればソレを使い、未指定時は <input name=~>
が hoge[title]
では無く、title
だけとなります
:as
で指定すると、for
での指定を上書きできます
<div>
<h2><%= @title %></h2>
<.form
let={f}
for={@changeset}
as="foo"
id="blog-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save">
…
これは下記にオプションパラメータの記述があります(がここの書きっぷりはイマイチ挙動が見えない記述ね…)
text_input
が name
を使って構成するコードも追ってみましょう
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form.ex#L625
input_name
は、f
である form
と、その後の :title
等である field
から name
を生成していそうです
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form.ex#L821
ビンゴッ! … input_name
は、【name】[【field】]
という書式を構成しており、これが blog[title]
や hoge[title]
を生成します
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form.ex#L550
教訓:ソースコードを読む前にリファレンスを読もう
今回、確認した <.form>
の仕様、hexdocに書かれていないか調べるために、上記説明をググったところ、普通に書かれていました
https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#module-with-limited-data
一昔前は、リファレンスを読んでも仕様が分からず、ソースコードを読む必要があったElixirですが、現在のElixirは、よほどマイナーなものでも無い限り、上記のようにリファレンスが準備されています
そのため、
「Elixirはライブラリやフレームワークのソースコードを読まないと分からない」
という古い頭の人(自分も含む)は、
「Elixirはリファレンスを読めば仕様が分かる」
にちゃんと切り替えましょう