Reputation: 1209
Using Ruby on Rails 4.2, I have a nested form. When testing the validations for the entire form, I noticed that the validation errors for the nested form appear at the top of the validation errors list, with the validation errors for the main form appearing below.
This is the opposite order that they are declared (since the fields_for
has to appear within the scope of the parent form_for
), so it looks like this:
[name ]
[description ]
[others ]
[nested #1 ]
[nested #2 ]
But the validation errors appear like this (using blank as an example validation error):
This is confusing for the user, since the errors appear out of order to how they appear on the page. It don't expect it to be in the correct position according to where it appears in the form, since it is obviously just validating each model in turn, but since the nested form model is usually subordinate, it should at least be added to the end instead of showing up in the beginning. Is there any way to get the nested form validation errors to appear after the parent form validation errors?
Additional Info:
Errors are being displayed in the view using the following:
application_helper.rb
def error_messages(resource)
return '' if resource.errors.empty?
messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
sentence = I18n.t('errors.messages.not_saved',
count: resource.errors.count,
resource: resource.class.model_name.human.downcase)
html = <<-HTML
<div class="validation-error alert alert-danger alert-dismissable fade in alert-block">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
<p>#{sentence}</p>
<ul>
#{messages}
</ul>
</div>
HTML
end
and by using this in each view file containing a form:
<%= error_messages(@model) %>
Upvotes: 3
Views: 4267
Reputation: 1209
Update 1:
I discovered that the answer by februaryInk was very close to correct if you don't need to worry about i18n and translation of your application text. If you put the has_many :child_model
underneath all your validations, the validations will appear in the correct order. However, full_messages
doesn't appear to translate model or attribute names using the locale files, so if you require the error messages to be translated (which I do), my answer still seems like a decent solution.
Update 2:
Just realized after posting the first update that I could simplify my code that generates the messages
list a lot by removing the part that does the ordering using the discovery in update 1, and just keep the part that does the translation. So here is my new solution, which is a combination of my update 1 and my original solution. All other information about the config/locales/xx.yml
and config/application.rb
files is still the same for this updated solution as it was for the original.
app/models/parent_model.rb
...
validates :name, # validations hash
validates :description, # validations hash
validates :others, # validations hash
has_many :child_models
accepts_nested_attributes_for :child_models
...
app/models/child_model.rb
...
validates :nested_1, # validations hash
validates :nested_2, # validations hash
...
app/helpers/application_helper.rb
messages = resource.errors.messages.keys.map {|value| error_message_attribute(resource, value) + I18n.t('space') + resource.errors.messages[value].first}.map { |msg| content_tag(:li, msg) }.join
private
def error_message_attribute(resource, symbol)
if symbol.to_s.split(".").length > 1
model_name, attribute_name = symbol.to_s.split(".")
model_class = model_name.singularize.camelize.constantize
model_class.model_name.human + I18n.t('space') + model_class.human_attribute_name(attribute_name).downcase
else
resource.class.human_attribute_name(symbol)
end
end
End of Update
I made a few changes to my error_messages
function in application_helper.rb
and now have everything working the way I wanted: main form validation errors are on the top, nested form validation errors are under those, the order of the errors does not change except moving the nested form errors under the main form errors.
My solution was to change the messages =
line in error_messages
as shown below and to add a private helper method. (This should probably be broken down into parts to make it easier to read and understand, but I built it up in the console to get what I wanted and just pasted it directly from there).
app/helpers/application_helper.rb
messages = Hash[resource.errors.messages.keys.map.with_index(1) { |attribute, index| [attribute, [index, attribute.match(/\./) ? 1 : 0]] }].sort_by {|attribute, data| [data[1], data[0]]}.collect { |attributes| attributes[0]}.map {|value| error_message_attribute_name(resource, value) + I18n.t('space') + resource.errors.messages[value].first}.map { |msg| content_tag(:li, msg) }.join
private
def error_message_attribute_name(resource, symbol)
if symbol.to_s.split(".").length > 1
model_name, attribute_name = symbol.to_s.split(".")
model_class = model_name.singularize.camelize.constantize
model_class.model_name.human + I18n.t('space') + model_class.human_attribute_name(attribute_name).downcase
else
resource.class.human_attribute_name(symbol)
end
end
This solution should also work for other other locales, since I used I18n
to get all the names. You will have to add the following also:
config/locales/en.yml
en:
space: " "
This is so the model and attribute names will be handled correctly in languages that either have or don't have spaces between words (the first locale I need to support is Chinese, which doesn't have spaces between the words). If you did need to support Chinese, for example, you would use this:
config/locales/zh.yml
zh:
space: ""
If you don't have to support this case, all instances of I18n.t('space')
can be replaced with " "
. The model and attribute names can also be translated as, but again if you don't need to support locales beyond English you don't need to do anything (although you can use the en.yml
file to change the names of the model or attributes that are displayed).
As an example using en.yml
to change the names displayed using the common Authors/Books example:
config/locales/en.yml
en:
activerecord:
models:
author: "writer"
book: "manuscript"
attributes:
author:
name: "non de plume"
book:
name: "title"
published: "year"
In this example the default, without the above additions to en.yml
, would be:
But with the above additions to en.yml
it would be:
And of course, if you have a zh.yml
file with the appropriate translations, whatever you have in those would show up instead.
If you do need to support multiple locales, don't forget to add the following to config/application.rb
(this part was only tested superficially, and may need some additional configuration):
config/application.rb
config.i18n.available_locales = [:zh, :en]
config.i18n.default_locale = :en
Upvotes: 3
Reputation: 725
The order for error messages seems to reflect the order of the validations and accepts_nested_attributes_for
in your model file. Put the validations in the order you want them to come, with accepts_nested_attributes_for
last. To get the order you gave as an example, try this:
parent_model.rb
...
validates :name, # validations hash
validates :description, # validations hash
validates :others, # validations hash
accepts_nested_attributes_for :child_model
...
child_model.rb
...
validates :nested_1, # validations hash
validates :nested_2, # validations hash
...
The order of the individual validations in each hash also seems to have an effect in that it changes the order that a particular attribute's error messages are displayed.
Upvotes: 2