Compound Attributes and fields_for in Rails

I had a Rails application (3.0.3) where I wanted to take a date-time and break it out into date, time, and a modal select. Modes were things like ‘Now’, ‘In an hour’, ‘Tomorrow’ etc. Certain modes obviated the need of the date/time fields entirely.

I initially made some progress by defining virtual model attributes to return the appropriate date or time. I ran into trouble when it came time to figure out submitting those attributes back. The catch is that when you use Rails standard params flow, the entire params hash gets passed to the object in one go, so there is no telling what order the parts would get processed in. Reassembling the date/time fields could get tricky, and the modal select might mean they should be ignored al-together.

Since the three parts are really representing one real value, I wanted some way to hand a sort of virtual compound date in the form, which could be handled as a whole when submitted back to the model.

  <%= f.fields_for :compound_event_time do |t| %>
    <div id="event_time">
      <div class="toggle">
        <%= t.radio_button :select, '15m', :class => 'relative' %>
        ...
      </div>
      <div class="custom">
        <%= t.text_field :date, :class => 'split date' %>
        <%= t.text_field :time, :class => 'split time' %>
      </div>
    </div>

Rails appears to support this. The fields_for construct was designed for displaying associated models. Reading the documentation it appeared that all it required was a method to return the named attribute, and then a <field>_attributes= writer to interpret the hash on the other side (commonly provided by accepts_nested_attributes_for, which operates with ActiveModels)

def compound_event_time
  t = CompoundEventTime.new
  t.select = '15m'
  t.date = event_time.strftime(DATE_FORMAT)
  t.time = event_time.strftime(TIME_FORMAT)
  t
end

def compound_event_time_attributes=(hash)
  self.event_time = time_selected(hash)
end

private
def time_selected(hash)
  case hash[:selected]
  ..
end

The documentation suggested that this was all that was required, but it seems that an actual model object was expected. Using a hash got nowhere (Interestingly, I stumbled across a rails commit that appears to make field_for less picky just within the last month, so this might be easier come 3.1) I was able to create a dummy object with a few attributes. Because it’s assuming a model object, I had to stub persisted? as false to quiet a NoMethodError.

class CompoundEventTime
  attr_accessor :select
  attr_accessor :date
  attr_accessor :time
  def persisted?; false; end
end
Posted Saturday, June 11th, 2011 under Essay.

2 comments

  1. Hello from Hot Texas!

    Great stuff… I will be sad to miss your talk!

    • It’s fairly warm here as well. You didn’t miss me – we found a full-length presenter and a good talk on messaging queuing systems.