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

【本コラムは、10分で読めて、10分くらいでお試しいただけます】
[piacere](https://twitter.com/piacere_ex) （[ElixirImp／fukuoka.ex](https://fukuokaex.connpass.com)、[LiveView JP](https://liveviewjp.connpass.com/)） です、ご覧いただいてありがとございます :bow:

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

<h3>:ocean::ocean: Elixir Advent Calendar: 言語カテゴリ1位 & 全カテゴリ2位！ :ocean::ocean:</h3>

例年を遥かに超える盛り上がりを見せ、堂々のトップ獲得ッ！ :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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/356a15c6-0dbb-097e-38f3-a1b4fed47555.png)](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を生成しているでしょう？

```diff_elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/de316e4e-926c-ec99-c17c-0093a3fa79a5.png)

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

```diff_elixir
%{
  "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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/967a900d-494a-025a-5a6a-8a34152ae971.png)

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

```diff_elixir: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>
…
```  

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

```diff_elixir: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` に変わります

```diff_elixir:HTML展開後のlib/basic_web/live/blog_live/form_component.html.heex
…
  <input id="blog-form_title" name="hoge[title]" type="text">
…
  </form>
…
```  

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

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

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

```diff_elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/936a3a68-e7e4-8d7b-9f0d-17f69554ddfe.png)

```diff_elixir
#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` を生成するのか、やはり分かりません…

```diff_elixir: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のレポジトリにあります

https://github.com/phoenixframework/phoenix_html

Githubを検索すると、以下が見つかります（Elixir OSSの関数の検索は、このパターンで探せます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/1eed6f6e-0c37-1bee-c74e-c1ccb8c872b7.png)

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

すると、`form.ex` 冒頭に早速、`form_for` についての取り扱いが書かれています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/cd8f17fc-7378-bc7e-79ca-89029d330be4.png)

どうやら `form_for` は、`Changeset` だけで無く、`Conn` やアトムを受け付ける仕様があり、アトムはコネクションを持たない場合に使うパターンであることが分かりました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/04612f69-dff9-a3fd-8037-712f9261d7c9.png)

[以前、`mix phx.gen.auth` で生成される認証のコントローラをハック](https://qiita.com/piacerex/items/6d6963e18463eb920971)した際、下記のように `@changeset` と `@conn` を使い分けているのを見て、「ん？…どういうこと？…」と謎に思っていましたが、その背景が分かりました

```elixir: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 %>
…
```

```erb: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/eca5069f-5123-2d0e-5fba-5a4e23a4a965.png)

`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
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/b97574d2-ea7e-947f-d067-ea2708955920.png)

アトムと `Conn` の扱いは、下記のような差があり、アトムのときは `<.form for=～>` で渡されたアトムを文字列に変更して、`<input name=～>` に使い、`Conn` のときは、`:as` が指定されていればソレを使い、未指定時は `<input name=～>` が `hoge[title]` では無く、`title` だけとなります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/8c63727b-abd3-64df-6a2d-c4b191e937a8.png)

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

```diff_elixir: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">
…
```

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

https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#form_for/4-options

`text_input` が `name` を使って構成するコードも追ってみましょう
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form.ex#L625
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/8abe7641-110a-fab8-4f90-d13e314f5cf3.png)

`input_name` は、`f` である `form` と、その後の `:title` 等である `field` から `name` を生成していそうです
https://github.com/phoenixframework/phoenix_html/blob/394f3605f656abe21432d6a11d3d75f4d5c00f7e/lib/phoenix_html/form.ex#L821
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/38087015-d137-4ebf-957d-711c6922edb8.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/cbc4da19-b02a-cc83-9638-a18f02e9d237.png)

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

今回、確認した `<.form>` の仕様、hexdocに書かれていないか調べるために、上記説明をググったところ、普通に書かれていました :sweat_smile:
https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#module-with-limited-data
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/8daa3f63-c036-98ee-101e-e4135daca430.png)

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

そのため、

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

という古い頭の人（自分も含む）は、

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

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