Skip to content
Geremia Taglialatela edited this page Aug 11, 2019 · 38 revisions

Custom Validators

Local Validators

Client Side Validations supports the use of custom validators in Rails 3. The following is an example for creating a custom validator that validates the format of email addresses.

Let's say you have several models that all have email fields and you are validating the format of that email address on each one. This is a common validation and could probably benefit from a custom validator. We're going to put the validator into config/initializers/email_validator.rb

Heads-up!: Put custom initializers in config/initializers, otherwise named validator helpers will not be available and migrations will not work (ref: #755)

# config/initializers/email_validator.rb

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A[^@\s]+@[^@\s]+\z/i
      record.errors[attribute] << options[:message]
    end
  end
end

# This is optional and allows to use `validates_email`
# helper in the model
module ActiveModel::Validations::HelperMethods
  def validates_email(*attr_names)
    validates_with EmailValidator, _merge_attributes(attr_names)
  end
end

Next we need to add the error message to the Rails i18n file config/locales/en.yml

# config/locales/en.yml

en:
  errors:
    messages:
      email: "Not an email address"

Finally we need to add a client side validator. This can be done by hooking into the ClientSideValidations.validators object.

Create a new file app/assets/javascripts/rails.validations.custom.js

// app/assets/javascripts/rails.validations.custom.js

// The element variable is a jQuery Object
// The options variable is a JSON Object
window.ClientSideValidations.validators.local['email'] = function(element, options) {
  // Your validator code goes in here
  // Basically, you need to rewrite the Ruby server side validator in JavaScript
  if (!/^[^@\\s]+@[^@\\s]+$/i.test(element.val())) {
    // When the value fails to pass validation, you need to return the error message.
    return options.message;
  }
}

Require rails.validations.custom.js from your application.js after //= require rails.validations

That's it! Now you can use the custom validator as you would any other validator in your model

# app/models/person.rb

class Person < ApplicationRecord
  # ...

  validates :email, email: true

  # ...
end

Client Side Validations will apply the new validator and validate your forms as needed.

Remote Validators

A good example of a remote validator would be for zipcodes. It wouldn't be reasonable to embed every single zipcode inline, so we'll need to check for its existence with remote JavaScript call back to our app. Assume we have a zipcode database mapped to the model Zipcode. The primary key is the unique zipcode.

At first we add our back-end validator code:

# config/initializers/zipcode_validator.rb

class ZipcodeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if Zipcode.where(id: value).exists?

    record.errors[attribute] << options[:message]
  end
end

# This is optional and allows to use `validates_zipcode`
# helper in the model
module ActiveModel::Validations::HelperMethods
  def validates_zipcode(*attr_names)
    validates_with ZipcodeValidator, _merge_attributes(attr_names)
  end
end
# config/locales/en.yml

en:
  errors:
    messages:
      zipcode: "Not a valid zip code"
# app/models/user.rb

class Person < ApplicationRecord
  # ...

  validates :zipcode, zipcode: true

  #...
end

And let's add the JavaScript validator. Because this will be remote validator we need to add it to ClientSideValidations.validators.remote:

// application.js

window.ClientSideValidations.validators.remote['zipcode'] = function(element, options) {
  if ($.ajax({
    url: '/validators/zipcode',
    data: { id: element.val() },
    // async *must* be false
    async: false
  }).status != 200) { return options.message; }
}

All we're doing here is checking to see if the resource exists (in this case the given zipcode) and if it doesn't the error message is returned.

Notice that the remote call is forced to async: false. This is necessary and the validator may not work properly if this is left out.

Now the extra step for adding a remote validator is to add an endpoint in our application.

# app/controllers/validators/validators_controller.rb

module Validators
  class ValidatorsController < ApplicationController
    # Please note that you may need to expose this endpoint to the public
    # and protect it against brute force or DOS attacks.
    def zipcode
      if Zipcode.where(id: params[:id])
        head :ok
      else
        head :no_content
      end
    end
  end
end
# config/routes.rb
Rails.application.routes.draw do
  # ...

  namespace :validators do
    get :zipcode, to: 'validators#zipcode'
  end

  # ...
end

In the JavaScript we set the 'id' in the params to the value of the zipcode input, in the endpoint we check to see if this zipcode exists in our zipcode database. If it does, we return 200, if it doesn't we return 204.

Please note that the endpoint may need to be protected by rack-attack, works differently if the resources to check have been persisted and could allow information disclosure.

This solution is not recommended for a uniqueness validator.

Ref: