Saturday, September 25, 2010

Custom Form Builders in Rails...

I've started working on a new Rails application and needed to display chunks of spreadsheet-like text for financial data (e.g., Profit & Loss Statement and Balance Sheet). Everything went fine (well, not so fine: money fields were a bit of challenge, but more about that later) until I started displaying my data in a view. As you can imagine, there are a lot of cells for these displays and after some prototyping, I realized that the default size of 30 for an input field was just too big. Of course, that's easy to solve by just adding ":size => 15" to the text_field definition, but the prospect of adding that to numerous fields and than probably having to change them all left me pretty grumpy.  That's when I figured that a Custom Form Builder would be the way to go.

As usual, I headed off to the Rails API but found little there and much of the external documentation on how to use them was either way over-complicated for what I wanted to do, or going in another direction.  I did find a few good resources, including the following:

1. Ryan Bates' "Mastering Rails Forms" episode 3 - Great presentation; worth every penny of the $5 cost per episode.
2. APIDoc description of form_for method - APIDoc is your very good friend.
3. Alex Reisner's "Anatomy of a Form Builder" - Best introduction of the details behind Form Builder I found.
4. Ruby Pond's Semantic Form Builder - Lots of complex changes, nicely done
5. Aldenta post with metaprogramming example - Shows how to replace a slew of methods with metaprogramming; compare to Ruby Pond example where they are all hand-crafted.

Because of their often complicated solutions and the fact that there were different ways of doing things, I found it challenging to extract the minimum that I would have to do make a simple change. In the end, after much reading and trial coding, Ryan Bates' "Mastering Rails Forms, episode 3" completed my current understanding of the process.

Here are the key points that I learned which finally helped me get through the morass and make it work for me. In the discussion that follows, CustomFormBuilder is my class name.

To start, this is the code that I wanted to change...
from this:
<td>f.textfield :name, :size => 12</td>
to this:
f.textfield :name
Not a big change at all, but with many lines like this, I thought this would be a nice, easy way to learn about custom form builders.

Here's what I learned that helped me understand how it all works.

1) the basic form builder code looks like this:
class CustomFormBuilder < ActionView::Helpers::FormBuilder
 ...your code...
There may be other approaches out there that will work, but this is the most straight-forward way I have found.

You want to be able to reference your custom form builder easily, as in:
form_for :person, :builder => CustomFormBuilder do |f|
...form code...
It turns out there are ways to customize even this, so that your code looks like this:
custom_form_for :person do |f|
...form code which automatically uses your CustomFormBuilder
but that's for a little further down the page.

2) There is more than one way to implement the logistics of a custom form builder (reminds me of Perl's TMTOWTDI and I am rapidly coming to the conclusion that this applies to Rails everywhere). This was one of the things that tripped me up at first, since different posts give different instructions on how to create your builder. By logistics I mean where do you put the code that will be your form builder so that you can reference it easily?
  1. You can put it in a helper file... either in application_helper.rb or controller_helper.rb. The basic code layout is as described above... The really important thing about including this code in an existing helper file is that the code MUST come after the end statement that terminates the helper code! Failing to do this will result in your form builder not being found (this one cost me a few hours) or your having to reference it as:
    :builder => ApplicationHelper::CustomFormBuilder
  2. You can put it in a separate file in the helpers directory, with a name that matches the class name, i.e., custom_form_builder.rb
  3. You can put it anywhere you like (for example, like Ryan Bates does, in a separate directory that you create called app/form_builders). In this case, however, you must add that directory to Rails' default search path by adding the following to your environment.rb file (just uncomment and modify the skeleton statements that's already there):
    config.load_paths += %W( #{RAILS_ROOT}/app/form_builders)
3) You can make your CustomFormBuilder the default by adding the following to the bottom of your environment.rb file. Doing this means that you can leave off the :builder => option in your form_for:
ActionView::Base.default_form_builder = CustomFormBuilder
  • However, I think I agree with Ryan Bates that this is not such a great idea -- it hides what's going on and could make it difficult for some one new to your code to figure it out.
  • [Note]: Other authors recommend doing this in app/helpers/application_helper.rb and one author explicitly stated that the environment.rb approach would not work because helpers don't get initialized until they are actually called/used {could this be a version-dependent issue?]
4) Altlernatively, you can make a helper method that encapsulates the CustomFormBuilder and makes it explicit that you've got some extra magic going on. Do this in your Application Helper file:
def custom_form_for(*args, &block)
  options = args.extract_options!
  options.merge!(:builder => CustomFormBuilder)
  form_for(*(args + [options]), &block)
5) The code to actually do this very simple change is also very simple:
def text_field(name, options = {})
  options.reverse_merge! (:size => 12)
  @template.content_tag(:td, super)
  • The "reverse_merge" is so that I can override the default size of 12 with another value coded in the form
6) You'll see in some of the examples that the method signature is given with *args instead of option as I have shown it. That's a more general approach, but requires that you know a little more about how Rails can manipulate the arguments; see below. Suffice it to say that it works for my simple example, but if you're going to get serious about making more complicated customm form builders, you should get familiar with the following approaches.

extract_options! is a Rails method that will -- not surprisingly -- pull the options hash out of the method arguments. Very useful for the general method signature which uses *args.

merge! and reverse_merge! are methods that will merge and overwrite the receiving hash with the results. So, for example:
options.merge!(:size => 12)
will add :size => 12 to the options hash, while:
options.reverse_merge!(:size => 12)
will add :size => 12 to the options hash ONLY if there is no :size element already there.
  • Note the bang (!) on each of these methods -- very important. If you use the non-bang version, you won't update the options hash and things just won't work.

content_tag is a method on the @template object (provided to you by the FormBuilder) that wraps selected text in the given HTML tag. Very useful for building your output string.

There's lots more, but that should be enough to get you started -- hopefully a little easier than I did. Now that I understand some of these basics, looking at the more complicated examples is much easier for me.

1 comment:

  1. Hey,

    thank you for your very helpful explanation on how to customize the form building process.

    I noticed a small mistake at point 4), where you forgot to add the exclamation mark to the merge method. It should be:

    options.merge!(:builder => CustomFormBuilder)