フォームは一度スタイリングができたら、他のページでも同じスタイリングでいくことがほとんどであると思います。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)