Custom Form Builders in Rails
Rails is packed with tools to cleanup your codebase, and customizing these tools can sometimes take that a step further.
In a recent project, our forms had begun to get cluttered. Special styling and markup for displaying validation and icons were being repeated for each form control.
/ login.html.slim
= form_for(:user, { url: '/login' }) do |f|
h1 Sign Into Your Account
.field#field-username
ul.errorlist
.field-wrapper
.indicator
= f.text_field :username, placeholder: 'Username'
.field#field-password
ul.errorlist
.field-wrapper
.indicator
= f.password_field :password, placeholder: 'Password'
.submit
= f.submit 'Sign In', class: 'button'
What we needed was a way to encapsulate these chunks of markup into reusable pieces. Luckily, through the power of inheritance, we can do just that.
Creating a New Form Builder
The first step is to sublcass the ActionView::Helpers::FormBuilder
class so we can begin overriding pieces of it for our own use. Since this builder is going to take over for our vertically-oriented forms, we'll call it VerticalFormBuilder
.
# app/assets/helpers/vertical_form_builder.rb
class VerticalFormBuilder < ActionView::Helpers::FormBuilder
...
end
The next step is to override the methods that create the various form elements. Inside those methods, we can supply our own markup (using content_tag
) and then simply call super
. If we pass in the correct arguments in our overridden methods, we can keep using all of the existing functionality supplied by Rails. Since we'll also be using a similar wrapper for some of the form fields, we'll break that out into its own function.
def field_wrapper(attribute, args, &block)
@template.content_tag(:div, { class: 'field', id: "field-#{attribute}" }) do
@template.content_tag(:ul, '', class: 'errorlist') +
@template.content_tag(:div, class: 'field-wrapper') do
@template.content_tag(:div, '', class: 'indicator') +
block.call
end
end
end
def text_field(attribute, args)
field_wrapper(attribute, args) do
super(attribute, args)
end
end
def password_field(attribute, args)
field_wrapper(attribute, args) do
super(attribute, args)
end
end
Adding a Helper Method
Now that we have the class to handle our form building, we need to add a helper to make calling the form easier in our markup. This lives in a module that Rails will pick up automatically (as a helper). Our custom form builder is used by adding a new method that calls form_for
with an argument supplying the modified builder class.
module VerticalFormHelper
def vertical_form_for(name, *args, &block)
options = args.extract_options!
args << options.merge(builder: VerticalFormBuilder)
form_for(name, *args, &block)
end
end
Cleaner Markup
With these changes, our form markup now looks something like this:
= vertical_form_for(:user, { url: '/login' }) do |f|
h1 Sign Into Your Account
= f.text_field :username, placeholder: 'Username'
= f.password_field :password, placeholder: 'Password'
.submit
= f.submit 'Sign In', class: 'button'
A huge improvement, especially if we ever have to make major changes to the way forms look throughout the app.
Next Steps...
This can be extended much further. We use similar methods to wrap validation settings and markup, and when paired with a controller, model validation, and some standardized Javascript, our forms all have real-time validation simply by using our custom form builder.