Form Builders in Rails

November 06, 2008

Update 26 July 2009: for some ready-made form builders that may suit your needs please see my Informant gem.

There is no question that metaprogramming techniques bring enormous power to Ruby on Rails, but in some places that power comes at the cost of code readability. One place in which a maze of hook methods and dynamically-defined methods can at first obscure the behavior of the code is in ActionView’s FormBuilder. Because custom FormBuilders have been invaluable in my work I’d like to give you a guided tour of the source code so you can understand it and be prepared to harness the power of custom FormBuilders in your applications. At the end I’ll also offer some practical tips (feel free to skip to the bottom if you’re in a rush).

A Tour of the Code

Reading the source code is a great way to learn how Rails works, and I recommend keeping the relevant files open in your text editor alongside this article, though it’s not necessary. All the code I discuss is located in actionpack/lib/action_view/helpers/form_helper.rb. I will provide schematic views of the source code where helpful (line numbers taken from Rails 2.1.2). You don’t need to be a metaprogramming expert to read this article, but I do assume that you understand blocks, bindings, and the yield statement.

A FormBuilder’s Life

The first thing to understand is what a FormBuilder is, and really it’s just a proxy for calling methods of the Form*Helper* module. What methods are those? They should look familiar to you:

# File: actionpack/lib/action_view/helpers/form_helper.rb
# Line: 75, in module ActionView::Helpers

module FormHelper
  def form_for(      record_or_name_or_array, *args, &proc)
  def fields_for(    record_or_name_or_array, *args, &block)
  def label(         object_name, method, text = nil, options = {})
  def text_field(    object_name, method, options = {})
  def password_field(object_name, method, options = {})
  def hidden_field(  object_name, method, options = {})
  def file_field(    object_name, method, options = {})
  def text_area(     object_name, method, options = {})
  def check_box(     object_name, method, options = {}, ...)
  def radio_button(  object_name, method, tag_value, options = {})

These are probably some of the first Rails helper methods we all learn. All the field-generating methods (not the two field group generators at the top) take an object_name as the first parameter. When we are building a form for a single object (or “model”), this parameter is likely to be the same for every field, and the most obvious thing FormBuilder does for you is remember this parameter so you only have to specify it once, on initialization.

How is a FormBuilder initialized? Most often it happens by a call to the form_for method, for example, in a view:

<% form_for @car do |f| %>
  <%= f.text_field :name, ... %>
<% end %>

That little f is the FormBuilder object which you use to conveniently generate form fields without specifying the object_name every time. But what exactly happens in this call to form_for? Basically, the code in between <% form_for ... %> and the <% end %> is a block (i.e., anonymous method) that takes one parameter (f) and gets evaluated within form_for but retains its binding. Let’s follow our block into form_for and see what happens to it:

# File: actionpack/lib/action_view/helpers/form_helper.rb
# Line: 232, in module ActionView::Helpers

def form_for(record_or_name_or_array, *args, &proc)
      options.delete(:url)  || {},
      options.delete(:html) || {}
  fields_for(object_name, *(args << options), &proc)
  concat('</form>', proc.binding)

We see the opening HTML form tag rendered via the concat method (note too the binding from our template being preserved). Our block (proc) is then passed to fields_for where the FormBuilder part of this operation occurs:

# File: actionpack/lib/action_view/helpers/form_helper.rb
# Line: 299, in module ActionView::Helpers::FormHelper

def fields_for(record_or_name_or_array, *args, &block)
  builder = options[:builder] || ActionView::Base.default_form_builder
  yield, object, self, options, block)

The first of these two lines is where the class of our FormBuilder is determined. First the options array is consulted to see if :builder is specified, and if it’s not, a default class is used. (Yes, you can change this default to avoid specifying the :builder option on every form—see how in the tips below.)

The second line is the workhorse. It does two main things: it creates a new instance of the chosen FormBuilder class, and it yields to the block. In other words, it passes a new FormBuilder object to the anonymous method we wrote in our view (you know, the one that takes the f parameter and prints some form fields).

Anatomy of a FormBuilder

Now that we know how and where a FormBuilder is created we can look at what it does (or, if we’re writing a custom one, we’re probably interested in what it needs to do). Let’s add some fields to our view:

<% form_for @car do |f| %>
  <%= f.text_field :model %>
  <%= f.text_field :cylinders %>
<% end %>

It should be clear now that these fields are rendered by invoking the text_field method of our FormBuilder object. So a custom FormBuilder has to provide a text_field method, and probably some others like check_box, select, and submit. Here’s what the default FormBuilder defines:

# File: actionpack/lib/action_view/helpers/form_helper.rb
# Line: 694, in module ActionView::Helpers

class FormBuilder

  def initialize(       object_name, object, template, options, proc)
  def fields_for(       record_or_name_or_array, *args, &block)
  def label(            method, text = nil, options = {})
  def submit(           value = "Save changes", options = {})
  def error_message_on( method, prepend_text = "", ...)
  def error_messages(   options = {})

  def text_field(       method, options = {})
  def password_field(   method, options = {})
  def hidden_field(     method, options = {})
  def file_field(       method, options = {})
  def text_area(        method, options = {})
  def check_box(        method, options = {}, ...)
  def radio_button(     method, tag_value, options = {})

  def objectify_options(options)

As you would expect, the seven field-generating methods are the same ones defined in the FormHelper module (above), minus the initial object_name parameter. (If you look at the actual source code you will see that five of these methods—text_field, password_field, hidden_field, file_field, and text_area—are defined dynamically by define_method.) We’ll get to the details of these methods, but first let’s look at perhaps the most useful part of the FormBuilder object: the instance variables it carries. Here’s the default FormBuidler’s initialize method:

# File: actionpack/lib/action_view/helpers/form_helper.rb
# Line: 701, in class ActionView::Helpers::FormBuilder

def initialize(object_name, object, template, options, proc)
  @object_name, @object, @template, @options, @proc = 
    object_name, object, template, options, proc
  @default_options = @options ? @options.slice(:index) : {}

The FormBuilder stores each of its five parameters and stores them. They are:

This is the old, familiar object_name you used to have to pass to every field generator before you used FormBuilders.
This is the object you are building a form for (in our example it’s the first argument to form_for).
The options passed to form_for, more or less.
The block provided to form_for (remember him?).
The current view (an instance of ActionView::Base). This object provides all methods available in your view. Coupled with concat (for output) and @proc (which provides the binding from your view) you can call any of these methods as if you were writing code in your ERb template.

Let’s take a quick look at how the Rails-supplied FormBuilder uses these variables to implement a field-rendering method:

# File: actionpack/lib/action_view/helpers/form_helper.rb
# Line: 740, in class ActionView::Helpers::FormBuilder

def radio_button(method, tag_value, options = {})
    @object_name, method, tag_value, objectify_options(options)

This method invokes the radio_button method of the template—the exact same radio_button method you call when you’re creating a form without a FormBuilder. The first argument is the @object_name which is stored in the FormBuilder object, followed by the method, tag_value, and options. The options are first passed through objectify_options which simply merges in some defaults stored in the FormBuilder.

One final thing to note is the last four lines of the form_helper.rb file:

# File: actionpack/lib/action_view/helpers/form_helper.rb
# Line: 764, in module ActionView

class Base
  cattr_accessor :default_form_builder
  self.default_form_builder = ::ActionView::Helpers::FormBuilder

These lines re-open the ActionView::Base class to allow access to and set a default value for the default_form_builder attribute. I mentioned above that we can call this method to set an application-wide default FormBuilder class, but it’s important to notice that the default is set here, when Helper modules are included, not at boot time. You may be tempted to call ActionView::Base.default_form_builder in your config/environment.rb file, but this won’t work. Instead, call it in one of your helpers, after the included Rails helpers are loaded.

Some Practical Tips

So far this article has been a discussion of how Rails’ FormBuilder-related code works. If you want to do anything creative with FormBuilders, an understanding of how they work will help you greatly. However, you may just need to get something done, so in this section I will give you some quick pointers, sans explanation, for writing and using custom FormBuilders.

1. Inherit From the Included FormBuilder

You usually want to define your custom builder in app/helpers/application_helper.rb. When you do so, you should make sure it inherits from the Rails-included base class, which will give you a good starting place:

class YourFormBuilder < ActionView::Helpers::FormBuilder

Note that you should not place your class inside the ApplicationHelper module but at the end of the file.

2. Set an Application-Wide Default FormBuilder

In app/helpers/application_helper.rb call:

ActionView::Base.default_form_builder = YourFormBuilder

This saves you from having to pass :builder => YourCustomFormBuilder in the options hash on every call to form_for. You can still override this (with the :builder option) where necessary.

3. Define Methods Dynamically in Bulk

You can get an array of all standard field helper methods by calling field_helpers. You can add to/remove from this list:

helpers = field_helpers + 
  %w(time_zone_select date_select) - 
  %w(hidden_field fields_for label)

and then loop through all methods (field types) you want to customize, re-defining each dynamically by using define_method, like so:

helpers.each do |helper|
  define_method helper do |field, *args|
    options = args.detect{ |a| a.is_a?(Hash) } || {}
    # decorate your fields here

4. Call Helper Methods (Just As You Would In An ERb Template)

If you can use a helper in a view, you can use it in your FormBuilder. This includes your custom helpers. Let’s say you define a small_caps method in your ApplicationHelper. To call it in your FormBuilder, just do something like this:


5. Specify Field Layout In Partials

Just as you can do something like this in your ERb template:

<%= render :partial => "text_field",
           :locals  => {:label => "Name"} %>

so too can you do it in your FormBuilder by using the @template object:

@template.render :partial => "text_field",
                 :locals  => {:label => "Name"}

This allows you to specify the appearance of forms (along with their labels, error messages, and various decorations) in ERb templates, which many people prefer to plain Ruby code.

Now, Go and Build Them

I hope I’ve given you enough information to get started writing your own FormBuilder class. I really believe that FormBuilders are one of the keys to writing clean, reliable web applications and I’m sure they aren’t used as frequently as they should be due to lack of documentation.

Thanks for reading, and good luck.

comments powered by Disqus