Yuval Karmi
Yuval Karmi

Reputation: 26713

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!

Upvotes: 2

Views: 4128

Answers (4)

FernandoFabreti
FernandoFabreti

Reputation: 11

Similar to above, but, with the code below, I can test any object or array of objects for errors:

<%= show_errors( [@company, @estabs], :header_ref => 'Company' ) %>

def show_errors(objarray, opt)

    return '' if objarray.blank?

    err = [ ]
    html  = ""

    objarray.each { |obj|  
            if obj.is_a?(Array)
                obj.each { |oo| html << show_errors([oo], opt) }
            elsif (obj.errors.count > 0 ) 
                err << obj.errors.full_messages #didnt worked with gem i18n 0.5.0
            end
              }

    return '' if (err.count==0) and (html.blank?)

    if err.count > 0
        err.flatten!            #remove sub-arrays
        err.each { |x| x.strip! }   #strip:remove espaces
        err.uniq!               #remove duplicated messages
        err.sort!

        header_ref = opt[:header_ref].nil?  ?  ""  :  opt[:header_ref]

        erro_str = (err.count == 1) ? 'erro' : 'erros'
        erro_verb = (err.count == 1) ? 'ocorreu' : 'ocorreram'

        html << '<div class="erros"  id="erros"> '
        html <<  "<b> #{header_ref}: #{err.count.to_s}  #{erro_str} #{erro_verb}: </b> "
        html << '<ul> ' 

        err.each  { |e| html << '<li> ' + e + ' </li>' }

        html << '</ul> </div>'
    end
    return html
end

Upvotes: 0

Taryn East
Taryn East

Reputation: 27747

Firstly f and f are confusing for scope. use "g" or something for your fields_for section and it will understand the scope.

Then try:

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

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

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

  <%= f.submit 'Create' %>
<% end %>

Use this with accepts_nested_attributes_for :tracks on your album model.

I have tested this myself and it works. The errors for the individual tracks appear within the section for the tracks.

I am using the latest version of Rails.

Upvotes: 1

klew
klew

Reputation: 14967

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.

Upvotes: 3

Gordon Isnor
Gordon Isnor

Reputation: 2105

Did you try this?

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

Upvotes: 0

Related Questions