Ruby on Rails ships with form helpers based in Ruby out of the box. These helpers are helpful for dynamically generating HTML fields on forms that pertain to the data layer backed by ActiveRecord in a given application.
When generating a new resource in rails a common path for many developers is to scaffold a new model. A scaffold contains everything you need from the data side to the front end views. This practice is a big time saver for a developer looking to be extremely productive.
When generating such a resource there is commonly a new form view partial included. The form is how data goes from an end-user to a database. There's a lot to know about how Rails handles forms in a given app but this guide is focused on customizing the default form helper methods and/or creating your own from scratch.
Goals of automation with Tailwind CSS
In the video, I reference my kickoff_tailwind Ruby on Rails application template. The template uses Tailwind CSS for stylesheets. Tailwind is a highly productive CSS framework but it comes with a lot of repetitive qualities that inspired me to create a custom FormBuilder
.
Ultimately, the goal here is to allow any new resource that gets generated come already set up with Tailwind CSS classes. Doing this would allow me to not have to go back and add them every time.
My template comes with some form styles by default but they still need to be applied manually. Having a FormBuilder
such as this could automate that.
Creating a custom FormBuilder class
Inside the app
directory of a Ruby on Rails app you can create custom extensions in addition to what comes stock. This folder gets auto-loaded so there's very little to configure outside of whatever you're adding.
For the purposes of this guide I'll make a new folder called builders
. Inside it I'll create a new file called tailwind_builder.rb
.
This file will inherit from what's already part of the framework.
# app/builders/tailwind_builder.rb
class TailwindBuilder < ActionView::Helpers::FormBuilder
# include ActionView::Helpers::TagHelper
# include ActionView::Context
def text_field(attribute, options={})
super(attribute, options.reverse_merge(class: "input"))
end
def text_area(attribute, options={})
super(attribute, options.reverse_merge(class: "input"))
end
def select(object_name, method_name, template_object, options={})
super(object_name, method_name, template_object, options.reverse_merge(class: "select"))
end
def div_radio_button(method, tag_value, options = {})
@template.content_tag(:div,
@template.radio_button(
@object_name, method, tag_value, objectify_options(options)
)
)
end
end
In this file, you can invent new form helpers or extend existing helpers using the super()
method. Since my goal is to only modify the class attribute on the generated HTML I'll need to extend existing fields to accommodate.
If you look at the source code for a text_field
form helper, for example, you can get a sense of what parameters need to be passed through.
# https://github.com/rails/rails/blob/914caca2d31bd753f47f9168f2a375921d9e91cc/actionview/lib/action_view/helpers/tags/text_field.rb
# frozen_string_literal: true
require "action_view/helpers/tags/placeholderable"
module ActionView
module Helpers
module Tags # :nodoc:
class TextField < Base # :nodoc:
include Placeholderable
def render
options = @options.stringify_keys
options["size"] = options["maxlength"] unless options.key?("size")
options["type"] ||= field_type
options["value"] = options.fetch("value") { value_before_type_cast } unless field_type == "file"
add_default_name_and_id(options)
tag("input", options)
end
class << self
def field_type
@field_type ||= name.split("::").last.sub("Field", "").downcase
end
end
private
def field_type
self.class.field_type
end
end
end
end
end
There is a lot to unpack here but the line tag("input", options)
is about all we need to know.
Unfortunately, there is little documentation on the API docs so I would suggest using the actual code as your guide.
There is a large list of form helper tags you can view the source on via Github to get a better sense of what's going on under the hood.
Putting the builder to use
We haven't declared the builder in our view just yet so nothing should change from the default rendering. To do so you can simply pass a builder: TailwindBuilder
option on the form.
For this guide, I generated a Post model scaffold by running
rails g scaffold Post title content:text
And modified the form in app/views/posts/_form.html.erb
.
<%= form_with(model: post, builder: TailwindBuilder) do |form| %>
<% if post.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>
<ul>
<% post.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="mb-6">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="mb-6">
<%= form.label :content %>
<%= form.text_area :content %>
</div>
<%= form.div_radio_button :title, "Test" %>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
With this code in place our basic text_field
and text_area
fields not have an input
class I created prior that stems from a style sheet in my template.
/* app/javascript/stylesheets/components/_forms.scss */
%focus-style {
@apply shadow outline-none border-gray-500;
box-shadow: 0 0 0 0.2rem theme("colors.gray.200");
background-clip: padding-box;
}
.input {
@apply appearance-none block w-full text-gray-700 border border-gray-400 rounded px-3 leading-tight bg-white shadow-inner;
padding-top: .65rem;
padding-bottom: .65rem;
}
.input:focus,
.input:hover {
@extend %focus-style;
}
...
Opting into a form builder app-wide
Adding a form builder option to every form you create could get tedious and repetitive. Luckily, there is a way to enable a default builder on the application configration level.
# app/config/application.rb
require_relative "boot"
require "rails/all"
Bundler.require(*Rails.groups)
module CustomFormBuilder
class Application < Rails::Application
...
config.action_view.default_form_builder = TailwindBuilder
...
end
end
Don't overdo it
I think form helpers solve a lot of problems for a rails developer looking to move fast. Extending them is a great way to increase speed but it comes at cost if you want more customization down the line. Generating large blocks of HTML might be overkill or it might be desired given your team size and how often you're creating form data. Hopefully this guide proved useful!
Happy Coding!
New to Ruby on Rails? I made a course for you.
Hello Rails is a modern course designed to help you start using and understanding Ruby on Rails fast. Get 10% off the master version of the course using the promo code RAILZ2021
Top comments (0)