public
Last active

Using promises in a UI context

  • Download Gist
README.md
Markdown

The scenario:

  • We are writing a digital textbook-reading app.
  • Most of the time you have a "basic" license for your textbook, but one (and only one) of your computers can request an "enhanced" license.
  • You can only print from the computer with the enhanced license.

The problem statement:

  • We want a simple printAsync() method that we can attach to our click handler for a print button somewhere in the UI.

The complications:

  • If the user has the basic license, we need to check if it's possible for them to acquire the enhanced license:
    • If so, ask the user if they want to upgrade, and then continue the print operation if they agree.
    • If not, error out: printing is impossible.
  • Printing is an asynchronous operation, provided to us via window.cPlusPlusApis.print, that could fail (e.g. if the printer is out of paper).
  • Due to some horrible threaded C++ code, we get deadlocks if the user navigates through the book at the same time as the book is printing, so we need to block the UI while it prints.

The plan:

  • Use a bunch of promise methods that work together to encapsulate all this complexity into different layers.
  • The simplest of the bunch is the dialogs module: it just gives you some nice promise-returning, jQuery-UI-dialog-using versions of alert, confirm, and prompt. It exemplifies the use of deferreds to create promise-returning methods, even in situations where the asynchronous nature of the task is indeterminate (i.e. depends on the user clicking a button).
  • The drm module talks to the server (via Ajax) to figure out if a user's computer can upgrade to the enhanced license, or to perform the upgrade process itself. It exemplifies a few more advanced promise-consumption techniques.
  • The printingUI module uses the former two to create a (private) function, ensureEnhancedAsync, that carries out all the UI and business logic around upgrading to enhanced if at all possible. It exports a printAsync function, which makes use of ensureEnhancedAsync to do its very best to print, or error out to the user if it's not. Here we see the true power of error bubbling and promise piping.
  • Finally, some other part of the app will wire up printingUI's printAsync export to a button somewhere. That's the easy part ;).
dialogs.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
"use strict";
 
var Q = require("q");
var $ = require("jquery");
 
exports.alertAsync = function (text) {
var deferred = Q.defer();
$("<div />").text(text).dialog({
buttons: {
OK: deferred.resolve
}
});
return deferred.promise;
};
 
exports.confirmAsync = function (text) {
var deferred = Q.defer();
$("<div />").text(text).dialog({
buttons: {
OK: function () {
deferred.resolve(true);
},
Cancel: function () {
deferred.resolve(false);
}
},
close: function () {
deferred.resolve(false);
}
});
return deferred.promise;
};
 
exports.promptAsync = function (text) {
var deferred = Q.defer();
var $input = $("<input type='text' />");
$("<div />").text(text).append($input).dialog({
buttons: {
OK: function () {
deferred.resolve($input.val());
},
Cancel: function () {
deferred.resolve(null);
}
},
close: function () {
deferred.resolve(null);
}
});
return deferred.promise;
};
drm.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
"use strict";
 
var Q = require("q");
var $ = require("jquery");
var system = require("./system"); // Implementation not shown. It has a computerId export.
 
// Returns a promise for a boolean.
exports.getCouldUpgradeAsync = function (book) {
var jQueryPromise = $.get("/book/" + book.id + "/drm/status?computerId=" + system.computerId, "json");
var qPromise = Q(jQueryPromise);
// Pluck the isEnhancedAvailable property off of the JSON result. This `get` is
// a nice Q feature that is essentially sugar for
// `qPromise.then(function (o) { return o.isEnhancedAvailable; })`.
return qPromise.get("isEnhancedAvailable");
};
 
// Returns a promise with no fulfillment value (equivalent to a synchronous function that doesn't
// return anything). The returned promise will be fulfilled if the HTTP POST succeeds, meaning the
// enhanced license has been acquired for this computer. It will be rejected if the HTTP POST fails
// (e.g. server sends back 500 error), which we interpret as meaning that for some reason we
// couldn't assign an enhanced license to this computer.
exports.upgradeAsync = function (book) {
var jQueryPromise = $.post("/book/" + book.id + "/drm/acquire-enhanced-license?computerId=" + system.computerId);
var qPromise = Q(jQueryPromise);
// Convert a HTTP error into a meaningful error object.
return qPromise.catch(function () {
throw new Error("Could not acquire enhanced license for this computer.");
});
};
printingUI.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
"use strict";
 
var Q = require("q");
var dialogs = require("./dialogs");
var drm = require("./drm");
var spinner = require("./spinner"); // Implementation not shown; has start() and stop() methods.
 
// C++ has nicely tacked this method onto the window object for us.
// Signature: printImpl(bookId, pageNumber, onPrintDone(err))
// (For simplicity let's say the user can only print one page through our interface.)
var printImpl = window.cPlusPlusApis.print;
 
// Promisify the C++ method, which happily follows the Node-style callback-with-error-as-first-argument
// convention.
var printImplAsync = Q.denodeify(printImpl);
 
// Returns a promise that will be fulfilled if and only if the book becomes (or already is) an
// enhanced copy.
function ensureEnhancedAsync(book) {
if (book.isEnhanced) {
// Return an already-fulfilled promise (with no fulfillment value): we're good to go.
return Q();
} else {
// First see if we could possibly upgrade.
return drm.getCouldUpgradeAsync().then(function (couldUpgrade) {
// Now act on that knowledge: either give up if we can't, or upgrade if we can.
if (!couldUpgrade) {
// This will bubble up, resulting in the promise returned from ensureEnhancedAsync
// being rejected.
throw new Error("You cannot print from the basic version, and have already downloaded "
+ "the enhanced version on another computer. Use that computer instead.");
} else {
// Ask the user if they want to upgrade.
return dialogs.confirmAsync("Do you want to upgrade to enhanced?").then(function (result) {
// If they do, perform the upgrade.
if (result) {
return drm.upgradeAsync(book);
} else {
throw new Error("Since you have not chosen to upgrade, printing is not available.");
}
});
}
});
}
}
 
// The promise returned here will always be fulfilled, since we transform errors into user notifications.
// But, when the promise is fulfilled, you can be sure that the entire printing process is done, one way
// or another.
exports.printAsync = function (book) {
return ensureEnhancedAsync(book)
.then(function () {
return dialogs.promptAsync("What page would you like to print?").then(function (pageNumber) {
// If they gave us a page number, print it!
if (pageNumber) {
// Start up the spinner so as to block the UI and avoid deadlocks!
spinner.start();
return printImplAsync(book.id, pageNumber);
}
// If they cancelled out of this prompt, then this method has done its job, and we can
// just let it return a fulfilled promise.
});
})
.catch(function (error) {
// If we got an error, either in the ensureEnhancedAsync processing or bubbled up from
// printImplAsync, alert the user.
return dialogs.alertAsync(error.message);
})
.finally(spinner.stop); // Make sure to stop the spinner on either success or failure
// in order to unblock the UI.
};

I think it will look better for promises if it is kept flat like:

// Returns a promise that will be fulfilled if and only if the book becomes (or already is) an
// enhanced copy.
function ensureEnhancedAsync(book) {
    if (!book.isEnhanced) {
        // Return an already-fulfilled promise (with no fulfillment value): we're good to go.
        return Q();
    } 
    // First see if we could possibly upgrade.
    return drm.getCouldUpgradeAsync().then(function (couldUpgrade) {
        // Now act on that knowledge: either give up if we can't, or upgrade if we can.
        if( !couldUpgrade ){
            // This will bubble up, resulting in the promise returned from ensureEnhancedAsync
            // being rejected.
            throw new Error("You cannot print from the basic version, and have already downloaded "
                          + "the enhanced version on another computer. Use that computer instead.");             
        }
        // Ask the user if they want to upgrade.
        return dialogs.confirmAsync("Do you want to upgrade to enhanced?")
    }).then( function( result ) {
        // If they do, perform the upgrade.
        if( result ) {
            return drm.upgradeAsync(book);
        }
        else {
            throw new Error("Since you have not chosen to upgrade, printing is not available.");
        }
    });

}

This should work the same if I'm not mistaken. Right now it almost looks like no callback hell was solved after all as the code keeps growing on the right...

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.