First of all, take a look at Class ActiveRecord::Errors. Here are pretty decent error methods to start with.
The error_messages_for method outputs neatly formatted error messages for a given object. The catch is that you need to specify the name of the instance variable here, not the object name itself. Take the following example.
If you have this instance variable in your controller:
@user = PremiumUser.find(your_id)
Then you should use this code in your view to output the error messages:
<%= error_messages_for 'user' %>
If you play it safe, you store your passwords in some encrypted format, e.g. an md5 hash. Here's the code to do this:
def password=(password) write_attribute(:password, Digest::MD5.hexdigest(password) ) end ## NOTE: do NOT use this code
Now, you also want to safeguard your web application against brute force password guessing cracks. Nobody should be allowed to have a password of less than 8 characters. So, you add in a validation:
validates_length_of( :password, :minimum => 8, :message => 'too short (minimum 8 characters)'.t)
But as you test this code, you notice that even one character-sized passwords get validated! This is because every md5 hash generates a string with a predetermined length (usually 128 bits), which almost always exceeds your minimum password length.
So, how do you validate the password's length? By converting it into a hash after validation has taken place! Scrap the write accessor, and put in this code:
def prepare_password self.password = Digest::MD5.hexdigest(self.password) end
How do we run this converter after validation? Simple. Put in this callback:
after_validation :prepare_password
Use the globalize plugin to localize the error messages. Insert the following code in rails_apps/your_rails_app/vendor/plugins/for-1.1/lib/globalize/rails/active_record_helper.rb
. This is the only code that really works in Rails 1.1.6 (there are several other code sources, but I couldn't get them to work). I found the code on the Spongecell Techblog
def error_messages_for(object_list, options = {}) return "" if object_list.nil? options = options.symbolize_keys bullets,main_obj_name = bullets_from_errors(object_list, options) if !bullets.blank? #default to standard error message. replace the ${NUM_ERRORS} with the number of errors. #options[:header_message] ||= "%d errors prohibited this %1 from being saved".t(nil,bullets.length).gsub('%1',"#{(main_obj_name || 'object').t}") #options[:header_message] ||= ("%d errors prohibited this %1 from being saved".t(nil,bullets.length)).gsub('%1',"#{(main_obj_name || 'object').t}") options[:header_message] ||= "The data could not be saved".t options[:header_message] = options[:header_message] % bullets.length content_tag("div", content_tag( options[:header_tag] || "h2", options[:header_message]) + content_tag("p", (options[:header_sub_message] || "There were problems with the following fields:").t) + content_tag("ul", "#{bullets}"), "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation" ) else "" end end def bullets_from_errors(object_list,options) subs = options[:sub] ? options[:sub].stringify_keys : {} options[:skip] ||= [] main_obj_name = nil #create a list of bullets (html <li> tags) by concatenating the objects bullets= [object_list].flatten.inject([]) do |msg_list,object| #if it is a string get the instance variable if object.kind_of?(String) main_obj_name ||= "#{object.to_s.gsub("_", " ")}" object = instance_variable_get("@#{object}") elsif object main_obj_name ||= object.class.to_s.titleize.downcase end #if the object exists and responds have the errors object append each error to the list if (object && object.respond_to?('errors') && !object.errors.empty?) object.errors.each do |attr, msg| if (!options[:skip].include?(attr.to_s)) obj_name = (subs[attr] || "#{attr == 'base' ? '' : object.class.human_attribute_name(attr)}") #replace the field names with the names specified in the subs hash msg_list << content_tag('li', "#{obj_name} #{msg}".t("#{obj_name.t} #{msg.to_s.t}")) end end end msg_list end return bullets, main_obj_name end
Suppose you have a model that contains about thirty required fields, with fifty optional fields mixed in. Every interaction designer will tell you that you need to split up your eighty fields form into at least four separate forms. But as you do so, you notice that some required fields end up in the form for the last step. Now how do you validate the data in each steps? You could save up all data and do one massive insert or update, pointing out all validation errors in a separate form. But your interaction designer somehow doesn't seem to like this idea. In fact, what he wants is this:
This requires us to save the data from each form if the form fields are valid, ignoring all error messages about the attributes that were not on the form. In other words, we need relevant information only. Here's the strategy:
save_with_validation(false)
method to save the data without validation.Here's an example I've actually used in one of my controllers:
def save_step_two @user = User.new if @user.update_attributes(params[:user]) ## Go ahead as usual flash[:notice] = 'User was successfully updated.'.t redirect_to :controller => 'membership', :action => 'step_three' session[:membership] = @user.membership.system_name else ## See if there are any errors pertaining to our current form. If there are, store them in ## a temporary hash. Delete all error messages from the original errors object. temp_errors = Hash.new @user.errors.each do |attr, msg| temp_errors[attr] = msg[0] if params[:user].has_key?(attr) end @user.errors.clear if temp_errors.empty? ## Save the form without further validation ## - and move on to the next step @user.save_with_validation(false) flash[:notice] = 'User was successfully updated.'.t redirect_to :action => 'step_three' else ## Add the error messages in our temporary hash to the error object ## - and go back to the original form temp_errors.each do |attr, msg| @user.errors.add(attr, msg) end flash[:error] = 'Something went wrong while updating the user.'.t render :action => 'step_two' end end end
If you define required attributes for your model with validates_presence_of
, you don't have to repeat this information in your views any longer. This forum thread was a source of inspiration for auto-generating css classes on form fields.
Given an example object @user use this method in your views @user.class.attr_presence_required.include?(:firstname)
to find out whether firstname
is a required field. You can use this information to output an appropriate css class, as shown in the next sample code:
<div class="<%= (@user.class.attr_presence_required.include?(:firstname)) ? 'required' : 'optional' %>"> <label for="user_firstname"><%= 'First Name'.t %></label> <%= text_field 'user', 'firstname' %> </div>
Here, the html class attribute is set to required
if the firstname
field is required, otherwise the html class attribute is set to optional
.
You can combine this approach with an error indicator:
<div class="<%= error_message_on('user', 'firstname') ? 'error' : ((@user.class.attr_presence_required.include?(:firstname)) ? 'required' : 'optional') %>"> <label for="user_firstname"><%= 'First Name'.t %></label> <%= text_field 'user', 'firstname' %> </div>
To make it all work, we have to extend the ClassMethods module. Add the following code to a library which you then require
in environment.rb.
module ActiveRecord module Validations module ClassMethods alias_method :no_tracking_validates_presence_of, :validates_presence_of @@attr_presence_required = Hash.new def validates_presence_of(*attr_names) self.attr_presence_required = Hash.new unless self.attr_presence_required attr_names.each {|a| self.attr_presence_required[a]=true} no_tracking_validates_presence_of(*attr_names) end def attr_presence_required @@attr_presence_required end def attr_presence_required=(value) @@attr_presence_required = value end end end end
Here, the required fields are copied to a hash which is stored in a class variable. Because the ClassMethods module is mixed in with the model classes, you can access the hash from within your model class. Please note that you're using an instance variable in your views (most of the time). So to get to the hash from within your view, you need to get to the class first: @user.class.attr_presence_required
.
Furthermore, you can also access the validation status for each field: the attribute is stored as the key in the attr_presence_required
hash, which points to a boolean indicating whether the attribute is valid (true) or not (false).
Finally, there's also a more comprehensive (and complicated) approach by Michael Schuerig. Please visit these links to learn more about it:
The livevalidation plugin reads your model-based validation rules (e.g. “validates_presence_of”) and turns them into client-side javascript-based form validation. Here's the API of the plugin: http://livevalidation.rubyforge.org. By and large, this plugin works okay. There are a few pitfalls though.
Some regular expressions which can be used in Ruby do not translate into JavaScript. Livevalidation literally copies everything out of the validation rules. So:
validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :message => "Please check email format. Example: mike@gmail.com"
Is turned into:
<script type="text/javascript"> var user_email = new LiveValidation('user_email');user_email.add(Validate.Format, {"validMessage": "", "failureMessage": "Please check email format. Example: mike@gmail.com", "pattern": /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i}); user_email.add(Validate.Length, {"validMessage": "", "minimum": 3, "maximum": 100}); var user_email_presence = new LiveValidation('user_email',{onlyOnSubmit: true}); user_email_presence.add(Validate.Presence, {"validMessage": "", "failureMessage": "Please fill in your email address"}) </script>
This lovely piece of javascript code is inserted right underneath your email form field.
The message here is to check your regular epxressions. See to it that they keep working in javascript!
The normal usage pattern of “validates_confirmation_of :password” is that you first have a password field, and then a confirmation field. Livevalidation, however, scans your model and adds its javascript to the first field. For instance,
validates_confirmation_of :password
results in a situation where the confirmation check is applied immediately when you leave the password field and enter the password-confirmation field. This is very awkward of course, so you'll have to reverse and relabel the fields in your view:
<p> <label for="user_password_confirmation">Password</label><br/> <%= f.password_field :password_confirmation, :onchange => "$('user_password').value=''" %> </p> <p> <label for="user_password">Confirm Password</label><br/> <%= f.password_field :password %> </p>
If you take a good look at the ids (as shown in the “for” attribute of the label tag), you'll notice that I've put the confirmation field before the actual password field. This is okay as long as you remember to put your onchange attribute (which clears the second field upon changes in the first field) in the first field as well.
Livevalidation's default is to immediately check a form field as you start typing. This is very annoying, as it results in error messages before you've even had a chance to complete the field. So I've made some changes in the javascript file (LiveValidation 1.3, prototype.js version). Take a look at the initialize function (the constructor) of the LiveValidation object. Here you get a chance to change the default values. For example, I've set “onlyOnBlur” to true:
onlyOnBlur: true
Now my form field does not get validated until I leave the field.
Another annoying feature is that empty form fields are reported as invalid as well. The following scenario clearly illustrates this:
This is unacceptable, so I've arranged for all validates_presence_of check to be made onSubmit only. Here's my rather ugly hack of the form_helpers.rb
file:
def live_validation(object_name, method) if validations = self.instance_variable_get("@#{object_name.to_s}").class.live_validations[method.to_sym] rescue false field_name = "#{object_name}_#{method}" presence_code = nil validation_types = validations.map do |type, configuration| if type == :presence presence_code = live_validation_code("#{field_name}_presence", type, configuration) '' else live_validation_code(field_name, type, configuration) end ##live_validation_code(field_name, type, configuration) end.join(';') javascript = initialize_validator(field_name) + validation_types if presence_code javascript + initialize_validator(field_name, "{onlyOnSubmit: true}") + presence_code else javascript end else '' end end def initialize_validator(field_name, options = nil) if options "var #{field_name}_presence = new LiveValidation('#{field_name}',#{options});" else "var #{field_name} = new LiveValidation('#{field_name}');" end end
Now, for all validates_presence_of validations, a separate LiveValidation object is instantiated which is called only when you submit the form.
validates_length_of
does not take custom messages in Rails 2.1. This needs repairing first. Here is a patch. Now apply this patch to Globalize too, if you use that module. This is the file: plugins/globalize/lib/globalize/rails/active_record.rb
, and this is the patched code:
def validates_length_of(*attrs) [... stuff omitted ... ] #too_short = options[:too_short] #too_long = options[:too_long] too_short = (options[:message] || options[:too_short]) % option_value.begin too_long = (options[:message] || options[:too_long]) % option_value.end [... stuff omitted ... ] end
Now Rails 2.1 supports custom error messages on validates_length_of
. Hurray! So, why not have LiveValidation play along? In live_validation.js
, look for the Length: function(value, paramsObj)
line. Apply these changes:
Length: function(value, paramsObj){ var value = String(value); var paramsObj = paramsObj || {}; // Onno Schuit inserted paramsObj.failureMessage as fallback, to ensure compatibility with Rails' :within parameter. var params = { wrongLengthMessage: paramsObj.wrongLengthMessage || paramsObj.failureMessage || "Must be " + paramsObj.is + " characters long!", tooShortMessage: paramsObj.tooShortMessage || paramsObj.failureMessage || "Must not be less than " + paramsObj.minimum + " characters long!", tooLongMessage: paramsObj.tooLongMessage || paramsObj.failureMessage || "Must not be more than " + paramsObj.maximum + " characters long!", is: ((paramsObj.is) || (paramsObj.is == 0)) ? paramsObj.is : null, minimum: ((paramsObj.minimum) || (paramsObj.minimum == 0)) ? paramsObj.minimum : null, maximum: ((paramsObj.maximum) || (paramsObj.maximum == 0)) ? paramsObj.maximum : null }
That's it!
Rails 2.3 saw the introduction of nested models / nested attributes. So, to keep LiveValidation (1.3) running, I've patched vendor/plugins/livevalidation/lib/form_helpers.rb
and (in the same directory) live_validations.rb
. As an added bonus, the patch also provides:
select
method)I've also customized the livevalidation.js file, which I have included here as an external backup: livevalidation_patched2.js.txt
Please note that the Rails ajax_wipe method defined below outputs a call to the javascript method destroyById, which is defined in my patched livevalidation.js file.
form_helpers.rb
:
module ActionView mattr_accessor :live_validations ActionView::live_validations = true module Helpers module FormHelper [ :text_field, :text_area, :password_field ].each do |field_type| define_method "#{field_type}_with_live_validations" do |object_name, method, options| live = options.delete(:live) live = ActionView::live_validations if live.nil? object = options[:object] send("#{field_type}_without_live_validations", object_name, method, options) + ( live ? live_validations_for(object_name, method, object) : '' ) end alias_method_chain field_type, :live_validations end def live_validations_for(object_name, method, object = nil) script_tags(live_validation(object_name, method, object)) end private def tag_id(object_name, method_name) "#{sanitized_object_name(object_name)}_#{sanitized_method_name(method_name)}" end def sanitized_object_name(object_name) object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") end def sanitized_method_name(method_name) method_name.sub(/\?$/,"") end def live_validation(object_name, method, object = nil) if validations = (object) ? object.class.live_validations[method.to_sym] : self.instance_variable_get("@#{object_name.to_s}").class.live_validations[method.to_sym] rescue false #if validations = self.instance_variable_get("@#{object_name.to_s}").class.live_validations[method.to_sym] rescue false field_name = tag_id(object_name,"#{method}") presence_code = nil validation_types = validations.map do |type, configuration| if type == :presence presence_code = live_validation_code("#{field_name}_presence", type, configuration) '' else live_validation_code(field_name, type, configuration) end ##live_validation_code(field_name, type, configuration) end.join(';') ajax_wipe = "LiveValidationForm.destroyById('#{field_name}'); " javascript = (validations.size == 1 and presence_code) ? "" : initialize_validator(field_name) + validation_types if presence_code #javascript + ";" + initialize_validator(field_name, "{onlyOnSubmit: true}") + presence_code ajax_wipe + initialize_validator(field_name, "{onlyOnSubmit: true}") + presence_code + ";" + javascript else ajax_wipe + javascript end else '' end end def initialize_validator(field_name, options = nil) if options "var #{field_name}_presence = new LiveValidation('#{field_name}',#{options});" else "var #{field_name} = new LiveValidation('#{field_name}');" end end def live_validation_code(field_name, type, configuration) "#{field_name}.add(#{ActiveRecord::Validations::VALIDATION_METHODS[type]}" + ( configuration ? ", #{configuration.to_json}" : '') + ')' end def script_tags(js_code = '') ( js_code.blank? ? '' : "<script type='text/javascript'>#{js_code}</script>" ) end end module FormOptionsHelper def select_with_live_validations(object, method, choices, options = {}, html_options = {}) live = options.delete(:live) live = ActionView::live_validations if live.nil? real_object = options[:object] select_without_live_validations(object, method, choices, options, html_options) + ( live ? live_validations_for(object, method, real_object) : '' ) end alias_method_chain :select, :live_validations end class InstanceTag #def to_input_field_tag_with_script(field_type, options = {}) # "#{to_input_field_tag_without_script(field_type, options = {})}<script>a='test';</script>" #end #alias_method_chain :to_input_field_tag, :script end end end
live_validations.rb
:
module ActiveRecord module Validations LIVE_VALIDATIONS_OPTIONS = { :failureMessage => :message, :pattern => :with, :onlyInteger => :only_integer } # more complicated mappings in map_configuration method VALIDATION_METHODS = { :presence => "Validate.Presence", :numericality => "Validate.Numericality", :format => "Validate.Format", :length => "Validate.Length", :acceptance => "Validate.Acceptance", :confirmation => "Validate.Confirmation" } module ClassMethods VALIDATION_METHODS.keys.each do |type| define_method "validates_#{type}_of_with_live_validations".to_sym do |*attr_names| send "validates_#{type}_of_without_live_validations".to_sym, *attr_names define_validations(type, attr_names) end alias_method_chain "validates_#{type}_of".to_sym, :live_validations end def live_validations @live_validations ||= {} end private def define_validations(validation_type, attr_names) conf = (attr_names.last.is_a?(Hash) ? attr_names.pop : {}) attr_names.each do |attr_name| configuration = map_configuration(conf.dup, validation_type, attr_name) add_live_validation(attr_name, validation_type, configuration) end end def add_live_validation(attr_name, type, configuration = {}) @live_validations ||= {} @live_validations[attr_name] ||= {} @live_validations[attr_name][type] = configuration end def map_configuration(configuration, type = nil, attr_name = '') LIVE_VALIDATIONS_OPTIONS.each do |live, rails| configuration[live] = configuration.delete(rails) end if type == :numericality if configuration[:onlyInteger] configuration[:notAnIntegerMessage] = configuration.delete(:failureMessage) else configuration[:notANumberMessage] = configuration.delete(:failureMessage) end end if type == :length and range = ( configuration.delete(:in) || configuration.delete(:within) ) configuration[:minimum] = range.begin configuration[:maximum] = range.end end if type == :confirmation configuration[:match] = self.to_s.underscore + '_' + attr_name.to_s + '_confirmation' end configuration[:validMessage] ||= '' configuration.reject {|k, v| v.nil? } end end end end
LiveValidation does not support radio button validation. That's perfectly understandable because, after all, there is not much to validate. Or is there? Well, sometimes you want to make sure that a user has made a choice, without preselecting a default choice. For example, if you want to be politically correct on your forms, you should probably abstain from making a default choice for “gender”.
So, to ensure that the user has made a choice when the form is submitted, store the “choice fact” (“a choice has been made”) in a dummy form field. This is the field that we will actually validate.
<div class="formItem required"> <label for="guest_male" class="formLabel" ><%= t("guest.form.gender") %></label> <div class="formField"> <%= main_form.radio_button :male, true, {:onchange => "recordState('dummy')"}%><%= t("guest.form.male") %> <%= main_form.radio_button :male, false, {:onchange => "recordState('dummy')"} %><%= t("guest.form.female") %> <%= text_field_tag "dummy", "", :readonly => "readonly", :style => "width:0px;height:18px;padding:0px;margin:0px;border:0px solid white;visibility:hidden;" %> <script type="text/javascript"> var dummy_presence = new LiveValidation('dummy',{onlyOnSubmit: true});dummy_presence.add(Validate.Presence, {"validMessage": "", "failureMessage": "<%= t('guest.validation.male.presence') %>"}); </script> </div> </div>
(No, the actual underlying code is not PC, but the user never gets to see that unless she dives into the html source).
Here's the accompanying little javascript function:
/** * Set a field value to 1 and give it focus (usefull if you want to make sure * that the user has selected at least one item from a range of options) * * @var id {string} - the string of the object */ function recordState(id) { $(id).value = "1"; $(id).focus(); }
Rails 2.3.8 escapes all Ruby strings in Erb templates, including our LiveValidation javascript. To prevent this, add the following code (in addition to the changes mentioned above) in vendor/plugins/livevalidation/lib/form_helpers.rb
.
module ActionView mattr_accessor :live_validations ActionView::live_validations = true module Helpers module FormHelper ## Nothing changed... [ :text_field, :text_area, :password_field ].each do |field_type| define_method "#{field_type}_with_live_validations" do |object_name, method, options| live = options.delete(:live) live = ActionView::live_validations if live.nil? object = options[:object] send("#{field_type}_without_live_validations", object_name, method, options) + ( live ? live_validations_for(object_name, method, object) : '' ) end alias_method_chain field_type, :live_validations end ## Following two methods have been changed to use the 'raw' method def live_validations_for(object_name, method, object = nil) ## As of Rails 2.3.8, strings which end up in Erb templates are always escaped - but we need this one 'raw' raw("<script type='text/javascript'>#{live_validation(object_name, method, object)}</script>") end def custom_livevalidation(html_id, validations) content = "LiveValidationForm.destroyById('#{html_id}');\n" validations.each do |validation, message| message_key = (validation.to_s == "Numericality") ? "notANumberMessage" : "failureMessage" only_on_submit = (validation.to_s == "Numericality") ? "false" : "true" content += "var val_#{html_id}_#{validation} = new LiveValidation('#{html_id}',{onlyOnSubmit: #{only_on_submit}});\n" content += "val_#{html_id}_#{validation}.add(Validate.#{validation}, {\"validMessage\": \"\", \"#{message_key}\": \"#{message}\"});\n" end #script_tags(content) raw("<script type='text/javascript'>#{content}</script>") end ## Nothing changed... private def tag_id(object_name, method_name) "#{sanitized_object_name(object_name)}_#{sanitized_method_name(method_name)}" end def sanitized_object_name(object_name) object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") end def sanitized_method_name(method_name) method_name.sub(/\?$/,"") end def live_validation(object_name, method, object = nil) if validations = (object) ? object.class.live_validations[method.to_sym] : self.instance_variable_get("@#{object_name.to_s}").class.live_validations[method.to_sym] rescue false #if validations = self.instance_variable_get("@#{object_name.to_s}").class.live_validations[method.to_sym] rescue false field_name = tag_id(object_name,"#{method}") presence_code = nil validation_types = validations.map do |type, configuration| if type == :presence presence_code = live_validation_code("#{field_name}_presence", type, configuration) '' else live_validation_code(field_name, type, configuration) end ##live_validation_code(field_name, type, configuration) end.join(';') ajax_wipe = "LiveValidationForm.destroyById('#{field_name}'); " javascript = (validations.size == 1 and presence_code) ? "" : initialize_validator(field_name) + validation_types if presence_code #javascript + ";" + initialize_validator(field_name, "{onlyOnSubmit: true}") + presence_code ajax_wipe + initialize_validator(field_name, "{onlyOnSubmit: true}") + presence_code + ";" + javascript else ajax_wipe + javascript end else '' end end def initialize_validator(field_name, options = nil) if options "var #{field_name}_presence = new LiveValidation('#{field_name}',#{options});" else "var #{field_name} = new LiveValidation('#{field_name}');" end end def live_validation_code(field_name, type, configuration) "#{field_name}.add(#{ActiveRecord::Validations::VALIDATION_METHODS[type]}" + ( configuration ? ", #{configuration.to_json}" : '') + ')' end def script_tags(js_code = '') ( js_code.blank? ? '' : "<script type='text/javascript'>#{js_code}</script>" ) end end module FormOptionsHelper def select_with_live_validations(object, method, choices, options = {}, html_options = {}) live = html_options.delete(:live) live = ActionView::live_validations if live.nil? real_object = options[:object] select_without_live_validations(object, method, choices, options, html_options) + ( live ? live_validations_for(object, method, real_object) : '' ) end alias_method_chain :select, :live_validations end class InstanceTag #def to_input_field_tag_with_script(field_type, options = {}) # "#{to_input_field_tag_without_script(field_type, options = {})}<script>a='test';</script>" #end #alias_method_chain :to_input_field_tag, :script end end end