Using jQuery.whenSync() For Asynchronous Form Validation And Processing
Lately, I've been playing around with Node.js on Amazon EC2 (Elastic Compute Cloud). Coming from a ColdFusion background, moving into an asynchronous event loop can be challenging. Simple things like form processing can become complex workflows when even one step of Validation or Processing needs to be performed asynchronously. I've been trying to hone my asynchronous skills in a jQuery context where I can use jQuery Deferred objects to wrangle asynchronous processes. Last week, I created a jQuery plugin, jQuery.whenSync() which can be used to chain asynchronous tasks in serial; this week, I wanted to use that plugin to further explore form validation and processing in an asynchronous context.
To frame the conversation, let me just sum up my current synchronous FORM validation and processing workflow. As the client-side of web applications has become much more robust and feature-rich, we can start to create server-side code that is much more API-oriented. For this, I use an Exceptions-based workflow that makes multi-step processing extremely easy (pseudo-code):
try
--- if (field is invalid) Throw exception...
--- if (field is invalid) Throw exception...
--- if (field is invalid) Throw exception...
--- if (field is invalid) Throw exception...
--- processData()
catch( ExceptionType == 1 )
catch( ExceptionType == 2 )
catch( ExceptionType == 3 )
generateHTTPResponse()
In ColdFusion, this is easy since everything in ColdFusion (less CFThread) is blocking by default. Therefore, if any of the IF statements or data processing steps throws an error, it can be caught by one of the top-level Catch statements.
In jQuery or Node.js, where functionality may be executed in parallel, catching errors becomes much more complex. Luckily, jQuery has the Deferred object which allows for success-and-fail-based asynchronous processing. Taking the above pseudo-code and translating it over to use jQuery Deferred, we can get something that looks like this (pseudo-code):
whenSync()
--- if (field is invalid) reject( exception... )
--- if (field is invalid) reject( exception... )
--- if (field is invalid) reject( exception... )
--- if (field is invalid) reject( exception... )
--- processData()
fail
--- catch( ExceptionType == 1 )
--- catch( ExceptionType == 2 )
--- catch( ExceptionType == 3 )
always
--- generateHTTPResponse()
Here, rather than using jQuery.when() to allow all given callbacks to execute in parallel, I'm using the jQuery.whenSync() plugin to make sure asynchronous callbacks are executed in serial. Then, I'm moving the "catch" statements to the fail() callback. And, finally, I'm moving the HTTP Response generation to the always() callback where it can be executed whether or not the request validation and processing succeeded or failed.
Now that we understand the translation of synchronous validation and processing onto an asynchronous platform, let's take a look at some demo code. In the following demo, I am processing a fake FORM submission - validating each field, processing the data, and then generating a fake HTTP response - all in a workflow that plays nicely with asynchronous methods.
<!DOCTYPE html>
<html>
<head>
<title>Using jQuery $.whenSync() For Asynchronous Form Processing</title>
<!-- Include jQuery and the whenSync() plugin. -->
<script type="text/javascript" src="../jquery-1.7.1.js"></script>
<script type="text/javascript" src="./jquery.whensync.js"></script>
<script type="text/javascript">
// Create a fake data post (to validate and process).
var form = {
id: 4,
name: "Tricia",
age: 42
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// -------------------------------------------------- //
// -------------------------------------------------- //
// Create a demo API response to explore the use of
// whenSync() to handle validation in an asynchronous
// fashion.
var apiResponse = {
statusCode: 200,
statusText: "OK",
data: "",
error: ""
};
// Var values that will be produced *during* the asynchronous
// validation process. This way, as they get set during the
// validation, they can later be used during the always()
// set of callbacks.
var user = null;
// Perform our asynchronous API processing workflow.
var process = $.whenSync(
// Validate the user ID.
function( deferred ){
// Check to see if the ID can be found in our
// database -- Clearly, this is just for DEMO!!!
if (form.id !== 4){
// Not found!
return(
deferred.reject(
"NotFound",
"The record could not be found."
)
);
}
// If we made it this far, the user was found -
// store a reference for later use.
user = {
id: 4,
name: "Sarah",
age: 37
};
// Resolve the validation step.
deferred.resolve();
},
// Validate then user name.
function( deferred ){
// Check to see if there was a length provided.
if (form.name.length === 0){
// Name is required.
return(
deferred.reject(
"BadRequest",
"The name value is required."
)
);
}
// If we made it this far, name is valid.
deferred.resolve();
},
// Validate the user age.
function( deferred ){
// Check for a valid number.
if (
isNaN( form.age ) ||
(form.age < 0)
){
// Age is invalid.
return(
deferred.reject(
"BadRequest",
"The age value must be a positive number."
)
);
}
// If we made it this far, age is valid.
deferred.resolve();
},
// Once the validation is done, if everything went
// according to plan, then we should be able to process
// the request further.
//
// NOTE: We are performing this as part of the whenSync()
// callback list *rather* than the DONE() callback since
// there is a possibility that this part of the process
// will ALSO create exceptions.
function( deferred ){
// Update the user record with the valid data.
user.name = form.name;
user.age = form.age;
// Resolve the entire validate process.
deferred.resolve();
}
);
// If the entire validation and subsequent processing (last
// step in the above workflow) has gone correctly, then our
// API response should be successful.
process.done(
function(){
// Adjust the API response.
apiResponse.data = {
id: user.id,
name: user.name,
age: user.age
};
}
);
// Once the processing is done, we can try to "Catch" any
// errors that were rejected during the validation phase.
process.fail(
function( type, message ){
// Check to see which type of error we are "Catching."
switch( type ){
case "NotFound":
// Adjust the API response.
apiResponse.statusCode = 404;
apiResponse.statusText = "Not Found";
apiResponse.error = message;
break;
case "BadRequest":
// Adjust the API response.
apiResponse.statusCode = 400;
apiResponse.statusText = "Bad Request";
apiResponse.error = message;
break;
// If none of the other case statements caught
// the erorr, then something unexpected must
// have happened.
default:
// Adjust the API response.
apiResponse.statusCode = 500;
apiResponse.statusText = "Server Error";
apiResponse.error = "Ooops! Not good!";
break;
}
}
);
// At this point, whether the validation and form processing
// was SUCCESSFUL or a FAILURE, we should have an updated API
// response object. Now, all we need to do is stream it back
// to the client.
process.always(
function(){
// When creawting the HTTP response body, we need to
// check to see if we are returning an error or a
// success.
if (apiResponse.error){
// Fail.
var responseData = {
code: apiResponse.statusCode,
error: apiResponse.error
};
} else {
// Success.
var responseData = apiResponse.data;
}
// Serialize the response for streaming to client.
var responseBody = JSON.stringify( responseData );
// Build the HTTP response content.
console.log( "StatusCode:", apiResponse.statusCode );
console.log( "StatusText:", apiResponse.statusText );
console.log( "Content-Type:", "application/json" );
console.log( "Content-Length:", responseBody.length );
console.log( "Body:", responseBody );
}
);
</script>
</head>
<body>
<!-- Left intentionally blank. -->
</body>
</html>
As you can see from the code, we are making use of jQuery Deferred methods: fail() and always(). We are not making use of the done() method! This is an important choice since our form "processing" may throw errors even after the data has been deemed valid (think database failure?). As such, we want our fail() method to handle more than just the validation aspects - we want it to catch errors raised through the entire workflow. This requires us to put our traditional "done" code into the last step of the jQuery.whenSync() callbacks.
When we run the above code, we get the following fake HTTP response in our console:
StatusCode: 200
StatusText: OK
Content-Type: application/json
Content-Length: 33
Body: {"id":4,"name":"Tricia","age":42}
To see the "Catch" portion of the code in action, take a look at the above video.
My understanding of server-side processing is always changing. Even when I get to something that I am quite happy with, I read something like the REST API Design Rulebook by Mark Masse, and realize that my approach needs to be refined even further. Take that journey and put it in an asynchronous context like jQuery or Node.js, and suddenly, you're re-learning how to walk. Luckily, with objects like Deferred and plugins like jQuery.whenSync(), validation and processing practices can, more or less, be translated over with just a little tweaking.
Want to use code from this post? Check out the license.
Reader Comments