Friday, October 10, 2008

Writting own Form Helper for Rails

The plot of the story is following. Say I've got some simple models, like


Country
- name

City
- name
- country

Customer
- city

A city belongs to a country and a customer belongs to a city. And then on the customer form I would like to have a select box of cities with selection groups by country.

Well, that's not a problem, we can do that.


<%= select_tag "user[city_id]",
option_groups_from_collection_for_select(
Country.all, :cities, :name, :id, :name, @user.city_id
), :id => "user_city_id" %>

Not pretty but works. But then we add another unit

Office
- name
- city

Customer
- office

Now each customer belongs to an office, and we would like to write another drop-down list with groups on the customer's form.

We have already got some not-pretty stuff on the form. So, what you gonna do about that?

Obviously we should wrap this puppy up.
How?
Well, that's the question.

Certainly we can write yet another method in our helpers, and be happy, but I would like to be not just happy, but feel myself kinda sexy as well. So, I would like to write it in such a way that it was look like a standard form-element. Like that.


<%= f.grouped_collection_select :city_id, Country.all, :cities %>
<%= f.grouped_collection_select :office_id, Countries.all, :offices %>

How can we achieve such a beauty?
Simple.
You just need to know some simple things.

First of all, you should know about the existence of the class ActionView::Helpers::FormBuilder. This class is the class which instance you use in the forms building with rails. And that's the class we are going to extend.

The second thing you should know is the existence of the instance variables of the FormBuilder class, which you can use when you are building your own methods

@template - this is a reference to the current template processing object. Each helper method you call in your templates can be called as the variable method inside the FormBuilder class.
@object - a reference to the current model instance which the form covers.
@object_name - the name of the model variable you use to build your form.

The third thing you should know is how to extend classes and modules on fly. Say we create a special separated helper for our new form elements.


module FormHelper
def self.included(base)
ActionView::Helpers::FormBuilder.instance_eval do
include FormBuilderMethods
end
end

module FormBuilderMethods
def grouped_collection_select(method, collection, sub_name,
group_method="name", unit_id="id", unit_name="name")
@template.select_tag "#{@object_name}[#{method}]",
@template.option_groups_from_collection_for_select(
collection, sub_name, group_method, unit_id,
unit_name, @object.send(method)
), :id => "#{@object_name}_#{method}"
end
end
end

We define some module of ours, where new form-methods will be defined and then we include the module inside the FormBuilder class when our helper will get included to the system. And I've added some default values to the method, case my models all have uniform "name" method, so I didn't need to repeat that things over and over.

And voila, this is it. Enjoy!