Last Updated: February 25, 2016
·
4.64K
· bodacious

A Custom Rails FormBuilder for Twitter Bootstrap 3

Include the following module in your helpers directory

# app/helpers/forms_helper.rb
module FormsHelper

  # A custom FormBuilder class for Rails forms with Twitter Bootstrap 3
  class CustomFormBuilder < ActionView::Helpers::FormBuilder

    # ActionView::Helpers::FormHelper and ActionView::Helpers::FormBuilder
    # methods each have different args. This hash stores the names of the args
    # and the methods that have those args in order to DRY up the method aliases
    # defined below.
    METHOD_NAMES_FOR_ARG_SETS = {

      'method,options={}' => %w{
        date_field datetime_field datetime_local_field email_field file_field
         hidden_field number_field password_field phone_field range_field
        search_field telephone_field text_area text_field time_field url_field
      },

      # Removing checkboxes for now - the error field causes alignment issues
      # "method,options={},checked_val='1',unchecked_val='0'" => %w{ check_box },

      "method,collection,value_method,text_method,options={},html_options={},&block" => %w{
        collection_check_boxes collection_radio_buttons
      },

      "method,collection,value_method,text_method,options={},html_options={}" => %w{
        collection_select
      },

      "method,options={},html_options={}" => %w{date_select datetime_select time_select},

      "method,tag_value,options={}" => %w{ radio_button },

      "method,choices,options={},html_options={}" => %w{ select }
    }

    for args_set, method_names in METHOD_NAMES_FOR_ARG_SETS

      html_class_in = args_set.index("html_options") ? 'html_options' : 'options'
      args_without_defaults = args_set.gsub(/\=[^,]+/, '')
      for method_name in method_names

        method_str =  <<-RUBY
          def #{method_name}_with_error_field(#{args_set})
            update_options_with_form_control_class! #{html_class_in}
            content = #{method_name}_without_error_field(#{args_without_defaults})
            append_error_field_to_content!(content, method, options)
          end
        RUBY

        # Subtracting 2 from the __LINE__ var for accurate error messages
        class_eval(method_str, __FILE__, __LINE__-2)
        alias_method_chain method_name, :error_field
      end

    end

    # Creates a div wrapper with class 'form-group'
    def group(options ={}, &block)
      @template.form_group(options, &block)
    end

    # Shows the error messages for :base if any
    def error_messages
      @template.error_messages_for(object)
    end

    # Creates a div wrapepr with class 'input-group'
    def input_group(options={}, &block)
      @template.input_group(options, &block)
    end

    # Creates a fieldset tag
    def fieldset(&block)
      @template.form_fieldset(&block)
    end

    # Creates a legend tag
    def legend(content, options ={})
      @template.form_legend(content, options)
    end

    # Creates a div with class 'text-error' for displaying error messages
    # on specific fields
    def error_field(attribute, options = {})
      return unless object.errors[attribute]
      update_options_with_class!(options, 'text-error')
      content = object.errors[attribute].first
      @template.content_tag(:div, content, options)
    end

    # Creates a small tag with class 'help-block' for displaying helpful
    # text under a form input
    def help_text(method, content, options = {})
      return if object.errors[method].any?
      update_options_with_class!(options, 'help-block')
      @template.content_tag(:small, content, options)
    end

    # Creates a div wrapper with class 'checkbox'
    def checkbox(&block)
      @template.form_checkbox(&block)
    end

    # Creates a submit button with class 'btn btn-primary'
    def submit(name, options ={})
      update_options_with_class!(options, 'btn btn-primary')
      super(name, options) << " " # add a space to the end
    end

    # Creates a cancel button to go back to a previous page.
    def cancel(path, edit_path = nil, options ={})
      options[:class] ||= ''
      options[:class] << "btn btn-default"
      options[:data] = { confirm: "Cancel without saving?"}

      if edit_path && object.persisted?
        path = edit_path
      end
      @template.link_to('cancel', path, options)
    end

    private

    # Add the error_field div to each field by default
    def append_error_field_to_content!(content, method, options)
      unless object.errors[method].any? || options[:error_field] == false
        content << error_field(method)
      end
      content
    end

    # Update an options hash with class 'form-control'
    def update_options_with_form_control_class!(options)
      update_options_with_class!(options, 'form-control')
    end

    # Update an options hash with a :class
    def update_options_with_class!(options, klass)
      @template.update_options_with_class!(options, klass)
    end

  end

  # Use this instead of form_for
  def custom_form(record, options = {}, &block)
    options.update(builder: CustomFormBuilder)
    form_for(record, options, &block)
  end

  # Creates a div wrapper with class 'input-group'
  def input_group(options={}, &block)
    content = capture(&block)
    update_options_with_class!(options, 'input-group')
    content_tag(:div, content, options)
  end

  # Creates a div wrapper with class 'form-group'
  def form_group(options ={}, &block)
    content = capture(&block)
    update_options_with_class!(options, 'form-group')
    content_tag(:div, content, options)
  end

  # Creates a fieldset with class from current_namespace (used internally in our project)
  def form_fieldset(&block)
    klass = "#{current_namespace}-fieldset"
    content_tag(:fieldset, capture(&block), class: klass)
  end

  # Creates a legend tag
  def form_legend(content, options ={})
    content_tag(:legend, content, options)
  end

  # Creates a div wrapper with class 'input-group'
  def form_input_group(&block)
    content_tag(:div, capture(&block), class: "input-group")
  end

  # Creates a div wrapper with class 'checkbox'
  def form_checkbox(&block)
    content_tag(:div, capture(&block), class: "checkbox")
  end

  # Show the error messages on :base for a record
  def error_messages_for(object)
    if object.errors[:base].any?
      message = object.errors[:base].first    
      %{<div class="text-danger">#{message}</div>}.html_safe
    end
  end

  # Update an options hash with a given :class value
  def update_options_with_class!(options, klass)
    options[:class] ||= ''
    options[:class] << " #{klass}"
    options
  end

end

This will give you much cleaner, DRYer forms that come pre-equiped with twitter bootstrap
3 classes.

Here's an example:

<%= custom_form(@user) do |f| %>

  <%= f.error_messages %>

  <%= f.fieldset do %>

    <%= f.legend("Your contact info") %>

    <%= f.group do %>

      <%= f.label :email %>
      <%= f.email_field :email %>

    <% end %>

    <%= f.group do %>

      <%= f.label :phone_number %>
      <%= f.phone_field :phone_number %>

    <% end %>

    <%= f.checkbox do %>

      <%= f.check_box :may_contact %>
      <%= f.label :may_contact, 'You may contact me' %>

    <% end %>

  <% end %>

  <%= f.submit("Save") %>
  <!-- Goes to root if it's a new record, otherwise goes to /my_profile -->
  <%= f.cancel root_path, my_profile_path %>

<% end %>

3 Responses
Add your response

Hi really helpful, I was thinking the same thing when I found you're code. I've just done a very similar version for Rails 3 and with fields_for.

I've made a gist so we can work on it as we move forward, here's the link :

https://gist.github.com/giacomomacri/7486617

Bye

over 1 year ago ·

Where is the definition for @template.update_options_with_class!? Google search comes up empty and searching the rails repository on github finds no matches.

Update: Nevermind. I found it at the bottom of this example. Got stuck thinking it was a Rails method.

over 1 year ago ·
over 1 year ago ·