Friday, March 30, 2007

(Almost) Overriding helpers in rails

In a project I am currently working on I use the select_datetime() helper to provide me with year + month + day + hour + minute drop down boxes. After using the UI for a while the customer mentioned that they do not require the full minute listing (from 00 to 59 minutes) for the minute drop down box and would prefer to only have 15 minute increments to shorten the drop down and waste less scrolling time when choosing the minutes.

Umm ... OK!

This type of functionality can be provided in a few ways (that I
know of ):
  • Wrapper: Create your own helper wrapper
  • Replace: Replace the original helper method in the module where it comes from
  • RTFM: Read the docs first and then code
select_datetime() is actually a composition of two other helpers:
select_time() is another composition of:
Because we're simply interested in changing the behaviour of the minutes we will only be changing select_minute().

This approach is pretty straight forward. All you need to do is add your own helper as a wrapper for the helper you wish to masquerade. Unfortunately we cannot use this technique here because the helper we're using (select_datetime()) cannot provide us with the relevant functionality we want by simply massaging the helper's input or output.


Your intuition may be itching at this point and that annoying inner voice you never ignore could be saying:

'Why don't you just simply override the select_datetime() in application_helper.rb or ActionView::Base class, lilly-livered limp-wristed sissy!'.

In this case your inner voice needs to be muzzled. Helpers are included as mixins. This means the methods in the mixed in module become available to the classes using them as if they were part of the actual class.

The PickAxe book says it succinctly:

'In fact, mixed-in modules effectively behave as superclasses.'

Unfortunately this also means that if you change the methods in a module they will change for all classes that use the mixed-in module. If you want to change the helper behaviour you need to change the behaviour in the module that is providing the helper as a mixin, in this case ActionView::Helpers::DateHelper.

Off we go and open ActionView::Helpers::DateHelper to start mucking with select_minute() so that we can support periods as the customer requested. Here is the relevant code:
# File vendor/rails/actionpack/lib/action_view/helpers/date_helper.rb, line 190

190: def select_minute(datetime, options = {})
191: val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.min) : ''
192: if options[:use_hidden]
193: hidden_html(options[:field_name] || 'minute', val, options)
194: else
195: minute_options = []
196: 0.step(59, options[:minute_step] || 1) do |minute|
197: minute_options << ((val == minute) ? 198: %(#{leading_zero_on_single_digits(minute)}\n) : 199: %(#{leading_zero_on_single_digits(minute)}\n) 200: ) 201: end 202: select_html(options[:field_name] || 'minute', minute_options, options) 203: end 204: end

Alrighty! Let's add that stepping code to provide the periodic minutes ...

Damn! Had I spent a little time drilling down through the documentation I would have noticed that the select_minute() helper already provides the required functionality via its :minute_step option.

Here is what the documentation says about this helper:
select_minute(datetime, options = {})

Returns a select tag with options for each of the minutes 0 through 59 with the
current minute selected. Also can return a select tag with options by minute_step
from 0 through 59 with the 00 minute selected The minute can also be substituted
for a minute number. Override the field name using the :field_name option, 'minute'
by default.

So, all of this may seem a little pointless but the lesson here is check the docs before you take-off at light speed. I look forward to writing an actual article on overriding helpers in rails soon.

No comments:

About Me

My photo
I love solving real-world problems with code and systems (web apps, distributed systems and all the bits and pieces in-between).