Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created January 31, 2012 15:41
Using jQuery Deferred To Chain Validation Rules In An Asynchronous, Non-Blocking Environment
// Define the aynchronous validate() plugin for jquery.
(function( $ ){
// Define the validation factory. This will create an augmented
// Deferred object for each validator within the validation chain.
function Validation(){
// Create a deferred object that will be used to hold the
// state of a given validation step.
var deferred = $.Deferred();
// Define the convenience methods for semantically meaningful
// rejection states.
// The data submitted with the request is bad (in part).
deferred.badRequest = function( message ){
// Reject the deferred with a 400 response.
deferred.reject( 400, (message || "Bad Request") );
};
// The user is not authorized to make the request.
deferred.notAuthorized = function( message ){
// Reject the deferred with a 401 response.
deferred.reject( 401, (message || "Not Authorized") );
};
// The target of the request could not be found.
deferred.notFound = function( message ){
// Reject the deferred with a 404 response.
deferred.reject( 404, (message || "Not Found") );
};
// The request method was not allowed for this call.
deferred.methodNotAllowed = function( message ){
// Reject the deferred with a 405 response.
deferred.reject( 405, (message || "Method Not Allowed") );
};
// Return the augmented deferred object.
return( deferred );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Define the public API for the validation plugin.
$.validate = function( /* callbacks */ ){
// Create a master deferred object for the entire validation
// process. This will be rejected if ANY of the validators
// is rejected. It will be resolved only after ALL of the
// validators is resolved.
var masterDeferred = $.Deferred();
// Create a true array of validation functions (so that we
// can make use of the core Array functions).
var validators = Array.prototype.slice.call( arguments );
// I provide a recursive means to invoke each validator.
var invokeValidator = function( validator, previousResults ){
// Create a deferred result for this validator.
var result = Validation();
// Create a promise for our deferred validation so that
// we can properly bind to the resolve / reject handlers
// for the validation step.
result.promise().then(
function( /* Resolve arguments. */ ){
// This validation passed. Now, let's see if we
// have another validation to execute.
var nextValidation = validators.shift();
// Check for a next validation.
if (nextValidation){
// Recusively invoke the validation. When we
// do this, we want to pass-through the
// previous validation result in case it is
// needed by the next step.
return(
invokeValidator( nextValidation, arguments )
);
}
// No more validation steps are provided. We can
// therefore consider the validation process to
// be resolved. Resolve the master deferred.
masterDeferred.resolve();
},
function( type, message ){
// This validation failed. We cannot proceed with
// any more steps in validation - we must reject
// the master deferred.
// Check to see if we have any arguments.
if (arguments.length === 0){
// Since we have no data, we have to default
// to a 500 erorr.
type = 500;
message = "Internal Server Error";
// Check to see if we have two arguments - if not,
// then we can fill in the TYPE.
} else if (arguments.length === 1){
// Shift message to appropriate param.
message = type;
// Set 400 error message since we have an
// error message, but we don't know what type
// of error it was exactly.
type = 400;
}
// Reject the master deferred.
masterDeferred.reject( type, message );
}
);
// While the validation is intended to be asynchronous,
// let's catch any synchronous errors.
try {
// Create an invocation arguments collection so that
// can seamlessly pass-through any previous result.
var validatorArguments = (previousResults || []);
// Prepend the result promise onto the array.
Array.prototype.unshift.call(
validatorArguments,
result
);
// Call the validator.
validator.apply( null, validatorArguments );
} catch( syncError ){
// If there was a synchronous error in the callback
// that was not caught, let's return a 500 server
// response error.
masterDeferred.reject(
500,
(syncError.type || "Internal Server Error")
);
}
};
// Invoke the first validator.
invokeValidator( validators.shift() );
// Return the promise of the master deferred object.
return( masterDeferred.promise() );
};
})( jQuery );
// End jQuery plugin.
// ---------------------------------------------------------- //
// ---------------------------------------------------------- //
// ---------------------------------------------------------- //
// ---------------------------------------------------------- //
// Set up a dummy http object for our validation.
var HTTP = {
method: "PUT",
authorization: "ben"
};
// Set up a dummy form object for our validation.
var form = {
id: 4,
name: "Tricia",
age: "20",
email: "tricia-smith@gmail.com"
};
// ---------------------------------------------------------- //
// ---------------------------------------------------------- //
// Run the validation on the request.
var requestValidation = $.validate(
// Validate that the incoming request has been authorized for
// this API request.
function( validation ){
// Check to see if the given user is authorized.
if (HTTP.authorization !== "ben"){
// Reject the request.
return( validation.notAuthorized() );
}
// Approve this STEP of the validation process.
validation.resolve();
},
// Make sure this was a PUT since we are updating data.
function( validation ){
// Check for PUT verb (required for this update).
if (HTTP.method !== "PUT"){
// Reject the request.
return( validation.methodNotAllowed( "Updates require PUT." ) );
}
// Approve this STEP of the validation process.
validation.resolve();
},
// Validate ID of target user.
function( validation ){
// PRETEND that this step was actually something that
// required going to the disk / databse asynchronously.
setTimeout(
function(){
// For this step, just assume it was found.
if (false){
// Reject the request.
return( validation.notFound( "User was not found." ) );
}
// Approve this STEP of the validation process.
validation.resolve();
},
1000
);
},
// Validate the name.
function( validation ){
// Check to see that the name has a valid length.
if (!$.trim( form.name ).length){
// Reject the request.
return( validation.badRequest( "Name is required." ) );
}
// Approve this STEP of the validation process.
validation.resolve();
},
// Validate age data type (for numeric).
function( validation ){
// Make sure the age is numeric.
if (isNaN( form.age )){
// Reject the request.
return( validation.badRequest( "Age must be a number." ) );
}
// Approve this STEP of the validation process and pass the
// AGE value through to the next step. We don't need to do
// this, really - I'm just testing the feature.
validation.resolve( form.age );
},
// Validate that the age number is appropriate.
function( validation, age ){
// Make sure the age at least 18.
if (age < 18){
// Reject the request.
return( validation.reject( "Age must be GTE to 18." ) );
}
// Approve this STEP of the validation process.
validation.resolve();
},
// Validate the email address.
function( validation ){
// LOSELY check for email format.
if (form.email.search( /^[^@]+@[^@]+\.\w+$/ ) === -1){
// Reject the request.
return( validation.reject( "Valid email address is required." ) );
}
// Approve this STEP of the validation process.
validation.resolve();
}
);
// Bind to the outcome of the request validation. If all of the steps
// have passed validation then the request is valid and can be
// processed further. If ANY OF THE STEPS were rejeceted, the request
// is NOT valid.
requestValidation.then(
// Success handler.
function(){
// Log success!
console.log( "SUCCESS!" );
},
// Fail handler (for validation).
function( type, message ){
// Log error!
console.log( "FAIL", type, message );
}
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment