4
1

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.

ElixirAdvent Calendar 2017

Day 10

Elixir Phoenixアプリのフォームフィールドを一行で記述したい(2022年)

Last updated at Posted at 2022-05-15

Necessity is the mother of invention.

フォームのスタイリングを抽象化する方法はいくつかありますが、個人的にフォームフィールドはヘルパー関数を自作するのが一番シンプルな気がしており、実際にやってみた結果も気に入ってます。

やりたいこと

  • 各フォームフィールドを一行で描けるようにしたい。
  • 共通のCSSクラスは自動的に適用したい。

例えば、mix phx.gen.authコマンドで生成される以下のようなフォームフィールドがあります。

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

それをこのように共通のCSSクラスも含め一行で簡潔に記述したいのです。

<%= bulma_input f, :email %>

動作環境

elixir          1.13.4-otp-24
erlang          24.3.4
❯ mix phx.new --version
Phoenix installer v1.6.8

アイデア

カスタムビューヘルパーを書くために必要な知識とアイデアはElixir言語の作者José Valimさんがこの記事(Dynamic forms with Phoenix)の中で丁寧に解説してくれています。
ですのでそれを読めば大体わかります。

また、Phoenix自体がPhoenix.HTML.Form等ビルトインのヘルパー関数を多数持っているのでそれらを大いに活用することも大事だと思います。

やってみる

lib/my_app_web/views/input_helpers.exファイルを作成。

defmodule MyAppWeb.InputHelpers do
  use Phoenix.HTML

  # TODO: define my custom view helper functions
end

lib/my_app_web.exview_helpers関数でそれを忘れずにインポートしておく。

   defp view_helpers do
     quote do
       # Use all HTML functionality (forms, tags, etc)
       use Phoenix.HTML

       # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
       import Phoenix.LiveView.Helpers
       import MyAppWeb.LiveHelpers

       # Import basic rendering functionality (render, render_layout, etc)
       import Phoenix.View

+      import MyAppWeb.InputHelpers
       import MyAppWeb.ErrorHelpers
       import MyAppWeb.Gettext
       alias MyAppWeb.Router.Helpers, as: Routes
     end
   end

あとはMyAppWeb.InputHelpersに好きなようにヘルパー関数を定義するだけ。
先日たまたまBulma CSSフレームワークを使って遊んでいた時に、それ用に一つ作ってみました。
一つのサンプルコードになるかもしれません。

defmodule MyAppWeb.InputHelpers do
  use Phoenix.HTML

  def bulma_input(form, field, opts \\ []) do
    label_opts = Keyword.take(opts, ~w[required label]a)
    input_opts = Keyword.drop(opts, ~w[required label]a)

    content_tag :div, class: "field" do
      [
        build_label(form, field, label_opts),
        build_input(form, field, input_opts),
        MyAppWeb.ErrorHelpers.error_tag(form, field)
      ]
    end
  end

  def bulma_checkbox(form, field, opts \\ []) do
    content_tag :label, class: "checkbox" do
      [
        checkbox(form, field, opts),
        ' ',
        opts[:label] || field |> to_string() |> Phoenix.Naming.humanize()
      ]
    end
  end

  defp build_label(form, field, opts) do
    required = opts[:required] || Keyword.get(input_validations(form, field), :required)
    label_text = (opts[:label] || humanize(field)) <> if required, do: " *", else: ""

    Phoenix.HTML.Form.label(form, field, label_text, class: "label")
  end

  defp build_input(form, field, opts) do
    input_fun_name = opts[:using] || Phoenix.HTML.Form.input_type(form, field)
    permitted_attributes = Keyword.drop(opts, [:using])

    input_class =
      case input_fun_name do
        :textarea -> "textarea "
        _ -> "input "
      end <> form_state_class(form, field)

    input_opts =
      [{:class, input_class} | permitted_attributes]
      |> Enum.reject(&is_nil(elem(&1, 1)))

    content_tag :div, class: "control" do
      apply(Phoenix.HTML.Form, input_fun_name, [form, field, input_opts])
    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-danger"
      true -> "is-success"
    end
  end
end

lib/my_app_web/views/error_helpers.exPhoenixが生成したerror_tagがあるので、そこのCSSクラスも必要に応じて変更します。

   def error_tag(form, field) do
     Enum.map(Keyword.get_values(form.errors, field), fn error ->
       content_tag(:span, translate_error(error),
-       class: "invalid-feedback",
+       class: "invalid-feedback help is-danger",
         phx_feedback_for: input_name(form, field)
       )
     end)

Bulmaでスタイリングされたフォームフィールドを生成するbulma_input関数ができました。

bulma_input f, :email

使用するPhoenix.HTML.Formの関数を切り替えるオプションも受け付けます。

# Phoenix.HTML.Form.text_input/3の代わりにPhoenix.HTML.Form.textarea/3を使用したい場合
bulma_input f, :email, using: :textarea

必要に応じてHTML 属性を追加できるようにしました。

bulma_input f, :email, placeholder: "E-mail", autocomplete: "off"

IExで検証

テンプレート上のフォームで実際に実装した方が早いですが、興味があったのでIEx上でランできる方法を探しました。

Phoenixのソースコードのテストの中にヒントがあったので、その知識で適当にフォームを生成します。

iex

alias MyAppWeb.Accounts
alias MyAppWeb.Accounts.User

changeset = Accounts.change_user_registration(%User{})
form = Phoenix.HTML.Form.form_for(changeset, "/registration", [])

bulma_input(form, :email, placeholder: "E-mail", autocomplete: "off")
|> Phoenix.HTML.Safe.to_iodata()
|> to_string()
|> IO.puts()
<div class="field">
  <label class="label" for="user_email">Email *</label>
  <div class="control">
    <input autocomplete="off" class="input " id="user_email" name="user[email]" placeholder="E-mail" type="email">
  </div>
</div>

:tada:

Elixirコミュニティに初めて接する方は下記がオススメです

Elixirコミュニティ の歩き方 -国内オンライン編-

https://speakerdeck.com/elijo/elixirkomiyunitei-falsebu-kifang-guo-nei-onrainbian

image.png

日本には28箇所のElixirコミュニティがあります

image.png

日程からイベントを探すならElixirイベントカレンダー:calendar:

** Elixirイベントカレンダー **

https://elixir-jp-calendar.fly.dev/

image.png

4
1
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
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?