Ajax Validations with Rails and jQuery (Screencast)

Server side validations are a big part of the modern web and I'm consistently surprised at the number of sites that do not use this technique. Perhaps it's an issue of scalability that I'm not understanding, but for most of the applications I'm working on this technique works just fine.


The Ruby Code

# config/routes.rb
map.resources :contacts, :member => {:validate => :post}

# app/models/contact.rb
class Contact < ActiveRecord::Base
  validates_presence_of :name, :email
  validates_length_of :name, :within => 2..15, :too_long => "Must be under 15 characters", :too_short => "Must be over 2 characters"
  validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
  validates_uniqueness_of :email
  
  def self.validate_field(field, value)
    validity = Contact.new(field => value)
    validity.valid?
    if validity.errors.on field
      ajaxResponse = {:valid => false, field.to_sym => validity.errors[field]}
    else
      ajaxResponse = {:valid => true}
    end  
  end
    
end


# app/controllers/contacts_controller.rb
  def validate
    if params[:field].blank? || params[:value].blank?
      render :nothing => true
    else
       @valid = Contact.validate_field(params[:field], params[:value])
       render :json => @valid
    end
  end

The Views

# apps/layouts/contacts.html.erb || apps/layouts/application.html.erb
  <%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? %>
  <%= javascript_include_tag 'jquery-1.4.2.min', 'application' %>
  <%= yield :scripts %>


# apps/views/contacts/new.html.erb
<% content_for :scripts do %>
        <%= javascript_include_tag 'new_contact' %>
<% end %>

<h1>New contact</h1>

<% form_for(@contact) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </p>
  <p>
    <%= f.submit 'Create' %>
  </p>
<% end %>

The Javascript

// Application.js
function validate_field(name, value, field) {
        var postData = { };
        postData['field'] = name;
        postData['value'] = value;
        $.post('/contacts/validate', postData, function(data) {
                if(data.valid==false) {
                        $(field).parent('p').addClass('fieldWithErrors');
                        $(field).next('span').remove();
                        $(field).after('<span class="inlineError">'+ data[name] +'</span>');
                }else{
                        $(field).parent('p').removeClass('fieldWithErrors');
                        $(field).next('span').fadeOut(200, function() { $(this).remove(); });                        
                }                                                        
        }, 'json');        
}

// General delay function, used to watch keyups,
// Taken from: http://stackoverflow.com/questions/1909441/jquery-keyup-delay
var delay = (function(){
  var timer = 0;
  return function(callback, ms){
    clearTimeout (timer);
    timer = setTimeout(callback, ms);
  };
})();

// Checks an array for a passed value
Array.prototype.in_array = function(p_val) {
        for(var i = 0, l = this.length; i < l; i++) {
                if(this[i] == p_val) {
                        return true;
                }
        }
        return false;
}

// Allows for AJAX
// Taken from http://henrik.nyh.se/2008/05/rails-authenticity-token-with-jquery
$(document).ajaxSend(function(event, request, settings) {
  if (typeof(AUTH_TOKEN) == "undefined") return;
  // settings.data is a serialized string like "foo=bar&baz=boink" (or null)
  settings.data = settings.data || "";
  settings.data += (settings.data ? "&" : "") + "authenticity_token=" + encodeURIComponent(AUTH_TOKEN);
});


// new_contact.js
$(document).ready(function() {

        $("#contact_name").keyup(function() {
                delay(function() {
                        var name = $("#contact_name").val();
                        validate_field('name', name, $("#contact_name") );
                }, 600);
        });
        
        $("#contact_email").keyup(function() {
                delay(function() {
                        var email = $("#contact_email").val();
                        validate_field('email', email, $("#contact_email") );
                }, 600);
        });
        
});
  1. B78fd29c5f9049c997a358167bc0f52c

    Dino

    6:41 PM on Friday, September 24th, 2010

    Great tutorial works great with rails 2.3.8 but in rails 3 it gives me an error

    ActionController::RoutingError (No route matches "/contacts/validate"):
    Rendered /Library/Ruby/Gems/1.8/gems/actionpack-3.0.0/lib/action_dispatch/middleware/templates/rescues/routing_error.erb within rescues/layout (0.7ms)

    any ideas?

  2. 4a24c05eeb9e3ae07ac3a63b5fafa9da

    Emerson Lackey

    5:33 AM on Saturday, October 2nd, 2010

    @Dino,

    In the process of upgrading a few of my applications to Rails3 (including this blog). I'll hopefully have some time to take notes along the way and will try to consider a Rails 3 approach to this screencast/article.

  3. 13f645b7b813cc8a1f2c4225b25070c5

    ionas

    1:30 AM on Friday, October 15th, 2010

    1. Why post and not put, one can send as many validations as he/she wishes, it won't change the data on the server until you hit submit? You could even use GET, would that not work?

    2. Doing it for each field is okay if you only have "some" fields, but if you have a ton of fields, it would probably be better to validate all of them (and somehow add an exception). JQuery for doing that would be great.

    3. Are you still using exactly this? I am really looking forward to a Rails3 version of this!

  4. 13f645b7b813cc8a1f2c4225b25070c5

    ionas

    1:31 AM on Friday, October 15th, 2010

    @ Dino

    Looks like you didn't setup the the Rails3 routing correctly.

  5. 13f645b7b813cc8a1f2c4225b25070c5

    ionas

    1:32 AM on Friday, October 15th, 2010

    Also: You could instantly trigger the validation on losing fields user focus (tab, clicking in the next field) and up the time from 600 to 800ms for instance. Less server stress even.

  6. 3fa6b425a7eedb24fe97dc0b28ac4859

    Eli B

    10:44 PM on Thursday, December 16th, 2010

    Hey Emerson-
    Thanks a lot for the code here; works like a charm, except for a few things.
    -If you are using this with Devise 1.0.8, you need to do put the controller code in the ApplicationController or elsewhere as a :before_filter, since Devise for Rails 2 doesn't support custom actions.
    -I'm not sure this works if you're trying to validate a date, because of the way dates are selected (in multiple select fields, each with different ids).
    -I also found that my method of validating birthdates added a ~30 sec delay to the request, so I added ':unless => "birthdate.nil?"' to skip it.
    I'm using this method: http://snippets.dzone.com/posts/show/837
    I think this happens because when it's nil, it checks for every case in that list? (e.g. it's *the* worst case for a crappily-written linear time algo). Anyone know a better way to do this? (purely for the sake of it, will never happen with birthdate.nil? switch)

  7. 3fa6b425a7eedb24fe97dc0b28ac4859

    Eli B

    10:45 PM on Thursday, December 16th, 2010

    Lol, I was just accused of being a spambot by your site, wonderful :)

  8. 3fa6b425a7eedb24fe97dc0b28ac4859

    Eli B

    10:48 PM on Thursday, December 16th, 2010

    (I am not a spambot...resubmitting)
    Hey Emerson-
    Thanks a lot for the code here; works like a charm, except for a few things.
    -If you are using this with Devise 1.0.8, you need to do put the controller code in the ApplicationController or elsewhere as a :before_filter, since Devise for Rails 2 doesn't support custom actions.
    -I'm not sure this works if you're trying to validate a date, because of the way dates are selected (in multiple select fields, each with different ids).
    -I also found that my method of validating birthdates added a ~30 sec delay to the request, so I added ':unless => "birthdate.nil?"' to skip it.
    I'm using this method: http://snippets.dzone.com/posts/show/837
    I think this happens because when it's nil, it checks for every case in that list? (e.g. it's *the* worst case for a crappily-written linear time algo). Anyone know a better way to do this? (purely for the sake of it, will never happen with birthdate.nil? switch)

  9. 3fa6b425a7eedb24fe97dc0b28ac4859

    Eli B

    11:02 PM on Thursday, December 16th, 2010

    Hey Emerson-
    Thanks a lot for the code here; works like a charm, except for a few things.
    -If you are using this with Devise 1.0.8, you need to do put the controller code in the ApplicationController or elsewhere as a before filter, since Devise for Rails 2 doesn't support custom actions.
    -I'm not sure this works if you're trying to validate a date, because of the way dates are selected (in multiple select fields, each with different ids).
    -I also found that my method of validating birthdates added a ~30 sec delay to the request, so I added ':unless => "birthdate.nil?"' to skip it.
    http://snippets.dzone.com/posts/show/837
    I think this happens because when it's nil, it checks for every case in that list? (e.g. it's the worst case for a crappily-written linear time algo). Anyone know a better way to do this? (purely for the sake of it, will never happen with the above switch)

  10. 3fa6b425a7eedb24fe97dc0b28ac4859

    Eli B

    11:53 PM on Thursday, December 16th, 2010

    Hold the phone, so is there are a way to do this with information from multiple fields? This matters if you want to make sure the password confirmation field matches the password field.

  11. 5a754f99fbc5464ffd8beccd257160e1

    Dorian

    11:20 AM on Friday, February 4th, 2011

    Thanks for this screencast, it's exactly what i was looking for in order to understand how to implement Ajax on a form with Rails 2.3

  12. 19cf68871f3994dfca7c31affa00323c

    mynotes

    3:54 AM on Friday, August 5th, 2011

    first thank u for great screencast

    is this working on rails 3?
    bcoz i have plan to use this on my application

    regards,
    mynotes