Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: カスタムFormBuilderをform_withなしでインスタンス化する(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

Rails: カスタムFormBuilderをform_withなしでインスタンス化する(翻訳)

私がTailwindで構築しているRailsアプリは、ほぼどんな機能もすぐその場で使えますが、フォームについては、すべてのフィールドでまったく同じCSSクラスのリストを繰り返し記述するのを避けるために、重複を何らかの形で抽出する方法が欲しくなります(そういう重複があっても構わないなら別ですが)。

そのために、私は自分のあらゆるクラスに配置するカスタムのFormBuilderをコツコツ手作りしてきました(作るときの出発点をお探しの方は、私が作った現状のTailwindFormBuilderをどうぞ)。

このカスタムFormBuilderは、form_withbuilderオプションを以下のように設定すると自動的に処理を引き継ぐので、form_withで使うのに最適です。

<%= form_with model: @user, builder: TailwindFormBuilder do |form| %>
<% end >

さらに、ActionView::Base.default_form_builder = FormBuilders::TailwindFormBuilderをグローバルに設定しておけば、このカスタムビルダーがデフォルトになります。いいですね!

しかし、通常のフォームのコンテキストの外でinput要素をレンダリングしなければならなくなったらどうでしょうか?今日の私は、クライアント側のUIでチェックボックスをいくつかレンダリングしたいのですが、これは決して「送信」されることはなく、form_withの引数に渡すのにふさわしいオブジェクトもありません。すぐ思いつける以下の2つのオプションは、どちらもよくありません。

  1. form_withにダミーのオブジェクトを渡すことで、チェックボックスを不要な<form>タグでラップする。
    私のTailwindFormBuilderが呼び出されるときの副作用を得るためだけにこんなことをするのは、少々馬鹿げている気がします。

  2. Railsに組み込まれているフォームヘルパーのうち、form_withの外でも動くものを使う(check_box_fieldなど)
    これでは私のTailwindFormBuilderが呼び出されず、このフォームビルダーのCSSクラスも反映されません。

こうした方法の代わりに、私のフォームビルダーを自分でインスタンス化するのがベストだと考えました(ドキュメントでそう推奨されているわけではありませんが)。そこで、FormBuilder#initializeのソースコードを拾ってきて、必要な引数を確認してみました。

def initialize(object_name, object, template, options)
  # ...
end

運の良いことに、ここで本当に必要なのはtemplateオブジェクトだけでした。このtemplateオブジェクトはERBファイルやビューヘルパーからselfとして渡されると推測したのですが、どうやら正しそうです。

以下は、TailwindFormBuilderヘルパーを手動でインスタンス化するために作った小さなヘルパーです。

# app/helpers/faux_form_helper.rb

module FauxFormHelper
  FauxFormObject = Struct.new do
    def errors
      {}
    end

    def method_missing(...)
    end

    def respond_to_missing?(...)
      true
    end
  end

  def faux_form
    @faux_form ||= FormBuilders::TailwindFormBuilder.new(
      nil,
      FauxFormObject.new,
      self,
      {}
    )
  end
end

個別の引数を出現順に説明します。

  1. object_name:
    これはnilに設定されるので、個別のinputのname属性は角かっこ[]で囲まれなくなります(例: name="some_object_name[pants]")。

  2. object:
    本当のフォームではないので、このobjectは重要ではありません。つまり、FauxFormObjectは、可能なすべての値があたかも本物のプロパティであるかのように応答し、errorsにも空ハッシュで応答します(私のフォームビルダーでは、バリデーションの問題を赤でハイライト表示するタイミングを指定するのに使っています)。

  3. template:
    これはselfに設定します(そうすることで動くようなので)。

  4. options:
    これは空ハッシュのままにしておきます(今はこれに依存することはなさそうなので)。

以上で、通常と同じスタイルを維持したフォームフィールドを好きな場所でレンダリングできるようになりました。以下は、これで得られたERBです。

<%= faux_form.check_box(:pants, checked: true) %>

上のレンダリング結果には、チェックボックス用のデフォルトCSSクラスが反映されます。

<input
  type="checkbox" value="1" checked="checked" name="pants" id="pants"
  class="block rounded size-3.5 focus:ring focus:ring-success checked:bg-success checked:hover:bg-success/90 cursor-pointer focus:ring-opacity-50   border border-gray-300 focus:border-success"
>

ここ数年、こうしたスタイルの不一致が悩みのタネだったのですが、ついに、通常のHTMLフォームコンテキストの中でも外でも、同じTailwindでスタイリングしたフィールドをレンダリングできる、実用的なソリューションが手に入ったことを嬉しく思います。

皆さんの参考になれば幸いです!🦦🪄

関連記事

Rails 5.1〜7.2: 'form_with' APIドキュメント(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。