Friday, July 17, 2009

Customizing The Rails Forms Builder

The word "customer", usually means someone who come and say "I wanna custom stuff". And because it is their nature they do it all the time. Sometimes several times a day.

I'm skipping here the obvious question "why would I need to customize a forms builder?" and get straight to the point.

So you need to customize your forms. Big time. And if the first what comes on your mind are helper methods and partial templates, this article is for you.

And please don't think about monkey patching rails form-builder. Don't behave like a php developer thinking he can use rails. Lord save their poor souls.

There are better way of doing that.

First of all you should realize that in 99.99% of cases you should not change the default rails templates. Meaningly that

<% form_for(@model) do |f| -%>
<%= f.error_messages %>
<p>
<%= f.label :name %>
<%= f.text_field :name %>
</p>
<% end -%>

Despite that it is a simple structure, with a little bit of imagination and css magic you can make it look like anything you need.

Secondly you really should pay a dollar of penalty every time you think about monkey patching. Ruby is not just a monkey patching language it is object oriented too, and rails form builder is a quite well organized structure.

Instead of patching, we will create our own form-builder using advantages of the inheritance and then swap the default builder from the built-in to our own one. This way we can change the logic of the forms building transparently for the templates, you even can have several form-builders which behave differently and swap them depend on the context.

The basic example would look like this. You create another helper module called FormsHelper and save it along with the other helpers in your application

module FormsHelper
def self.included(base)
ActionView::Base.default_form_builder = CustomFormBuilder
end

class CustomFormBuilder < ActionView::Helpers::FormBuilder
# your custom methods go here
end
end

Inside the class you will have access to the form builder context with all its built in methods and variables. For the beginning you should know about the following ones

  • @template - the template context, you call this variable if you need to call the basic helpers

  • @object - the current model of the form

  • @object_name - the string name under which the form knows the model


Okay now lets play with the thing a little. Say the customer says "don't like the <h2> header on the error reports, the one with the number of errors on the form". No problem, we just override the built in method

def error_messages(options={})
super options.reverse_merge(:header_message => nil)
end

Then say you as many other developers usually put your forms in a partial and then depends on the model state change the submit button caption. something like this

form_for(@model) do |f|
f.submit @model.new_record? ? "Create" : "Save"

This is a little bit annoying. Say I'd like just call it <:%= f.submit %> and want the form automatically set the caption. Here is the code

def submit(caption=nil, options={})
super(caption || @object.new_record? ? "Create" : "Save"), options
end

Then I got completely lazy and started to want my form builder to build a block of label + text-field in a single call, like this

form_for(@model) do |f|
f.labeled_text_field :name

# this should build the thing like that
<p>
<%= f.label :name %>
<%= f.text_field :name %>
</p>

# you could do it like this
def labeled_text_field(name, options={})
@template.content_tag(:p, label(name) + text_field(name, options))
end

Then, all the sudden, your customer comes back and says a scary thing "I like this, but I want for this particular controller, there actually was the h2 header saying 'Oh noooo!'. Yes, just for this special case".

No, my friend we are not going to the monkey patchers hell! We just define another form-builder over our own builder, and automatically swap it in the particular controller

class SpecialFormBuilder < CustomFormBuilder
def error_messages(options={})
super options.reverse_merge(:header_message => "Oh noooo!")
end
end

class ParticularController < ApplicationController
before_filter :swap_form_builder

def swap_form_builder
ActionView::Base.default_form_builder = SpecialFormBuilder
end
end

Think you understand the idea. If you do the things in a serious way, lord will love you and grand you a big deal of flexibility and rapidness.

This is pretty much it. Have a good one!

1 comment:

Greger Olsson said...

Really good post! Strange noone commented before. I will use this technique to DRY up our views, and also integrate automatic I18n of labels.
Thanks!