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 myTailwindFormBuilderinvoked, seemed kind of silly - Using one of Rails' built-in form helpers that work outside
form_with, like check_box_field, wouldn't invoke myTailwindFormBuilderand 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_nameis set to nil, so thenameattribute of each input isn't wrapped in brackets (e.g.name="some_object_name[pants]")objectdoesn't matter, because this isn't a real form, so myFauxFormObjectjust responds as if every possible value is a real property, as well as toerrorswith an empty hash (which my form builder uses to determine when to highlight validation problems in red)templateis set toself, because that seems to workoptionsis 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-sm size-3.5 focus:ring-3 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! 🦦🪄