How to show nested form validation errors after validation errors for parent model?
Using Ruby on Rails 4.2 I have a nested form. While testing validation for the entire form, I noticed that validation errors for the nested form appear at the top of the list of validation errors, with validation errors for the main form appearing below.
This is the opposite order in which they are declared (as it fields_for
should appear in the scope of the parent form_for
), so it looks like this:
[name ]
[description ]
[others ]
[nested #1 ]
[nested #2 ]
But the validation errors look like this (using a space as an example of a validation error):
- Nested # 1 NestedModelName cannot be empty.
- Nested # 2 NestedModelName cannot be empty.
- The name cannot be empty.
- Description cannot be empty.
- Others cannot be empty.
This is confusing to the user as the errors appear out of order as they appear on the page. It does not expect it to be in the correct position depending on where it appears in the form, as it is obviously just checking each model in turn, but since the nested form model is usually subordinate, it should at least be added to a do not appear at the beginning. Is there a way to get nested form validation errors after parent form validation errors appear?
Additional Information:
Errors are 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 using this in every view file that contains a form:
<%= error_messages(@model) %>
source to share
Update 1 :
I found that februaryInk's answer was very close to the rule of thumb if you don't have to worry about i18n and your application text translation. If you put has_many :child_model
under all of your checks, the checks will appear in the correct order. However, full_messages
does not display model names or attributes using locale files, so if you need the error messages to be translated (which I do), my answer still looks like a decent solution.
Update 2:
It's just that after posting the first update, I was able to simplify my code, which expands the list significantly messages
, by removing the part that makes the order using detection 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 files config/locales/xx.yml
and config/application.rb
remains unchanged for this updated solution as 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
...
Application / 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 function error_messages
in application_helper.rb
and now everything works as I wanted: main form validation errors are at the top, nested form validation errors are below them, the order of errors does not change except for moving nested form errors on main form errors.
My solution was to change the line messages =
in error_messages
as shown below and add a private helper method. (This should probably be broken down to make it easier to read and understand, but I created it in the console to get what I wanted and just pasted it right in from there).
Application / 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 work for other other locales as well as I used I18n
to get all names. You also need to add the following:
config / locale / en.yml
en:
space: " "
This way, model and attribute names will be handled correctly in languages ββthat have or don't have spaces between words (the first language I need to support is Chinese, which has no spaces between words). If you needed to support Chinese, for example, you would use this:
config / locale / zh.yml
zh:
space: ""
If you don't need to support this case, all instances I18n.t('space')
can be replaced with " "
. Model and attribute names can also be translated as, but again, if you don't need to support locales outside of English, you don't need to do anything (although you can use a file en.yml
to change the model names or the attributes that are displayed).
As an example using en.yml
to change names displayed using a generic authors / books example:
config / locale / 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:
- The name cannot be empty.
- Book name cannot be empty.
- The published book cannot be empty.
But with the above additions to en.yml
it, this would be:
- Nom de plume cannot be empty.
- The manuscript title cannot be empty.
- The year of the manuscript cannot be empty.
And of course, if you have a file zh.yml
with the corresponding translations, whatever you have in them will be displayed instead.
If you need to support multiple locales, be sure to add the following to config/application.rb
(this part has only been superficially tested and may require some additional configuration):
config / application.rb
config.i18n.available_locales = [:zh, :en]
config.i18n.default_locale = :en
source to share
The order of error messages appears to reflect the order of checks and accepts_nested_attributes_for
in the model file. Place the validations in the order you want them to arrive 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 checks in each hash also seems to have an effect in that it changes the order in which the error messages of a particular attribute are displayed.
source to share