Search code examples
ruby-on-railsmulti-model-forms

How to use error_messages_for in a multi-model form?


I have two models: Album and Track. Album has many tracks, and Track belongs to album.

I'd like to have the ability to create as many tracks as needed while creating the album, similailiy to railscasts episode 197. Unlike the railscasts episode, though, the Track form contains both a title and a description - both are required.

Right now, the form looks like this:

Create New Album

Name: [      ]

    Track (remove link)
          Name:        [      ]
          Description: [      ]

    Track (remove link)
          Name:        [      ]
          Description: [      ]

(add track link)

If I decide to submit the form blank, I get the following error messages on top of the form:

Description can't be blank
Title can't be blank
Title can't be blank

These error messages are not specific to the model, all located at the top of the page, and appear only once for each model (note that I left the fields for both blank and the error messages appear only once - not specific to which track).


To generate the initial track fields, I added the following line in the new action of my album_controller: 2.times { @album.tracks.build }

The gist of what my form looks like is this:

<% form_for @album do |f| %>
<%= f.error_messages %>

<%= f.label :title %><br />
<%= f.text_field :title %>

<% f.fields_for :tracks do |f, track| %>
  <%= render :partial => 'tracks/fields', :locals => {:f => f} %>
<% end %>

<%= f.submit "Submit" %>
<% end %>

I tried replacing the top <%= f.error_messages %> with <%= error_messages_for @album %> (to only display the messages for the album), and adding a <%= error_messages_for track %> (to display the error messages specific to each track) - but this does not do the trick. Does anybody know how to approach this?

Thanks!


Solution

  • If you want to separate error messages for parent and child object it can be a little complicated. Since when you save parent object it also validates child objects and it contains errors for children. So you can do something like this:

    <% form_for @album do |f| %>
    <%= custom_error_messages_helper(@album) %>
    
    <%= f.label :title %><br />
    <%= f.text_field :title %>
    
    <% f.fields_for :tracks do |t| %>
      <%= t.error_messages message => nil, :header_message => nil %>
      <%= render :partial => 'tracks/fields', :locals => {:f => t} %>
    <% end %>
    
    <%= f.submit "Submit" %>
    <% end %>
    

    Or you can put this line with t.error_messages in 'tracks/fields' partial (I renamed form builder object form f to t because it was confusing). It should display (at least it works for me) only errors for specific child object (so you can see what title error is for which object). Also keep in mind, that Rails will automaticaly add css class fieldWithErrors to fields that contains errors, on example add to css:

    .fieldWithErrors {
      border: 1px solid red;
    }
    

    With errors for parent object it is more complicated since @album.errors contains also errors for child objects. I didn't find any nice and simple way to remove some errors or to display only errors that are associatted with parent object, so my idea was to write custom helper to handle it:

    def custom_error_messages_helper(album)
      html = ""
      html << '<div class="errors">'
      album.errors.each_error do |attr, error|
        if !(attr =~ /\./)
          html << '<div class="error">'
          html << error.full_message
          html << '</div>'
        end
      end
     html << '</div>'
    end
    

    It will skip all errors that are for attribute which name conatins '.' - so it should print all errors that are associated with the parent object. The only problem is with errors that are added to base - since they attr value is base and I'm not sure how are errors added to base to child object added to errors in parent object. Probably they attr value is also base so they will be printed in this helper. But it won't be problem if you don't use add_to_base.