如何在父模型验证错误后显示嵌套表单验证错误?

How to show nested form validation errors after the validation errors for the parent model?

在 Rails 4.2 上使用 Ruby,我有一个嵌套表单。在测试整个表单的验证时,我注意到嵌套表单的验证错误出现在验证错误列表的顶部,主表单的验证错误出现在下方。

这与它们声明的顺序相反(因为 fields_for 必须出现在父 form_for 的范围内),所以它看起来像这样:

[name        ]
[description ]
[others      ]
[nested #1   ]
[nested #2   ]

但是验证错误是这样的(使用空白作为示例验证错误):

这会让用户感到困惑,因为错误在页面上的显示顺序是乱序的。它不期望它根据它在表单中出现的位置处于正确的位置,因为它显然只是依次验证每个模型,但由于嵌套表单模型通常是从​​属的,它至少应该添加到结束而不是出现在开始。有没有办法让嵌套表单验证错误出现在父表单验证错误之后?

附加信息:

使用以下方法在视图中显示错误:

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">&times;</button>
      <p>#{sentence}</p>
      <ul>
        #{messages}
      </ul>
    </div>
    HTML

  end

并在每个包含表单的视图文件中使用它:

<%= error_messages(@model) %>

错误消息的顺序似乎反映了模型文件中验证和 accepts_nested_attributes_for 的顺序。按照您希望的顺序进行验证,accepts_nested_attributes_for 最后。要获得您作为示例给出的订单,请尝试以下操作:

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

...

每个散列中各个验证的顺序似乎也有影响,因为它改变了特定属性错误消息的显示顺序。

更新 1:

我发现如果您不需要担心应用程序文本的国际化和翻译,februaryInk 的答案非常接近正确。如果您将 has_many :child_model 放在所有验证的下面 ,验证将以正确的顺序出现。但是,full_messages 似乎没有使用语言环境文件翻译模型或属性名称,因此如果您需要翻译错误消息(我这样做),我的回答似乎仍然是一个不错的解决方案。

更新二:

在发布第一个更新后才意识到,我可以通过删除使用更新 1 中的发现进行排序的部分来简化生成 messages 列表的代码,并只保留那部分做翻译。所以这是我的新解决方案,它是我的更新 1 和我原来的解决方案的组合。关于 config/locales/xx.ymlconfig/application.rb 文件的所有其他信息对于此更新的解决方案仍然与原始解决方案相同。

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

更新结束

我在 application_helper.rb 中对我的 error_messages 函数做了一些更改,现在一切都按照我想要的方式工作:主表单验证错误在顶部,嵌套表单验证错误在那些下面, 除了将嵌套表单错误移动到主表单错误下,错误的顺序没有改变。

我的解决方案是更改 error_messages 中的 messages = 行,如下所示,并添加一个私有辅助方法。 (这可能应该被分解成多个部分,以便于阅读和理解,但我在控制台中构建它以获得我想要的内容,然后直接从那里粘贴它)。

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

此解决方案也适用于其他语言环境,因为我使用 I18n 获取所有名称。您还必须添加以下内容:

config/locales/en.yml

en:
  space: " "

这样模型和属性名称将在单词之间有或没有空格的语言中正确处理(我需要支持的第一个语言环境是中文,它在单词之间没有空格).例如,如果您确实需要支持中文,您可以使用:

config/locales/zh.yml

zh:
  space: ""

如果您不必支持这种情况,I18n.t('space') 的所有实例都可以替换为 " "。模型和属性名称也可以翻译为,但是如果您不需要支持英语以外的语言环境,您不需要做任何事情(尽管您可以使用 en.yml 文件来更改显示的模型或属性)。

例如使用 en.yml 更改使用常见 Authors/Books 示例显示的名称:

config/locales/en.yml

en:
  activerecord:
    models:
      author: "writer"
      book: "manuscript"
    attributes:
      author:
        name: "non de plume"
      book:
        name: "title"
        published: "year"

在此示例中,如果没有对 en.yml 添加上述内容,则默认值为:

  • 姓名不能为空。
  • 书名不能为空
  • 出版的书不能为空

但是在 en.yml 中增加了上面的内容,它将是:

  • 姓名不能为空。
  • 稿件标题不能为空
  • 稿件年份不能为空

当然,如果您有一个包含适当翻译的 zh.yml 文件,那么您在其中的任何内容都会显示出来。

如果您确实需要支持多语言环境,请不要忘记在 config/application.rb 中添加以下内容(这部分仅进行了粗略的测试,可能需要一些额外的配置):

config/application.rb

config.i18n.available_locales = [:zh, :en]
config.i18n.default_locale = :en