Tuesday, January 22, 2013

I $.Promise - Part 3 - How to Make Your Own jQuery Promises

In Part 1 and Part 2 I talked about why promises exist and what you can do with them, but the true power of Promises cannot be realized until you start creating them yourself.  They're especially handy for points of user interactions, animations and blended AJAX operations.

The basic form

The basic form follows four basic steps when creating your own promise/Deferred implementation:
  1. Create the Deferred object.  I use the word Deferred here on purpose as this object should be retained internal to your function or object only, it should never be passed to the consumer.  In this way, no one can muck with the resolution or rejection of your promise and keeps your separations clear.
  2. Do some async stuff.  Whether it's an AJAX call, animating or getting feedback from the user.
  3. Resolve or Reject your Deferred.  At some point this thing needs a result.  Your method controls when and with what it's resolved or rejected.
  4. Return a Promise.  A promise is a deferred without the capabilities of resolving or rejecting.
// Totally contrived example of showing an edit form in a modal in Backbone.
function modalEdit(model) {
    // 1. Create the Deferred object.
    var editing = $.Deferred();

    // 2. Do some async stuff.
    var editView = new ModalEditView({ model: model, el: $('#modalSpot') });
    editView.render();

    // 3. Resolve or reject your deferred.
    model.on('sync', function () {
        editing.resolve(modal); // Resolve on save.
    });
    editView.on('cancel', function () {
        editing.reject(); // Reject on dialog cancel.
    });

    // 4. Return a Promise.
    return editing.promise();    
}


Your core Deferred object control functions are:

Deferred.resolve(args) - Executes 'done' handlers, passing args.
Deferred.reject(args) - Executes 'fail' handlers, passing args.
Deferred.notify(args) - Executes 'notify' handlers, passing args.

That's really all you need to get going.  If you'd like to see why you might want to do this, read on!

Putting it to use: An advanced use case

I recently did some work related to displaying a hierarchical "family tree" in SVG, driven by Raphael and Backbone (that's another post).

Prior to displaying the tree, there was a variable series of checks and prompts that had to be performed, all asynchronous.

It's a mess, and don't focus on it too hard, but the logic I came up with to understand this looks like this:

// Call create endpoint (get guid)
    // If NOT Affiliate Tree
        // Call get endpoint with guid
            // Render
    // Else (Affiliate Tree)
        // When done, check to see if user needs to be warned of billing
            // If Needing to warn, Prompt user
                // If User Accepts
                    // Post to bill
                        // Billing success, create tree
                            // Call get endpoint with guid
                                // Render
                // Else (User Declines (do nothing))
            // Else (No warning)
                // Post to bill
                    // Billing success, create tree
                        // Call get endpoint with guid
                            // Render
 
Now, try and think how you'd do this with callbacks.  Okay, now stop because your brain will explode.

Here's what my overridden fetch method on my Backbone model looked like in the end.  Please note:
  1. This method is pure workflow, no implementation details.
  2. This method returns a promise, so as far as calling fetch goes it's exactly the same as a native fetch implementation (minus the args).
  3. This methods represents variable amounts of async events comprised of AJAX operations and user prompts.
  4. There's no callbacks being passed into my business logic functions, meaning what it means to "promptWarnTree" doesn't care about what happens next, that's the job of the workflow to decide.  The "promptWarnTree" function just lets the workflow know when it's done.
  5. If at any point any promise is rejected, the entire flow ends and the tree is not shown.

fetch: function () {
  var orchestrating = createTree().then(function (type) {
   if (type === 'affiliate') {
    return checkWarnTree().then(function (warn) {
     if (warn) {
      return promptWarnTree().then(function () {
       return billTree();
      });
     }
    });
   }
  });
 
  return orchestrating.then(function () {
   return getTree();
  });
 }

I won't walk you through how this logic all works (for that check out my other posts on promises in this series), but the take-away should be how expressive and compact it is.

Specific implementation examples

In the scenario above, some methods are simple wrappers for other models or normal AJAX calls, but some are user interactions, such as asking the user if they want to accept the billing charges:

function promptWarnTree () {
    // I like to name all of my promise vars with "-ing" suffixes.  Makes things read nicely.
    var prompting = $.Deferred();

    // modalConfirm doesn't implement the Promise API
    // params: message, success, failure
    modalConfirm("Are you sure you wish to incur these charges?", prompting.resolve, prompting.reject);

    return prompting.promise();
}

Passing a value to attached callbacks

Notice how checkWarnTree's done callback has a "warn" parameter which tells the workflow of the result?  Passing a value here is easy via Deferred.resolve() and Deferred.reject() argument passing:

function createTree() {
    var creating = $.post('...').then(function (resp) {
        // In reality this was more complex, but the idea here is that creating is not the result of $.post(), it's a trimmed down version of the response or a derivative value.  In this way, the callbacks attached to creating do not get the full response object, merely the result I want them to have.
        return $.Deferred().resolve(resp.warn);
    });
    return creating;
}

Deferred.resolveWith() and Deferred.rejectWith() are also available and are used to pass a context to the callback as well as args.  I use them infrequently.

Closing

Creating your own promises is a great way to separate concerns in your application, make something that foolishly doesn't implement the promise API do so, and build up deep, long or otherwise variable workflows.

No comments: