Custom FormBuilder - automatically adding obligatory field marks

Author: Rafael - Publish date: Sun, 08 Feb 2009 14:39:01 -0300 Feb 8

Hi,

Rails form_for, and its associated FormBuilder, are very nice tools, they helps us create web forms without effort and easy to maintain, but they can be much better.

The basic problem with the default is that we can’t change the template used to create the fields, so we can’t easily add common features like automatic labels, obligatory field markings or even customize the display of errors.

To do so we need to create a custom FormBuilder and pass it to the form_for command. This is a quite common technique, mentioned in the Rails api documentation, but I’m writign this post to answer a question posted on RailsFrance.

The first thing we should know is that the “form_for” command uses the class FormBuilder to create the actual HTML output, and it has a optional parameter which we can use to inform it to use a different builder, like this:

<% form_for(@post, :builder => CustomBuilder) do |f| %>

And that’s what we’re going to do. We’ll add the following features to the custom FormBuilder:

  1. Create labels
  2. Add obligatory field markings
  3. Custom error template

Creating the CustomFormBuilder

We’ll use the default FormBuilder as the base for the custom one, redefining just the methods we need. For this example I’ll put the code into the “lib” directory, but you can change it to a plugin if you want. This is the final code for the form builder:

module Lib
  class CustomFormBuilder < ActionView::Helpers::FormBuilder
    helpers = field_helpers +
      %w(date_select datetime_select time_select collection_select) +
      %w(collection_select select country_select time_zone_select) -
      %w(hidden_field label fields_for)
    
    helpers.each do |name|
      define_method name do |field, *args|
        options = args.detect {|argument| argument.is_a?(Hash)} || {}
        build_shell(name, field, options) do
          super
        end
      end
    end

    def build_shell(method_name, field, options)
      @template.capture do
        locals = {
          :element => yield,
          :label   => translated_label(field, options[:label]),
          :required => object_class.required_fields.include?(field) || options[:required]
        }

        if has_errors_on?(field)
          locals.merge!(:error => error_message(field, options))
          @template.render :partial => 'lib/form/field_with_errors',
                           :locals => locals
        else
          locals.merge!(:error => nil)
          @template.render :partial => 'lib/form/field',
                           :locals => locals
        end
      end
    end
    
    private
      def object_class
        if @object.nil?
          @object_name.to_s.classify.constantize
        else
          @object.class
        end
      end
      
      def error_message(field, options)
        if has_errors_on?(field)
          errors = object.errors.on(field)
          errors.is_a?(Array) ? errors.to_sentence : errors
        else
          ''
        end
      end
      
      def has_errors_on?(field)
        !(object.nil? || object.errors.on(field).blank?)
      end

      def translated_label(field, label_options)
        result = object_class.human_attribute_name field.to_s
        result = label(field, label_options) if result.nil?
        result
      end
  end
end

This code is not what I’d call trivial, but after some Ruby you can get it easily. This code was inspired by a friend of mine, Yvon Cognard, which thought me a lot of Ruby, and by recipe 15 from Advanced Ruby Recipes.

In the first part we create a variable called helpers containing all the methods we should rewrite based on the base class field_helpers property, and excluding the ones we shouldn’t touch, like hidden_field.

After that we use some meta-programming magic to redefine these methods, encapsulating the old call into the new build_shell method, where the actual magic takes place. There we create a locals hash containing the information we need to use in the templates.

The yield in the :element key is where result of the base method will be put, the :label will have the humanized text for the field, that might be translated, and the :required will have a boolean value that indicates whether or not we should add the required field marking.

Note that the actual HTML code isn’t coded anywhere in here, we’ll delegate this part to two different views: “_field.html.erb” and “_field_with_erros.html.erb”, that should be stored in the directory “app/views/lib/form/”. This way we can customize the template without changing any code, making it easier to maintain and less prone to errors. Here is some example of how these templates could be:

#app/views/lib/form/_field.html.erb
<p>
  <span class="field_label">
    <%= label %><%= "<em>*</em>" if required %>
  </span>
  <span class="form_field">
     <%= element %>
  </span>
</p>


#app/views/lib/form/_field_with_errors.html.erb
<p>
  <span class="field_label">
    <%= label %><%= "<em>*</em>" if required %>
  </span>
  <span class="form_field">
     <%= element %>
     <span class="form_error_message">
       <%= error %>
     </span>
  </span>
</p>

Finding out which properties are required

You’ll notice that we’re calling a required_fields property from the ActiveRecord class to find out when we should pass the required variable as true to the view, but it doesn’t exist on the Rails core. In fact, there’s no easy way to get the information about which properties are required or not, so we need to create our own. The following code can be put just bellow the CustomFormBuilder, just make sure to put it outside the module.

module ActiveRecord
  class Base
    def self.validates_presence_of *args
      @@required_fields ||= Array.new
      @@required_fields |= args.select { |e| e.class == Symbol }
      super(*args)
    end
    
    def self.required_fields
      @@required_fields ||= Array.new
    end
  end
end

This piece of code adds a class variable to the ActiveRecord::Base class called @@required_fields which is filled up when the class methods validates_presence_of is called upon, but in order to do this we need to override the default method. This kind of change on the base classes is possible thanks to the principle of open classes over which Ruby is build, and this is one of the most important reasons why Ruby is so flexible and Rails is possible. This technique is called monkeypatching, but I’m not a big fan of this title, I’d prefer to call it rubypatching, even though this isn’t a exclusive Ruby feature, but it sounds nicer :)

Making it work

I’m having some problems initializing the code I put into the lib directory, I’m not sure if I’m doing something wrong or not, but the solution is very simple, just create a new file into “config/initializers/” called lib_init.rb and add the following code:

#config/initializers/lib_init.rb
require 'custom_form_builder'

Now, in the form view we can just pass the builder and see its magic:

#app/views/posts/new.html.erb
<% form_for(@post, :builder => Lib::CustomFormBuilder) do |f| %>
  <%= f.error_messages %>
  <%= f.hidden_field :secret_id %>
  <%= f.text_field :published_at %>
  <%= f.text_field :title %>
  <%= f.text_area :body %>

    <%= f.submit "Create" %>
  </p>
<% end %>

This code will create all the humanized labels, including the available translations, the fields and will not change the way a hidden_field is created. The code is cleaner and to change the way it’s generated we just need to change the partials. There’s only one thing we can do to make it easier: create a helper so we don’t need to explicitly pass the :builder parameter:

#app/helpers/application_helper.rb
  def custom_form_for(record_or_name_or_array, *args, &proc)
    options = args.extract_options!
    form_for(record_or_name_or_array, *(args << options.merge(:builder => Lib::CustomFormBuilder)), &proc)
  end

Now, our view code can be simplified to:

#app/views/posts/new.html.erb
<% custom_form_for(@post) do |f| %>
.....

That’s it. We can extend this code to extract and pass more information to the view, but I really don’t recommend expanding the code more than necessary, always remember: YAGNI :)

There are other custom FormBuilders out there, one that seems to be quite interesting is Justin French’s Formtastic, but I didn’t have time to look it up very carefully.

I hope this code helps somebody. Stay tuned for more Rails code soon.

Cheers,
Rafael.

Comments: 2 Sections: rails | Tags: form, rails22

Comments

  1. Alex G Wed, 15 Apr 2009 15:59:35 -0300

    good stuff, grabbed a few pointers from this.

    you can use this instead of the custom_form_for

    ActionView::Base.default_form_builder = Lib::CustomFormBuilder

    drop it in environment.rb

  2. Rafael Rosa Wed, 15 Apr 2009 15:59:35 -0300

    Hi,

    Thanks for your comment, for the tip and for your syntax hightlighter, it’s awesome :)

    Cheers

Add a commentary

Author*
E-mail*
Website
Commentary*