Rails affiche tous les messages d'erreur de validation associés à un champ donné. Si j'ai trois validates_XXXXX_of :email
et que je laisse le champ vide, trois messages apparaissent dans la liste des erreurs.
Exemple:
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 %>
me donne:
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)
Il est préférable d’afficher un message à la fois. Existe-t-il un moyen facile de résoudre ce problème? Il semble facile d’avoir une condition du type: Si vous rencontrez une erreur pour :email
, arrêtez de valider :email
et passez à l’autre champ.
Bert sur RailsForum a écrit à ce sujet il y a quelque temps. Il a écrit le code ci-dessous et j'ai ajouté quelques modifications mineures pour qu'il fonctionne sous Rails-3.0.0-beta2.
Ajoutez ceci à un fichier appelé app/helpers/errors_helper.rb
et ajoutez simplement helper "errors"
à votre contrôleur.
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
[Update] Jan/2013 to Rails 3.2.x - syntaxe de mise à jour; ajouter des spec
Inspiré par les nouvelles méthodes validation de Rails 3.0, j'ajoute ce petit validateur. Je l'appelle 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
Mon modèle ressemble à - remarquez le: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
Fonctionne comme un charme dans mon projet Rails actuel. L’avantage, c’est que j’ai mis le validateur sur quelques champs seulement.
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
Plus simple, c'est:
<% @model.errors.each do |attr, msg| %>
<%= "#{attr} #{msg}" if @model.errors[attr].first == msg %>
<% end %>
Qu'en est-il de ce @event.errors[:title].first
?
J'ai écrit un assistant personnalisé
def display_error(field)
if @user.errors[field].any?
raw @user.errors[field].first+"<br>"
end
end
puis je l'utilise en vue sous le champ de texte comme si
<%= display_error(:password) %>
Similaire à la réponse de olovwia :
<% @errors.keys.each do |attr| %>
<%= "#{attr.capitalize} #{@errors[attr].first}."%>
<% end %>"
J'utilise ce code pour la version 3.0 de Ruby on Rails, que j'ai mise dans 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
Il s’agit essentiellement du même code que ActiveModel::Errors#full_messages
, mais n’affichera pas plus d’une erreur par attribut. Assurez-vous d'exiger le fichier (par exemple, dans un initialiseur) et vous pouvez maintenant appeler @model.errors.full_message_per_field do |message| ...
Je voudrais afficher tous les messages d'erreur sur une ligne et dans un format de phrase. Vous ne voulez pas que l'utilisateur corrige une erreur et finisse par avoir une autre erreur dont il n'était pas conscient après la soumission. Leur dire toutes les règles leur sauvera les clics. Cela dit, voici comment je le ferais:
flash_message_now("error",
@album.errors.keys.map { |k| "#{Album.human_attribute_name(k)} #{@album.errors[k].to_sentence}"}.to_sentence
)
avec flash_message_now défini dans ApplicationController (vous pouvez l'ajouter à un assistant)
def flash_message_now(type, text)
flash.now[type] ||= []
flash.now[type] << text
end
Ajouter une méthode à la classe ActiveModel :: Errors
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
Ajoutez-le à un fichier, comme lib/core_ext/Rails/active_model/errors.rb
. Créez un fichier config/initializers/core_ext.rb
et ajoutez-y un require "core_ext/Rails/active_model/errors.rb"
.
# 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
Ou vous pouvez simplement modifier le tableau (avec la méthode 'bang' delete_at), de sorte que tout reste après Rails, i18n, etc.
<% @article.errors.keys.each { |attr| @article.errors[attr].delete_at(1) } %>
Code de travail complet:
<% 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 %>
Mon patch de singe de ActiveModel::Errors
class lib/core_ext/Rails/active_model/errors.rb
(j'utilise ce code pour la version Ruby on Rails 5.0):
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
Créez un fichier config/initializers/core_ext.rb
et ajoutez-lui un require core_ext/Rails/active_model/errors.rb
.