フォームは一度スタイリングができたら、他のページでも同じスタイリングでいくことがほとんどであると思います。inputタグを共通InputHelpersとして隠蔽すると、コーディングが楽になり、CSSクラスを気にせずに済むようになり、またテンプレートもスッキリとして読みやすくなります。
2021/3/12(金)〜 2021/3/15(月)開催のautoracex #16での成果です。
TL;DR
元ネタはJosé ValimさんのDynamic forms with Phoenixですが、それをもとに必須項目の印を表示させたり、Bootstrap 4に対応させたりしました。これは一例です。ひとそれぞれ好きなようにElixirでカスタマイズできます。
カスタムinput_tag
ヘルパーを用いラベル、スタイリング、エラーメッセージを含んだHTMLを動的に生成させます。
<%= f = form_for @changeset, "#", phx_submit: "submit-check-in-form" %>
<%= input_tag f, :name %>
<%= input_tag f, :phone %>
<%= submit "Check In", phx_disable_with: "Saving ...", class: "btn btn-primary" %>
</form>
未提出、提出後GOOD、提出後BADの3パターンの状態が考えられます。フォームの状態に対応したCSSクラスと共にHTMLが生成されます。
フォームtypeは、フィールド名から推測して生成します。デフォルトはPhoenix.HTML.Form.text_input/3
です。
フィールド名 | HTML生成に使用される関数 |
---|---|
:email |
Phoenix.HTML.Form.email_input/3 |
:password |
Phoenix.HTML.Form.password_input/3 |
:search |
Phoenix.HTML.Form.search_input/3 |
:url |
Phoenix.HTML.Form.url_input/3 |
<!-- 提出前 -->
<div class="form-group">
<label for="volunteer_name">Name *</label>
<input type="text"
class="form-control "
id="volunteer_name"
name="volunteer[name]"
placeholder="Name">
</div>
<!-- 提出後BAD -->
<div class="form-group">
<label for="volunteer_name">Name *</label>
<input type="text"
class="form-control is-invalid" <-- CSSが変化
id="volunteer_name"
name="volunteer[name]"
placeholder="Name"
value="">
<span class="invalid-feedback d-inline-block"
phx-feedback-for="volunteer_name">
can't be blank
</span>
</div>
<!-- 提出後GOOD -->
<div class="form-group">
<label for="volunteer_name">Name *</label>
<input type="text"
class="form-control is-valid" <-- CSSが変化
id="volunteer_name"
name="volunteer[name]"
placeholder="Name"
value="Masatoshi">
</div>
カスタムinput_tag
ヘルパー実装例
defmodule MnishiguchiWeb.InputHelpers do
use Phoenix.HTML
@custom_field_form_mapping %{
"phone" => :telephone_input
}
@doc """
Dynamically generates a Bootstrap 4 form input field.
http://blog.plataformatec.com.br/2016/09/dynamic-forms-with-phoenix/
## Examples
input_tag f, :name, placeholder: "Name", autocomplete: "off"
input_tag f, :phone, using: :telephone_input, placeholder: "Phone", autocomplete: "off"
"""
def input_tag(form, field, opts \\ []) do
# Some input type can be inferred from the field name.
input_fun_name = opts[:using] || Phoenix.HTML.Form.input_type(form, field, @custom_field_form_mapping)
required = opts[:required] || form |> input_validations(field) |> Keyword.get(:required)
label_text = opts[:label] || humanize(field)
permitted_input_opts = Enum.filter(opts, &(elem(&1, 0) in [:id, :name, :autocomplete, :placeholder]))
phx_attributes = Enum.filter(opts, &String.starts_with?(to_string(elem(&1, 0)), "phx_"))
custom_class = [class: "form-control #{form_state_class(form, field)}"]
input_opts =
(permitted_input_opts ++ phx_attributes ++ custom_class)
|> Enum.reject(&is_nil(elem(&1, 1)))
content_tag :div, class: "form-group" do
label = label_tag(form, field, label_text, required)
input = apply(Phoenix.HTML.Form, input_fun_name, [form, field, input_opts])
error = MnishiguchiWeb.ErrorHelpers.error_tag(form, field)
[label, input, error]
end
end
defp form_state_class(form, field) do
cond do
# Some forms may not use a Map as a source. E.g., :user
!is_map(form.source) -> ""
# Ignore Conn-based form.
Map.get(form.source, :__struct__) == Plug.Conn -> ""
# The form is not yet submitted.
!Map.get(form.source, :action) -> ""
# This field has an error.
form.errors[field] -> "is-invalid"
true -> "is-valid"
end
end
end
input type
Phoenix.HTML.Form.input_type/3により、input項目名をもとにtypeの決定します。仕組みはシンプルです。予め用意されたマッピングが使用されます。デフォルトのマッピングは下記のとおりです。第3引数にカスタムマッピングをしていするとデフォルトのマッピングにマージされます。
%{"email" => :email_input,
"password" => :password_input,
"search" => :search_input,
"url" => :url_input}
:xxx_input
アトムはPhoenix.HTML.Formに予め用意された関数名と一致している必要があります。
理解を深めるために、Iexで挙動を確認してみます。例では、volunteers
テーブルとVolunteer
スキーマがあることを想定しています。
iex> alias Mnishiguchi.Volunteers.Volunteer
iex> import Ecto.Changeset
iex> changeset = %Volunteer{} |> cast(%{}, [:name]) |> validate_required([:name])
#Ecto.Changeset<
action: nil,
changes: %{},
errors: [
name: {"can't be blank", [validation: :required]},
],
data: #Mnishiguchi.Volunteers.Volunteer<>,
valid?: false
>
iex> form = Phoenix.HTML.Form.form_for changeset, "#"
%Phoenix.HTML.Form{
action: "#",
data: %Mnishiguchi.Volunteers.Volunteer{
__meta__: #Ecto.Schema.Metadata<:built, "volunteers">,
id: nil,
inserted_at: nil,
name: nil,
updated_at: nil
},
errors: [],
hidden: [],
id: "volunteer",
impl: Phoenix.HTML.FormData.Ecto.Changeset,
index: nil,
name: "volunteer",
options: [method: "post"],
params: %{},
source: #Ecto.Changeset<
action: nil,
changes: %{},
errors: [
name: {"can't be blank", [validation: :required]},
],
data: #Mnishiguchi.Volunteers.Volunteer<>,
valid?: false
>
}
# using default mapping
iex> Phoenix.HTML.Form.input_type(form, :name)
:text_input
iex> Phoenix.HTML.Form.input_type(form, :email)
:email_input
iex> Phoenix.HTML.Form.input_type(form, :search)
:search_input
iex> Phoenix.HTML.Form.input_type(form, :password)
:password_input
iex> Phoenix.HTML.Form.input_type(form, :url)
:url_input
# 第3引数にカスタムマッピングを指定するとデフォルトにマージされます。
iex> Phoenix.HTML.Form.input_type(form, :denwa, %{"denwa" => :telephone_input})
:telephone_input
必須項目かどうか
必須項目かどうかはPhoenix.HTML.Form.input_validations/2で確認できます。
iex> form |> input_validations(:name)
[required: true]
iex> form |> input_validations(:name) |> Keyword.get(:required)
true
iex> form |> input_validations(:hello) |> Keyword.get(:required)
false
HTMLをElixirで組み立て
Phoenix.HTML.Tag.content_tag/2を用い、ElixirでHTMLを組み立てることができます。他にも同様の関数がPhoenix.HTML.Form functionsに用意されてます。
iex> Phoenix.HTML.Tag.content_tag(:p, "hello")
{:safe, [60, "p", [], 62, "hello", 60, 47, "p", 62]}
iex> Phoenix.HTML.Tag.content_tag(:p, "hello") |> Phoenix.HTML.safe_to_string
"<p>hello</p>"
humanize
Phoenix.HTML.Form.humanize/1が便利です。
iex> Phoenix.HTML.Form.humanize("name")
"Name"
iex> Phoenix.HTML.Form.humanize("hello_world")
"Hello world"
Top comments (0)