TailwindCSS is all about adding lots of utility classes to your markup to style elements directly. The problem with that, of course, is that you end up adding lots of utility classes to your markup. Like, a lot of classes. Dozens, sometimes, by the time you've accounted for the various break points and perhaps dark mode and what have you.
This can become tiresome, especially in the case of forms, where each element of the form may have all those classes applied and these classes need to be repeated for a dozen of elements in the form. Nobody has got time for that, I'm sure.
You want all the fields in your form to be consistent with each other, but you also want all the forms on your site to be consistent with each other, too. If you want to change how a text_field
tag looks you want to make that change in one place and see the change reflected everywhere. DRY, and all that.
Tailwind encourages us to extract components in our views as a way to reduce duplication. In Rails we also have the concept of partials that we can use to share view code between different contexts. If we have a form, we might extract it into a partial and use the same form in the new
and edit
views.
That's cool, but we still have to duplicate all the utility classes on every element in that form, and we can't share elements between forms for different things.
Not very DRY.
FormBuilders
But Rails has been doing this stuff for years, and obviously there is a super easy and well thought out solution for solving this exact problem: FormBuilders.
We can create our own FormBuilder
, supplement the default behaviour with our TailwindCSS utility classes, and then whenever we write f.text_field :whatever
we'll get a nicely styled text field that looks the same as every other text field on our site.
Here's how to do that (presuming you already have TailwindCSS in your Rails application, which is beyond the scope of this article).
Install the Tailwind forms plugin
To get us off to a good start, install the Tailwind forms plugin. This'll give us some nice forms out of the box, and allow us to style things further.
Create a form builder
I like to create a folder in app
to keep form builders in. Files in subfolders of app
are automatically loaded, and if we decide to add more form builders later then there is a sensible place to put them.
The basic form builder looks like this:
class TailwindFormBuilder < ActionView::Helpers::FormBuilder
def text_field(method, opts={})
default_opts = { class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 #{'border-2 border-red-500' if @object.errors.any?}" }
merged_opts = default_opts.merge(opts)
super(method, merged_opts)
end
end
We create a new class that inherits from ActionView::Helpers::FormBuilder
and then go ahead and override the text_field
method it provides. In our version, we're going to slam a bunch of Tailwind class names into the options hash we're given by default. This allows us to set different classes on a case by case basis but it's a fairly crude implementation so if we want to override one class we have to supply all the classes we want to see applied to the final element. Finally, we super
the original method
we were passed and our freshly mangled opts
hash up to the original text_field
method we're overriding and let it do its thing.
A more sophisticated solution is left as an exercise to the reader.
Use the FormBuilder
We can then use the form builder very simply by passing it to form_with
:
<main class="p-12 mx-auto text-gray-800 max-w-7xl">
<%= form_with model: @widget, builder: TailwindFormBuilder, class: "mt-6" do |f| %>
<%= f.label :reference %>
<%= f.text_field :reference, placeholder: "Your reference" %>
<div class="pt-5">
<%= f.submit "Begin" %>
</div>
<% end %>
</main>
When we render the form and take a look at our text field, we see all our wonderful TailwindCSS classes!
<input class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indiogo-300 focus:ring focus:ring-indiogo-200 focus:ring-opacity-50 " placeholder="Your reference" type="text" name="widget[reference]" id="widget_reference">
We can rinse and repeat this process for any and every Rails form tag we want to apply sensible default TailwindCSS styles to.
Making TailwindFormBuilder the default
Making our form builder the default is easy, too. In our ApplicatioHelper
, we can set it as the default like this:
ActionView::Base.default_form_builder = TailwindFormBuilder
We can then remove builder: TailwindFormBuilder
from our call to form_with
in the view, and all forms will use our nicely styled text_field
s by default.
Conclusion
This is obviously a super simple example of what you can do with FormBuilders in Rails and how they can help tuck those CSS utility classes out of the way. We can do some neat things like making the border of the text_area
red if the object has errors. We can even start cracking out content_tag
s to create chunks of HTML that include labels and inline error messages for each element. So far I'm still trying to find the balance between "everything is in the form builder" and "nothing is in the form builder". You'll notice above that the form is still managing a bit of it's own spacing, which may or may not be worth moving into the TailwindFormBuilder
, but I so far I've kept label
seperate from text_field
, for example, and I think I'm happy with that.
I'm currently experimenting with appending a ul
of error messages to each element in the form as a way of displaying nicely styled inline error messages to the user. This seems to be a bit of a reach, at first, because the markup for the errors don't appear in the form itself. But because they are applied by the form builder, they are applied consistently and to every text_field
that the form is used for. That's just how errors are dealt with in this kind of form. I quite like that (so far).
FormBuilders are a great way to tuck utility CSS classes away in Rails applications.
Top comments (0)