Instantiate a custom Rails FormBuilder without using form_with
I'm building a Rails app using Tailwind, which works really well for almost everything out-of-the-box except forms, because—unless you relish the idea of repeating the exact same list of CSS classes for each and every field in your app—you're going to be left wanting for some way to extract all that messy duplication.
To that end, I've gradually built up a custom FormBuilder to house all my classes. (If you're looking for a starting point, here's a gist of what my TailwindFormBuilder currently looks like).
This works great when you're using form_with
, because the custom form builder
will automatically take over when you set the builder
option:
<%= form_with model: @user, builder: TailwindFormBuilder do |form| %>
<% end >
And if you set it globally with ActionView::Base.default_form_builder = FormBuilders::TailwindFormBuilder
, the custom builder becomes the default.
Nifty!
But what about when you need to render input elements outside the context of a
proper form? Today, I wanted to render some checkboxes for a client-side UI that
would never be "submitted" and for which no object made sense as an argument to
form_with
. Both immediately-available options are bad:
- Wrapping those checkboxes in an unnecessary
<form>
tag by passing a dummy object toform_with
, just for the side effect of having myTailwindFormBuilder
invoked, seemed kind of silly - Using one of Rails' built-in form helpers that work outside
form_with
, like check_box_field, wouldn't invoke myTailwindFormBuilder
and would therefore lack any of its CSS classes
Instead, I figured the best path forward would be to instantiate my form builder myself, even though that's not something the docs would encourage you to do to. So, I pulled up the FormBuilder#initialize source to see what arguments it needed:
def initialize(object_name, object, template, options)
# …
end
Lucky for us, the only thing that really matters above is the template
object,
which I (correctly, apparently) guessed could be passed as self
from an ERB
file or a view helper.
Here's a little helper I made to instantiate my custom TailwindFormBuilder
manually:
# 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
Explaining each of these arguments in order:
object_name
is set to nil, so thename
attribute of each input isn't wrapped in brackets (e.g.name="some_object_name[pants]"
)object
doesn't matter, because this isn't a real form, so myFauxFormObject
just responds as if every possible value is a real property, as well as toerrors
with an empty hash (which my form builder uses to determine when to highlight validation problems in red)template
is set toself
, because that seems to workoptions
is left as an empty hash, because I don't appear to depend on any yet
This, in turn, lets me render consistently-styled form fields anywhere I like. As a result, this ERB:
<%= faux_form.check_box(:pants, checked: true) %>
Will render with all my default checkbox classes:
<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"
>
This incongruity has been a pebble in my shoe for a couple years now, so I'm glad to finally have a working solution for rendering Tailwind-ified fields both inside and outside the context of a proper HTML form.
Hope you find it helpful! 🦦🪄