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!

6 comments:

Thiagarajan said...

Interesting article.

I want to do something in a similar sense. I want to extend the text_field tag and make something like a required_text_field to show a little red star on to the right to indicate that it is a required field.

How would I go about doing that? Any hep would be appreciated.

Thanks!

Nikolay V. Nemshilov aka St. said...

You can define a method in the FormBuilderMethods module similar to the described one. Something like

def required_text_field(*args)
text_field(*args) + "<img src="star"/>"
end

george.anderson said...

Forgive my ignorance, but where does this code go and does it matter what the filename is?

I've tried lib/ and config/initializers but no luck.

Thanks,

/g

Nikolay V. Nemshilov aka St. said...

2 George

We are writing a helper module in the article. And it's named 'FormHelper', and therefore it should be saved in a file like app/helpers/form_helper.rb

george.anderson said...

Ah. I suppose I was making it more difficult than it was. Thanks.

/g

Anarrin said...

Thanks for the post, I was just contemplating how to extend FormHelper. The only issue I have is that I need to actually _overwrite_ one of the current form_tag_helper methods (token_tag).

Any suggestions on how to accomplish that? In adding method by the same name, I see that the original method is still called.