Friday, June 27, 2008

Refactoring block helpers with more block helpers


This post shows how to build helpers around create_table and form_for, which are my two most commonly used block helpers in Rails. This stuff is obvious to a Rubyist, but not everyone is a Rubyist.

create_table

If you reuse the same columns between tables, your migrations can end up containing a lot of duplicate code. For example, let's say you are making a content management system for your office supply company. Since each product is sufficiently different, you want to make a different ActiveRecord model for each product.

As a result, your migration to add products will look like this:



 1 create_table :tapes do |t|
2 t.string :product_name
3 t.integer :category
4 t.integer :length, :width
5 t.float :price
6 t.integer :total_sold, :total_in_stock, :default => 0
7 t.timestamps
8 end
9
10 create_table :staplers do |t|
11 t.string :product_name
12 t.integer :capacity, :type
13 t.string :color
14 t.float :price
15 t.integer :total_sold, :total_in_stock, :default => 0
16 t.timestamps
17 end


A polymorphic model can be used in this scenario. But what if you have 15 very unique products, and each needs their own indexing strategy? Nonetheless, that is not the point. The problem here is that duplicate columns exist between each table, and this can be refactored.

Simply create a new helper method create_product_table that wraps create_table
:


 1 def self.create_product_table(*args)
2 create_table(*args) do |t|
3 t.string :product_name
4 t.float :price
5 t.integer :total_sold, :total_in_stock, :default => 0
6 t.timestamps
7 yield(t) if block_given?
8 end
9 end


Then use the helper to refactor your existing migration. It removes the duplication, and makes the code more readable:


 1 create_product_table :tapes do |t|
2 t.integer :category
3 t.integer :length, :width
4 end
5
6 create_product_table :staplers do |t|
7 t.integer :capacity, :type
8 t.string :color
9 end



form_for

The solution for form_for is very similar to create_table.

In this arbitrary but typical example, your awesome web 2.0 website supports tagging of just about anything. Comments, Posts, Images, Movies, all taggable. That means all your forms need a "Tags:" input box.

Rather than duplicating the same code in each form for the tagging input, make a taggable_form_for helper:



1 def taggable_form_for(*args)
2 form_for(*args) do |f|
3 yield(f)
4 concat(f.label :tags, 'Tags')
5 concat(f.text_field :tags)
6 end
7 end


The forms are a bit tricky because you must call concat so that the html is emitted in the correct location.

0 comments: