-
Notifications
You must be signed in to change notification settings - Fork 403
Custom 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
# config/initializers/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attr_name, value)
unless value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
record.errors.add(attr_name, :email, options.merge(:value => value))
end
end
end
# This allows us to assign the validator 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.validator
object. Create a new file public/javascripts/rails.validations.custom.js
// public/javascripts/rails.validations.custom.js
// The validator variable is a JSON Object
// The selector variable is a jQuery Object
window.clientSideValidations.validators.local['email'] = function(element, options) {
// Your validator code goes in here
if (!/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i.test(element.val())) {
// When the value fails to pass validation you need to return the error message.
// It can be derived from validator.message
return options.message;
}
}
Don't forget to include your new javscript file
<%= javascript_include_tag 'jquery', 'rails.validations', 'rails.validations.custom' -%>
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 < ActiveRecord::Base
validates_email :email
end
Client Side Validations will apply the new validator and validate your forms as needed.
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. Our Rails validator would probably look something like this:
class ZipcodeValidator < ActiveModel::EachValidator
def validate_each(record, attr_name, value)
unless ::Zipcode.where(:id => value).exists?
record.errors.add(attr_name, :zipcode, options.merge(:value => value))
end
end
end
# This allows us to assign the validator in the model
module ActiveModel::Validations::HelperMethods
def validates_zipcode(*attr_names)
validates_with ZipcodeValidator, _merge_attributes(attr_names)
end
end
Of course we still need to add the i18n message:
en:
errors:
messages:
zipcode: "Not a valid US zip code"
And let's add the Javascript validator. Because this will be remote validator we need to add it to ClientSideValidations.validators.remote
:
ClientSideValidations.validators.remote['zipcode'] = function(element, options) {
if ($.ajax({
url: '/validators/zipcode',
data: { id: element.val() },
// async *must* be false
async: false
}).status == 404) { 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 to the middleware. All ClientSideValidations middleware should inherit from ClientSideValidations::Middleware::Base
:
module ClientSideValidations::Middleware
class ZipCode < Base
def response
if ::Zipcode.where(:id => request.params[:id]).exists?
self.status = 200
else
self.status = 404
end
super
end
end
end
The #response
method is always called and it should set the status accessor. Then a call to super
is required. In the javascript we set the 'id' in the params to the value of the zipcode input, in the middleware we check to see if this zipcode exists in our zipcode database. If it does, we return 200, if it doesn't we return 404.