Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with:

Using jQuery's Pipe() Method To Chain Asynchronous Validation Requests

By Ben Nadel on

The other day, I looked at using Stripe.com to accept PCI-compliant online payments without a merchant account. In that experiment, I performed a cross-domain AJAX request to validate the credit card (using Stripe.com's API) followed by a local AJAX request to validate the rest of the form data. To do this, I put the launch of the second AJAX request inside the success callback of the first AJAX request, manually chaining the dependent requests. This felt a bit sloppy, given the tools we have to work with today; as such, I wanted to see if I could use jQuery's .pipe() method to chain serial, asynchronous requests more elegantly.


 
 
 

 
  
 
 
 

Keeping with the credit card validation motif, I'm going to simulate a scenario in which we have three levels of possible failure during a form submission:

  1. Credit card validation.
  2. Local-server validation.
  3. Database insert.

NOTE: HTTP requests may also fail, but that will be handled explicitly by the AJAX callback methods.

Furthermore, since the credit card validation happens on a "third party server" (or rather, it did in my previous blog post), we don't have control over the response structure. As such, the credit card validation response will not be the same as the local server validation and insertion responses, respectively. We will have to use the chained callbacks to transform the credit card response, creating a uniform response structure.

Before we look at how these requests can be chained together using jQuery's pipe() method, let's look at the server-side code that will be producing the random responses for the demo.

credit_card.cfm (Our Credit Card Validation)

  • <!---
  • Create a default return value. Note that when a credit card
  • succeeed, there is no ERRORS key. We are doing this to simulate
  • non-symetric response structures for credit card validation.
  • --->
  • <cfif (randRange( 1, 10 ) % 2)>
  •  
  • <!--- Successful validation. --->
  • <cfset response = {} />
  • <cfset response[ "success" ] = true />
  •  
  • <cfelse>
  •  
  • <!--- Failed validation. --->
  • <cfset response = {} />
  • <cfset response[ "errors" ] = [] />
  • <cfset response.errors[ 1 ] = "Credit card failed to validate." />
  •  
  • </cfif>
  •  
  •  
  • <!--- Return a 200 response even if we have errors. --->
  • <cfheader
  • statuscode="200"
  • statustext="OK"
  • />
  •  
  • <!--- Return the API response. --->
  • <cfcontent
  • type="text/x-application-json"
  • variable="#toBinary( toBase64( serializeJSON( response ) ) )#"
  • />

When the credit card is validated, notice that the response doesn't have a consistent structure; if it succeeds, it has a "success" key; if it fails, it has an "errors" key. This will be different than the API responses that we have control over (given their location on our local server).

validation.cfm (Our Local Server Form Validation)

  • <!--- Create a default return value. --->
  • <cfset response = {} />
  • <cfset response[ "success" ] = true />
  • <cfset response[ "errors" ] = [] />
  •  
  • <!--- Return a 200 response. --->
  • <cfheader
  • statuscode="200"
  • statustext="OK"
  • />
  •  
  • <!--- Return the API response. --->
  • <cfcontent
  • type="text/x-application-json"
  • variable="#toBinary( toBase64( serializeJSON( response ) ) )#"
  • />

Our local form validation will always return a successful response for this demo. If the credit card validation succeeds, so will this step of the process. The next point of potential failure will come at the database level.

insert.cfm (Our Database Insertion Request)

  • <!--- Create a default return value. --->
  • <cfset response = {} />
  • <cfset response[ "success" ] = true />
  • <cfset response[ "errors" ] = [] />
  •  
  •  
  • <!--- Randomly decide to fail on insert. --->
  • <cfif (randRange( 1, 10 ) % 2)>
  •  
  • <cfset response.success = false />
  • <cfset response.errors[ 1 ] = "Unexcepted database error." />
  •  
  • </cfif>
  •  
  •  
  • <!--- Return a 200 response even if we have errors. --->
  • <cfheader
  • statuscode="200"
  • statustext="OK"
  • />
  •  
  • <!--- Return the API response. --->
  • <cfcontent
  • type="text/x-application-json"
  • variable="#toBinary( toBase64( serializeJSON( response ) ) )#"
  • />

Notice that the local server validation and the local database insert both return a uniform structure. Notice also that they return a "200 OK" response even if the server-side action fails. I'm keeping everything "200 OK" to simplify the demo. I've already examined the jQuery pipe() method as a means to change the Deferred resolution at runtime; but for this exploration, I didn't want to add the complexity of using appropriate HTTP status codes to define API responses.

Now that we see what the three levels of possible failure our, let's take a look at the code that pulls it all together in one serialized validation chain.

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Using Pipe() To Chain Asynchronous Validation</title>
  •  
  • <script type="text/javascript" src="../jquery-1.7.js"></script>
  • <script type="text/javascript">
  •  
  •  
  • // Get the remote credit card validation.
  • var authorization = $.ajax({
  • type: "post",
  • url: "./credit_card.cfm",
  • data: {},
  • dataType: "json"
  • });
  •  
  •  
  • // Chain the local validation to the credit card validation.
  • var localValidation = authorization.pipe(
  • function( response ){
  •  
  • // Check to see if the credit card validated.
  • // If so, then we want to proceed with our
  • // local, server-side validation.
  • if (response.hasOwnProperty( "success" )){
  •  
  • // Return the next validation promise.
  • return(
  • $.ajax({
  • type: "post",
  • url: "./validation.cfm",
  • data: {},
  • dataType: "json"
  • })
  • );
  •  
  • } else {
  •  
  • // Validation failed, return error response, but
  • // translate into a normalized response sturcture.
  • return({
  • success: false,
  • errors: response.errors
  • });
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // Chain the local validation to the insert.
  • var save = localValidation.pipe(
  • function( response ){
  •  
  • // Check to see if the local validation succeeded.
  • // If so, we'll try to insert the record.
  • if (response.success){
  •  
  • // Return the next validation promise.
  • return(
  • $.ajax({
  • type: "post",
  • url: "./insert.cfm",
  • data: {},
  • dataType: "json"
  • })
  • );
  •  
  • } else {
  •  
  • // Validation failed, return error response.
  • return( response );
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // Validate the entire chain.
  • save.then(
  • function( response ){
  •  
  • // Show validation results.
  • console.log( "Success:", response.success );
  • console.log( "Errors:", response.errors );
  •  
  • },
  • function( resolution ){
  •  
  • // Something went really wrong!!
  • console.log( "Something went wrong in a bad way." );
  • console.log( resolution );
  •  
  • }
  • );
  •  
  •  
  • </script>
  • </head>
  • <body>
  • <!-- Left intentionally blank. -->
  • </body>
  • </html>

This validation chain starts with the credit card validation. This response is then pipe()'d into the local validation request, which is then pipe()'d into the database insert request. Each call to jQuery's pipe() method either passes-on the failed response or, returns a XHR Promise for the next step in the validation chain. Not only does this allow our asynchronous requests to be performed in serial, it provides us with an encapsulated means to normalize the response structures at each step; notice that if the credit card validation fails, the error-only response is transformed into a structure that has both a "success" and "errors" key-set.

The nice thing about the jQuery Deferred's pipe() method is that you can return either an object or another Deferred object; this allows each step of the validation chain a chance to throw the entire chain into a "rejected" resolution, if necessary. This gives our final success and fail handlers a uniform way to react to any point of failure within the entire validation lifecycle.

Most of the time, we don't care that events are asynchronous; but, if one asynchronous event depends on the response of another asynchronous event, things get more complicated. We can end up writing "boomerang" code that nests one callback inside another callback inside another callback (etc.). By using the jQuery Deferred's pipe() method, we start to serialize our asynchronous actions in a way that keeps our code much more readable and elegant.




Reader Comments

Thanks Ben. I wasn't even aware of the deferred.pipe() method and its been available for almost a year now. I am excited to use this method to clean up some old sloppy code of mine. Keep up the great posts!

Reply to this Comment

@Andrew,

Cool man! The Deferred stuff, in general, is pretty awesome. Even if you use nothing more than AJAX calls, I find the way the callbacks get defined to be very pleasing - much more so than putting the success/error callbacks in the AJAX config object.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.