简体   繁体   中英

Rails validation error messages: Displaying only one error message per field

Rails displays all validation error messages associated with a given field. If I have three validates_XXXXX_of :email , and I leave the field blank, I get three messages in the error list.

Example:

validates_presence_of :name
validates_presence_of :email
validates_presence_of :text

validates_length_of :name, :in => 6..30
validates_length_of :email, :in => 4..40
validates_length_of :text, :in => 4..200

validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i<br/>

<%= error_messages_for :comment %> gives me:

7 errors prohibited this comment from being saved

There were problems with the following fields:

Name can't be blank
Name is too short (minimum is 6 characters)
Email can't be blank
Email is too short (minimum is 4 characters)
Email is invalid
Text can't be blank
Text is too short (minimum is 4 characters)

It is better to display one messages at a time. Is there an easy way to fix this problem? It looks straightforward to have a condition like: If you found an error for :email , stop validating :email and skip to the other field.

[Update] Jan/2013 to Rails 3.2.x - update syntax; add spec

Inspired by new validation methods in Rails 3.0 I'm adding this tiny Validator. I call it ReduceValidator .

lib/reduce_validator.rb :

# show only one error message per field
#
class ReduceValidator < ActiveModel::EachValidator

  def validate_each(record, attribute, value)
    return until record.errors.messages.has_key?(attribute)
    record.errors[attribute].slice!(-1) until record.errors[attribute].size <= 1
  end
end

My Model looking like - notice the :reduce => true :

validates :title, :presence => true, :inclusion => { :in => %w[ Mr Mrs ] }, :reduce => true
validates :firstname, :presence => true, :length => { :within => 2..50 }, :format => { :without => /^\D{1}[.]/i }, :reduce => true
validates :lastname, :presence => true, :length => { :within => 2..50 }, :format => { :without => /^\D{1}[.]/i }, :reduce => true

Works like a charm in my current Rails Project. The advantageous is, i've put the validator only on a few fields not all.

spec/lib/reduce_validator_spec.rb :

require 'spec_helper'

describe ReduceValidator do

  let(:reduce_validator) { ReduceValidator.new({ :attributes => {} }) }

  let(:item) { mock_model("Item") }
  subject { item }

  before(:each) do
    item.errors.add(:name, "message one")
    item.errors.add(:name, "message two")
  end

  it { should have(2).error_on(:name) }

  it "should reduce error messages" do
    reduce_validator.validate_each(item, :name, '')
    should have(1).error_on(:name)
  end

end

Imo simplier is:

<% @model.errors.each do |attr, msg| %>
  <%= "#{attr} #{msg}" if @model.errors[attr].first == msg %> 
<% end %>

Bert over at RailsForum wrote about this a little while back. He wrote the code below and I added some minor tweaks for it to run on Rails-3.0.0-beta2.

Add this to a file called app/helpers/errors_helper.rb and simply add helper "errors" to your controller.

module ErrorsHelper

  # see: lib/action_view/helpers/active_model_helper.rb
  def error_messages_for(*params)
        options = params.extract_options!.symbolize_keys

        objects = Array.wrap(options.delete(:object) || params).map do |object|
          object = instance_variable_get("@#{object}") unless object.respond_to?(:to_model)
          object = convert_to_model(object)

          if object.class.respond_to?(:model_name)
            options[:object_name] ||= object.class.model_name.human.downcase
          end

          object
        end

        objects.compact!
        count = objects.inject(0) {|sum, object| sum + object.errors.count }

        unless count.zero?
          html = {}
          [:id, :class].each do |key|
            if options.include?(key)
              value = options[key]
              html[key] = value unless value.blank?
            else
              html[key] = 'errorExplanation'
            end
          end
          options[:object_name] ||= params.first

          I18n.with_options :locale => options[:locale], :scope => [:errors, :template] do |locale|
            header_message = if options.include?(:header_message)
              options[:header_message]
            else
              locale.t :header, :count => count, :model => options[:object_name].to_s.gsub('_', ' ')
            end

            message = options.include?(:message) ? options[:message] : locale.t(:body)

            error_messages = objects.sum do |object|
              object.errors.on(:name)
              full_flat_messages(object).map do |msg|
                content_tag(:li, ERB::Util.html_escape(msg))
              end
            end.join.html_safe

            contents = ''
            contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
            contents << content_tag(:p, message) unless message.blank?
            contents << content_tag(:ul, error_messages)

            content_tag(:div, contents.html_safe, html)
          end
        else
          ''
        end
  end

  ####################
  #
  # added to make the errors display in a single line per field
  #
  ####################
  def full_flat_messages(object)
    full_messages = []

    object.errors.each_key do |attr|
      msg_part=msg=''
      object.errors[attr].each do |message|
        next unless message
        if attr == "base"
          full_messages << message
        else
          msg=object.class.human_attribute_name(attr)
          msg_part+= I18n.t('activerecord.errors.format.separator', :default => ' ') + (msg_part=="" ? '': ' & ' ) + message
        end
      end
      full_messages << "#{msg} #{msg_part}" if msg!=""
    end
    full_messages
  end

end

怎么样@event.errors[:title].first

I wrote a custom helper

def display_error(field)
    if @user.errors[field].any?
        raw @user.errors[field].first+"<br>"
    end
end

and then I use it in view under the text field like so

<%= display_error(:password) %>

Similar to olovwia 's answer:

<% @errors.keys.each do |attr| %>
 <%= "#{attr.capitalize} #{@errors[attr].first}."%>
<% end %>"

I use this code for Ruby on Rails 3.0 release, which I put in lib/core_ext/rails/active_model/errors.rb :

module ActiveModel
  class Errors
    def full_message_per_field
      messages_per_field = []
      handled_attributes = []

      each do |attribute, messages|
        next if handled_attributes.include? attribute
        messages = Array.wrap(messages)
        next if messages.empty?

        if attribute == :base
          messages_per_field << messages.first
        else
          attr_name = attribute.to_s.gsub('.', '_').humanize
          attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
          options = { :default => "%{attribute} %{message}", :attribute => attr_name }

          messages_per_field << I18n.t(:"errors.format", options.merge(:message => messages.first))
        end

        handled_attributes << attribute
      end

      messages_per_field
    end
  end
end

This is essentially the same code as ActiveModel::Errors#full_messages , but won't show more than one error per attribute. Be sure to require the file (say, in an initializer) and now you can call @model.errors.full_message_per_field do |message| ... @model.errors.full_message_per_field do |message| ...

I would display all the error messages on one line and in a sentence format. You don't want the user to fix one error and end up having another error he was not aware of after submission. Telling them all the rules will save them clicks. With that said, this is how I'd do it:

flash_message_now("error", 
   @album.errors.keys.map { |k| "#{Album.human_attribute_name(k)} #{@album.errors[k].to_sentence}"}.to_sentence
)

with flash_message_now defined in ApplicationController (you can add it to a helper)

def flash_message_now(type, text)
    flash.now[type] ||= []
    flash.now[type] << text
  end

Or you can simply modify the array (with 'bang' method delete_at), so everything after stays default rails, i18n etc.

<% @article.errors.keys.each { |attr| @article.errors[attr].delete_at(1) } %> 

Complete working code:

<% if @article.errors.any? %>
  <% @article.errors.keys.each { |attr| @article.errors[attr].delete_at(1) } %> 
   <ul>
    <% @article.errors.full_messages.each do |msg| %>
     <li><%= msg %></li>
    <% end %>
  </ul>
<% end %>

Add a method to ActiveModel::Errors class

module ActiveModel
  class Errors
    def full_unique_messages
      unique_messages = messages.map { |attribute, list_of_messages| [attribute, list_of_messages.first] }
      unique_messages.map { |attribute_message_pair| full_message *attribute_message_pair }
    end
  end
end

Add it to a file, like lib/core_ext/rails/active_model/errors.rb . Create a file config/initializers/core_ext.rb and add a require "core_ext/rails/active_model/errors.rb" to it.

# Extracts at most <strong>one error</strong> message <strong>per field</strong> from the errors-object.
# @param  [ActiveModel::Errors] the_errors_object The errors-object.
# @raise  [ArgumentError] If the given argument is not an instance of ActiveModel::Errors.
# @return [Array] A string-array containing at most one error message per field from the given errors-object.
def get_one_error_per_field(the_errors_object)
  if the_errors_object.is_a? ActiveModel::Errors    
    errors = {}  
    the_errors_object.each do |field_name, associated_error|
      errors[field_name] = the_errors_object.full_message(field_name, associated_error) unless errors[field_name]
    end 
    return errors.values
  else
    raise ArgumentError.new('The given argument isn\'t an instance of ActiveModel::Errors!')
  end 
end 

My monkey patch of ActiveModel::Errors class lib/core_ext/rails/active_model/errors.rb (I use this code for Ruby on Rails 5.0 release):

module ActiveModel
  class Errors

    # don't add an attribute's error message to details
    # if it already contains at least one message

    alias_method :old_add, :add

    def add(attribute, message = :invalid, options = {})
      if details[attribute.to_sym].size.zero?
        old_add(attribute, message, options)
      end
    end

  end
end

Create a file config/initializers/core_ext.rb and add a require core_ext/rails/active_model/errors.rb to it.

I think the easiest way is to use allow_bank option. For example, to avoid display the message that the name is too short when the field is left blank, you can do the following:

validates_length_of :name, allow_blank:true, :in => 6..30

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM