Rails: カスタムFormBuilderをform_withなしでインスタンス化する(翻訳)
私がTailwindで構築しているRailsアプリは、ほぼどんな機能もすぐその場で使えますが、フォームについては、すべてのフィールドでまったく同じCSSクラスのリストを繰り返し記述するのを避けるために、重複を何らかの形で抽出する方法が欲しくなります(そういう重複があっても構わないなら別ですが)。
そのために、私は自分のあらゆるクラスに配置するカスタムのFormBuilder
をコツコツ手作りしてきました(作るときの出発点をお探しの方は、私が作った現状のTailwindFormBuilder
をどうぞ)。
このカスタムFormBuilderは、form_with
のbuilder
オプションを以下のように設定すると自動的に処理を引き継ぐので、form_with
で使うのに最適です。
<%= form_with model: @user, builder: TailwindFormBuilder do |form| %>
<% end >
さらに、ActionView::Base.default_form_builder = FormBuilders::TailwindFormBuilder
をグローバルに設定しておけば、このカスタムビルダーがデフォルトになります。いいですね!
しかし、通常のフォームのコンテキストの外でinput要素をレンダリングしなければならなくなったらどうでしょうか?今日の私は、クライアント側のUIでチェックボックスをいくつかレンダリングしたいのですが、これは決して「送信」されることはなく、form_with
の引数に渡すのにふさわしいオブジェクトもありません。すぐ思いつける以下の2つのオプションは、どちらもよくありません。
form_with
にダミーのオブジェクトを渡すことで、チェックボックスを不要な<form>
タグでラップする。
私のTailwindFormBuilder
が呼び出されるときの副作用を得るためだけにこんなことをするのは、少々馬鹿げている気がします。-
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
個別の引数を出現順に説明します。
object_name
:
これはnil
に設定されるので、個別のinputのname
属性は角かっこ[]
で囲まれなくなります(例:name="some_object_name[pants]"
)。-
object
:
本当のフォームではないので、このobject
は重要ではありません。つまり、FauxFormObject
は、可能なすべての値があたかも本物のプロパティであるかのように応答し、errors
にも空ハッシュで応答します(私のフォームビルダーでは、バリデーションの問題を赤でハイライト表示するタイミングを指定するのに使っています)。 -
template
:
これはself
に設定します(そうすることで動くようなので)。 -
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でスタイリングしたフィールドをレンダリングできる、実用的なソリューションが手に入ったことを嬉しく思います。
皆さんの参考になれば幸いです!🦦🪄
概要
元サイトの許諾を得て翻訳・公開いたします。