
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 = {})
end
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)
...
concat(
form_tag(
options.delete(:url) || {},
options.delete(:html) || {}
),
proc.binding
)
fields_for(object_name, *(args << options), &proc)
concat('</form>', proc.binding)
end
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 builder.new(object_name, object, self, options, block)
end
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 = {})
private
def objectify_options(options)
end
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) : {}
end
The FormBuilder stores each of its five parameters and stores them. They are:
- @object_name
- This is the old, familiar
object_name
you used to have to pass to every field generator before you used FormBuilders. - @object
- This is the object you are building a form for (in our example it’s the first argument to
form_for
). - @options
- The options passed to
form_for
, more or less. - @proc
- The block provided to
form_for
(remember him?). - @template
- 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 = {})
@template.radio_button(
@object_name, method, tag_value, objectify_options(options)
)
end
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
end
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
...
end
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
end
end
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:
@template.small_caps(some_word)
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.
■