Vita Rara: A Life Uncommon

Multi-Model Forms in Ruby on Rails


Categories: |

This isn't going to be an exhaustive tutorial on multi-model forms, but more of some observations from my learning to work with them over the past week. For a great introduction to multi-model forms see Railscasts. The major issue I've found if that the error messages when things go wrong, particularly at the view layer are almost meaningless or at worse utterly and totally misleading.

Issue with fields_for

I have been working on a multi-model form in my application today and ran into the following error:

`@...' is not allowed as an instance variable name

This is coming from the following markup:

<% fields_for "...", la.contact_mechanism do |ta_la_f| %>
    <%= ta_la_f.text_field_row :email_address, :size => 40 %>
<% end %>

Over the course of the evening I used several permutations of a string to index into my multi-model data, but to no avail. It just wouldn't work and kept throwing that error. In the end it turned out that la.contact_mechanism was nil. A simple la.build_contact_mechanism in the method that prepares my model and all seems to be well. Now we'll just see if I can persist all of the values back to the right place.

Error Messages for Multi-Model Forms

When dealing with multi-model forms it is important to present user errors next to the field where the error occurred. You don't want the user to need to move up and down the page to look at errors and then go find the place where the error occurred. To solve this issue I created the following FormBuilder. It outputs errors next to the input field where the error occurred.

class TabledBuilder < ActionView::Helpers::FormBuilder
  
  include ActionView::Helpers::ActiveRecordHelper
  include ActionView::Helpers::TagHelper
  
  def self.create_helper_method(method_name)
    define_method(method_name) do |label, *args|
      # Determine if the user wants to override
      # the label.
      alternate_label = args.last.is_a?(Hash) ? (args.last[:label] != nil ? args.last[:label] : label) : label
      @template.content_tag ("span",
        @template.content_tag("td",
          @template.content_tag("label", 
                                 alternate_label ? alternate_label.to_s.humanize : label.to_s.humanize + ":",
                                 :for => "#{@object_name}_#{label}")
        ) +
        @template.content_tag("td", super + error_message_on (:object, label) )
      )
    end
  end
  
  def self.create_helper_row_method (method_name)
    define_method(method_name + "_row") do |label, *args|
      @template.content_tag ("tr", send (method_name.to_sym, label, *args) )
    end
  end
    
  field_helpers.each do |name|
    create_helper_method(name)
    create_helper_row_method(name)
  end

end

Now, I'm really new to Rails. I've been at this for two weeks today in my spare hours, which are very few.

If you want to use this helper simply place it in app/helpers, then add the following to your app/helpers/application_helper.rb file:

module ApplicationHelper
  def tabled_form_for(name,*args, &block)
    options = args.last.is_a?(Hash) ? args.pop : {}
    options = options.merge(:builder => TabledBuilder)
    args = (args << options)
    form_for(name, *args, &block)
  end
  
  def tabled_fields_for(name, *args, &block)
    options = args.last.is_a?(Hash) ? args.pop : {}
    options = options.merge(:builder => TabledBuilder)
    args = (args << options)
    fields_for(name, *args, &block)
  end
end

Then to use it in your rhtml files:

<% tabled_form_for :project, @project, :url => { :action => :create_project } do |f| %>
    <table>
        <%= f.text_field_row :name, :size => '42' %>
        <%= f.text_area_row :description %>
    </table>
<% end %>

thank you

you was the solution for my problem! very good!

note: `@...' is not allowed as an instance variable name

=D

Thanks!

You're right -- what a misleading error message. I had the same problem today. Thanks for saving me a lot of time!