Blog

Setting Rails Date Helpers Via Javascript

Posted: Mar 11, 2008 by Billy Gray Tagged rails

This took enough of my time that I think it’s worth a blog post. In Tempo you’ll see a familiar paradigm in the time reporting interface (the main screen): a list of editable items in a row, each with the same set of controls. They are editable via AJAX calls, so you can open a number of them for editing at once.

Now, when you’re looking to add javascript observers to these elements (to do automated things like type ahead, etc), you have to assign them unique id attributes, usually based on the object id. While it’s easy to add an :id attribute to any of the usual tag helpers in Rails, it doesn’t work like this with date_select:

%table
  %tr.s1
    %td{:colspan => '2'}= project_select(f, @current_user.projects, entry)
    %td{}
      = f.date_select :occurred_on, :order => [:month, :day, :year], :start_year => 2007, :use_short_month => true, :use_short_year => true, :id => "#{entry.id}"
      = popup_calendar("entry_#{entry.id}_occurred_on", entry.occurred_on)

Our javascript calendar is expecting a unique ID on the date select drop downs so that it can set their values. But, that’s not the case, the id of each drop down is generated automatically from the name attribute, thus:

<select id="entry_occurred_on_2i" name="entry[occurred_on(2i)]">
<option value="1">Jan</option>
<option value="2">Feb</option>
<option value="3" selected="selected">Mar</option>
...
</select>

Makes sense, really, since the separate drop downs are being generated to be re-assembled when posted, and what else to id them?

The trick to getting unique id’s into these elements was a monkey patch I put in config/initializers/date_helper.rb:

module ActionView
  module Helpers
    module DateHelper
      def name_and_id_from_options(options, type)
        options[:name] = (options[:prefix] || DEFAULT_PREFIX) + (options[:discard_type] ? '' : "[#{type}]")
        name = options[:name].gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '')
        unless options[:id].nil?
          options[:id] = name.sub(/_/, "_#{options[:id]}_")
        else
          options[:id] = name
        end
      end
    end
  end
end

Pretty close to the original, it preserves the original behavior, but respects your inclusion of the :id attribute in the options you pass to date_select. Now our id’s look like:

<select id="entry_2013_occurred_on_3i" name="entry[occurred_on(3i)]">

We found a similar problem with the auto_complete plugin – doesn’t work when there are more than one active on the screen at once, due to non-unique id’s. That required a bit more work. First a monkey-patch in config/initializers/auto_complete_macros_helper.rb:

module AutoCompleteMacrosHelper
  def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {}, object_id = nil)
    if object_id.nil?
      field_id = "#{object}_#{method}"
    else
      field_id = "#{object}_#{object_id}_#{method}"
      tag_options[:id] = field_id
    end
      
    (completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
    text_field(object, method, tag_options) +
    content_tag("div", "", :id => "#{field_id}_auto_complete", :class => "auto_complete") +
    auto_complete_field(field_id, { :url => { :action => "auto_complete_for_#{object}_#{method}" } }.update(completion_options))
  end
end

Adding an optional parameter on there seemed like the easiest thing to do for the moment, creates for only a slight change in our form:

= text_field_with_auto_complete :entry, :tag_s, {:size => 40}, { :indicator => "entry_#{entry.id}_tag_s_form_loader", :frequency => 0.4, :tokens => ' ' }, entry.id

I’m tempted to make it work off of whatever shows up in tag_options[:id] but this will do for now.


Add a comment