Yihang Ho bio photo

Yihang Ho

Coder

Twitter Github Email

The form builder is a mechanism that Rails exposes for us to customize the template and behavior of HTML forms. We will explore how this is done. The end product of this tutorial is available at this GitHub repository.

The Problem

Suppose we are using Bootstrap in a Rails project. To make use of Bootstrap, we need to attach certain CSS classes to certain DOM elements. Let's try to build a sign up form for this app. Our goal is to produce something that looks like this:

Looking at the Bootstrap documentation, this is the required HTML/ERB:

<!-- app/views/users/new.html.erb -->
<%= form_with model: @user, local: true do |form| %>
  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:name) %>">
    <%= form.label :name, class: "control-label" %>
    <%= form.text_field :name, class: "form-control" %>

    <% @user.errors.full_messages_for(:name).each do |message| %>
      <p class="help-block"><%= message %></p>
    <% end %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:email) %>">
    <%= form.label :email, class: "control-label" %>
    <%= form.email_field :email, class: "form-control" %>

    <% @user.errors.full_messages_for(:email).each do |message| %>
      <p class="help-block"><%= message %></p>
    <% end %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:password) %>">
    <%= form.label :password, class: "control-label" %>
    <%= form.password_field :password, class: "form-control" %>

    <% @user.errors.full_messages_for(:password).each do |message| %>
      <p class="help-block"><%= message %></p>
    <% end %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:password_confirmation) %>">
    <%= form.label :password_confirmation, class: "control-label" %>
    <%= form.password_field :password_confirmation, class: "form-control" %>

    <% @user.errors.full_messages_for(:password_confirmation).each do |message| %>
      <p class="help-block"><%= message %></p>
    <% end %>
  </div>

  <%= form.submit "Sign Up", class: "btn btn-primary" %>
<% end %>

It gets old pretty fast with all these repetitions.

Custom Form Builder

We will DRY up the markup. We notice the following repetitions (and will fix them in this order):

  1. The repeated for-loop used to generate the error message.
  2. The repeated control-label class name for the labels.
  3. The repeated form-control class name for the text fields.
  4. The repeated form-group class name and conditional in the class name of the container divs.

There are several ways for us to DRY up the markup. For example, partials and helpers might work. However, there is a better approach. We can customize the form builder for this particular form. A form builder is an object capable of producing the markup for a form. By default, ActionView::Helpers::FormBuilder is used. The general strategy here is to create a subclass of this, and override and implement certain methods to arrive at the desired results.

First, we create an errors method which will output the markup for the error messages:

# app/helpers/bootstrap_form_builder.rb
class BootstrapFormBuilder < ActionView::Helpers::FormBuilder
  def errors(method)
    object.errors.full_messages_for(method).map { |m| help_block(m) }.join.html_safe
  end

  def help_block(message)
    %Q(<span class="help-block">#{message}</span>).html_safe
  end
end

The implementation should be very straightforward with the following caveats:

  1. The object method used in the errors method is defined by FormBuilder. In this case, object returns a reference to @user.
  2. We call html_safe on the output string to mark the output as safe. By default, Rails will escape all strings to prevent XSS. Marking a string as safe will prevent this from happening. Hence, it is important to never mark user input as safe.

Now, we can indicate that we are using BootstrapFormBuilder and replace the for-loops in our markup with a single method call:

<!-- app/views/users/new.html.erb -->
<%= form_with model: @user, local: true, builder: BootstrapFormBuilder do |form| %>
  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:name) %>">
    <%= form.label :name, class: "control-label" %>
    <%= form.text_field :name, class: "form-control" %>
    <%= form.errors :name %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:email) %>">
    <%= form.label :email, class: "control-label" %>
    <%= form.email_field :email, class: "form-control" %>
    <%= form.errors :email %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:password) %>">
    <%= form.label :password, class: "control-label" %>
    <%= form.password_field :password, class: "form-control" %>
    <%= form.errors :password %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:password_confirmation) %>">
    <%= form.label :password_confirmation, class: "control-label" %>
    <%= form.password_field :password_confirmation, class: "form-control" %>
    <%= form.errors :password_confirmation %>
  </div>

  <%= form.submit "Sign Up", class: "btn btn-primary" %>
<% end %>

Next, we wish to remove any reference to the control-label CSS class in our markup. We will override the label method to inject this CSS class into the options hash:

# app/helpers/bootstrap_form_builder.rb
class BootstrapFormBuilder < ActionView::Helpers::FormBuilder
  # Below errors and help_block methods

  def label(method, text = nil, options = {}, &block)
    super(method, text, insert_class("control-label", options), &block)
  end

  private

  def insert_class(class_name, options)
    output = options.dup
    output[:class] = ((output[:class] || "") + " #{class_name}").strip
    output
  end
end

With this, we can remove the :class option from the labels:

<!-- app/views/users/new.html.erb -->
<%= form_with model: @user, local: true, builder: BootstrapFormBuilder do |form| %>
  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:name) %>">
    <%= form.label :name %>
    <%= form.text_field :name, class: "form-control" %>
    <%= form.errors :name %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:email) %>">
    <%= form.label :email %>
    <%= form.email_field :email, class: "form-control" %>
    <%= form.errors :email %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:password) %>">
    <%= form.label :password %>
    <%= form.password_field :password, class: "form-control" %>
    <%= form.errors :password %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:password_confirmation) %>">
    <%= form.label :password_confirmation %>
    <%= form.password_field :password_confirmation, class: "form-control" %>
    <%= form.errors :password_confirmation %>
  </div>

  <%= form.submit "Sign Up", class: "btn btn-primary" %>
<% end %>

We can do the same thing for text_field, email_field, and password_field. We will quickly notice that the body of the overridden methods are exactly the same. We can make use of some meta-programming inspired by how Rails implement those methods:

# app/helpers/bootstrap_form_builder.rb
class BootstrapFormBuilder < ActionView::Helpers::FormBuilder
  %w(text_field email_field password_field).each do |selector|
    class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
      def #{selector}(method, options = {})
        super(method, insert_class("form-control", options))
      end
    RUBY_EVAL
  end

  # The rest of the methods
end

Now, we can drop the :class option from the text, email, and password fields:

<!-- app/views/users/new.html.erb -->
<%= form_with model: @user, local: true, builder: BootstrapFormBuilder do |form| %>
  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:name) %>">
    <%= form.label :name %>
    <%= form.text_field :name %>
    <%= form.errors :name %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:email) %>">
    <%= form.label :email %>
    <%= form.email_field :email %>
    <%= form.errors :email %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:password) %>">
    <%= form.label :password %>
    <%= form.password_field :password %>
    <%= form.errors :password %>
  </div>

  <div class="form-group <%= 'has-error' if @user.errors.has_key?(:password_confirmation) %>">
    <%= form.label :password_confirmation %>
    <%= form.password_field :password_confirmation %>
    <%= form.errors :password_confirmation %>
  </div>

  <%= form.submit "Sign Up", class: "btn btn-primary" %>
<% end %>

Finally, we wish to simplify the container div. Let's say this is the desired end result:

<!-- app/views/users/new.html.erb -->
<%= form_with model: @user, local: true, builder: BootstrapFormBuilder do |form| %>
  <%= form.group :name do %>
    <%= form.label :name %>
    <%= form.text_field :name %>
    <%= form.errors :name %>
  <% end %>

  <%= form.group :email do %>
    <%= form.label :email %>
    <%= form.email_field :email %>
    <%= form.errors :email %>
  <% end %>

  <%= form.group :password %>
    <%= form.label :password %>
    <%= form.password_field :password %>
    <%= form.errors :password %>
  <% end %>

  <%= form.group :password_confirmation %>
    <%= form.label :password_confirmation %>
    <%= form.password_field :password_confirmation %>
    <%= form.errors :password_confirmation %>
  <% end %>

  <%= form.submit "Sign Up", class: "btn btn-primary" %>
<% end %>

Similar to how we implemented errors, we need to implement a group method that takes in a selector and a block, and return the markup for the container div and its content. This is how it can be implemented:

# app/helpers/bootstrap_form_builder.rb
class BootstrapFormBuilder < ActionView::Helpers::FormBuilder
  def group(method, &block)
    if object.errors.has_key?(method)
      class_names = "form-group has-error"
    else
      class_names = "form-group"
    end

    content = @template.capture(&block)

    %Q(<div class="#{class_names}">#{content}</div>).html_safe
  end

  # Other methods
end

The capture method is used to capture the results of a template as a string so that we can further inject it into our own template string.

Conclusion

The final code is available at this GitHub repository.

There is one more thing that we can improve. Our markup is still made up of very similar repetition in the following form:

<%= form.group :name do %>
  <%= form.label :name %>
  <%= form.text_field :name %>
  <%= form.errors :name %>
<% end %>

We can easily refactor this into something like

<%= form.text_field_group :name %>

This is left as an exercise to the reader. Similar to text_field, email_field, and password_field, there will be lots of repetitions among text_field_group, email_field_group, and password_field. Hence, meta-programming might also help here.