AJAX Form Validation for Rails

Last Saturday at the Chicago Ruby User Group I gave a short presentation on Rails and AJAX. For part of the presentation I decided to solve a problem that’s haunted me as a web developer for some time – form validation.

Here’s the dilemma:

You start with server side validation, then build your form. To validate the inputs, you have to make a round trip to the server, and re-render the entire page with the validation messages. Not the smoothest process, given the bar set by Web 2.0. But at the same time, if you try to engage the user more, you find yourself writing the same validation in JavaScript for the client – not fun.

Here’s the solution:

Again, start with server side validation and a form. Then use JavaScript not to validate the data, but to facilitate an AJAX request to the server, where you there validate the field and send simple feedback to the user.

I started by searching for solutions for this. As it appears, I’m not the only developer in the room who wants AJAX validation. Here’s what I found:

I opted, for the sake of practice, to use these as inspiration to my own solution, which goes something like this:

def error_message_for(model, field, options = {})
  tag_id = "#{model}_#{field}_validator"

  # generate javascript for AJAX request and field observation
  function = "new Ajax.Request('/users/validate?field=#{field}&value=' + value, {" +
    "method: 'get'," +
    "onSuccess: function(transport) {" +
      "element = document.getElementById('#{tag_id}');" +
      "var output = transport.responseText;" +
      "var css_class = '#{options[:error_css_class]}';" +
      "if (output.length == 0) {" +
        "output = '#{options[:success]}';" +
        "css_class = '#{options[:success_css_class]}';" +
      "}" +
      "element.innerHTML = output;" +
      "element.setAttribute('class', css_class)" +
    "}" +
  " });"

  js = observe_field "#{model}_#{field}", :function => function

  # generate html for placing error message
  tag = content_tag(options[:tag], options[:hint], {
    :id => tag_id, :class => options[:hint_css_class]
  })

  return tag + js
end

I wanted my solution to be equally elegant as Rails’ non-Ajax solution, so I created a method error_message_for, which takes the model, field name, and various options. First, it creates the JavaScript I’ll need with observe_field, wiring up the Ajax request to the field we’re monitoring. Then, using content_tag produces an empty tag which will serve as our feedback placeholder. It returns all that HTML/JS to the browser. But, we haven’t done any validation yet. For that we need a validate action that can be called on whatever model you’re working with (in this case I’m working with a User):

def validate
  field  = params[:field]
  user   = User.new(field => params[:value])
  output = "" user.valid? # trigger the errors hash to be filled

  if user.errors[field] != nil
    if user.errors[field].class == String
      output = "#{field.titleize} #{user.errors[field]}"
    else
      output = "#{field.titleize} #{user.errors[field].to_sentence}"
    end
  end

  render :text => output
end

Now, we’re cooking. As the user moves from field to field, the AJAX request we wrote is created, firing validate. Validate then uses the valid? method to populate the errors collection on our model which in turn allows us to return the same error messages Rails would if we were using error_message_on.

Now we just implement it. Call our new error_message_for method right after the input field like so and that’s a wrap. The HTML and JS is returned to the browser when the page renders the first time. When focus leaves a particular field, its value is sent down to the server for validation, and the server replies with whatever applicable error messages, which are displayed in the originally empty HTML generated to begin with.

form.text_field :email
error_message_for :user, :email, {
  :tag     => :span,
  :hint    => "Your email must be formatted properly.",
  :success => "Looking good!"
}

You may have noticed some code in there referring to :success and :hint. I figure, while we’re in there let’s add support for a hint to be displayed before any validation occurs, and a success message to display when validation is passed.

The best part of the whole thing, is it’s really DRY (Don’t Repeat Yourself), and therefore simple. Change your validation rules, and the UI updates dynamically. In addition the same approach can be used on any form that has an underlying model associated with it.

Download an example from GitHub.

Thanks for reading! I'm Avand.

I’ve been working on the web for over a decade and am passionate about building great products.

I devote most of my time to building Lopery, a free budgeting that helps people spend with confidence, recover from the unexpected, and achieve financial independence. I used simple (but time consuming) budgeting principles to buy my first home. Now, I'm codifying (pun intended) those same principles into an easy to use app that helps people achieve their financial goals.

My last job was with Airbnb, where I focused on internal products that helped teams measure the quality of the software they were building. I also built internal tools for employees to stay more connected, especially after the COVID-19 pandemic. Before that, I was lead engineer at Mystery Science, the #1 way in which science is now taught in U.S. elementary school classroms. For a while, I also taught with General Assembly, teaching aspiring developers the basics of front-end web development.

I was born in Boston, grew up in Salt Lake City, and spent many years living in Chicago. In 2013, I came out West to San Francisco, which I called home for almost a decade. Now, I’m based out Mariposa, in the foothills of the Sierras.

I enjoy the great outdoors and absolutely love music and dance. Cars have been an lifelong obsession of mine. I’m the proud owner of a 2002 E-250 Sportsmobile van, and he and I have enjoyed many trips to beautiful and remote parts of the Pacific North West spreading good vibes. I also have a very soft spot for magic (slight of hand, in particular). I love the feeling of being inspired and absolutely love inspiring others.

What can I do for you?

Read my other posts or get in touch: