Tuesday 26 November 2013

Rolling your own confirm mechanism using Promises and jQuery UI

It is said that a picture speaks a thousand words. So here's two:

That's right, we're here to talk about the confirm dialog. Or, more specifically, how we can make our own confirm dialog.

JavaScript in the browser has had the window.confirm method for the longest time. This method takes a string as an argument and displays it in the form of a dialog, giving the user the option to click on either an "OK" or a "Cancel" button. If the user clicks "OK" the method returns true, if the user clicks "Cancel" the method returns false.

window.confirm is wonderful in one way - it has a simple API which is easy to grok. But regardless of the browser, window.confirm is always as ugly as sin. Look at the first picture in this blog post; hideous. Or, put more dispassionately, it's not terribly configurable; want to change the button text? You can't. Want to change the styling of the dialog? You can't. You get the picture.

Making confirm 2.0

jQuery UI's dialog has been around for a long time. I've been using it for a long time. But, if you look at the API, you'll see it works in a very different way to window.confirm - basically it's all about the callbacks. My intention was to create a mechanism which allowed me to prompt the user with jQuery UI's tried and tested dialog, but to expose it in a way that embraced the simplicity of the window.confirm API.

How to do this? Promises! To quote Martin Fowler (makes you look smart when you do that):

"In Javascript, promises are objects which represent the pending result of an asynchronous operation. You can use these to schedule further activity after the asynchronous operation has completed by supplying a callback."

When we show our dialog we are in asynchronous land; waiting for the user to click "OK" or "Cancel". When they do, we need to act on their response. So if our custom confirm dialog returns a promise of a boolean (true when the users click "OK", false otherwise) then that should be exactly what we need. I'm going to use Q for promises. (Nothing particularly special about Q - it's one of many Promises / A+ compliant implementations available.)

Here's my custom confirm dialog:


/**
  * Show a "confirm" dialog to the user (using jQuery UI's dialog)
  *
  * @param {string} message The message to display to the user
  * @param {string} okButtonText OPTIONAL - The OK button text, defaults to "Yes"
  * @param {string} cancelButtonText OPTIONAL - The Cancel button text, defaults to "No"
  * @param {string} title OPTIONAL - The title of the dialog box, defaults to "Confirm..."
  * @returns {Q.Promise<boolean>} A promise of a boolean value
  */
function confirmDialog(message, okButtonText, cancelButtonText, title) {
    okButtonText = okButtonText || "Yes";
    cancelButtonText = cancelButtonText || "No";
    title = title || "Confirm...";

    var deferred = Q.defer();
    $('<div title="' + title + '">' + message + '</div>').dialog({
        modal: true,
        buttons: [{
            // The OK button
            text: okButtonText,
            click: function () {
                // Resolve the promise as true indicating the user clicked "OK"
                deferred.resolve(true);
                $(this).dialog("close");
            }
        }, {
            // The Cancel button
            text: cancelButtonText,
            click: function () {
                $(this).dialog("close");
            }
        }],
        close: function (event, ui) {
            // Destroy the jQuery UI dialog and remove it from the DOM
            $(this).dialog("destroy").remove();
            
            // If the promise has not yet been resolved (eg the user clicked the close icon) 
            // then resolve the promise as false indicating the user did *not* click "OK"
            if (deferred.promise.isPending()) {
                deferred.resolve(false);
            }
        }
    });

    return deferred.promise;
}

What's happening here? Well first of all, if okButtonText, cancelButtonText or title have false-y values then they are initialised to defaults. Next, we create a deferred object with Q. Then we create our modal dialog using jQuery UI. There's a few things worth noting about this:

  • We're not dependent on the dialog markup being in our HTML from the off. We create a brand new element which gets added to the DOM when the dialog is created. (I draw attention to this as the jQuery UI dialog documentation doesn't mention that you can use this approach - and frankly I prefer it.)
  • The "OK" and "Cancel" buttons are initialised with the string values stored in okButtonText and cancelButtonText. So by default, "Yes" and "No".
  • If the user clicks the "OK" button then the promise is resolved with a value of true.
  • If the dialog closes and the promise has not been resolved then the promise is resolved with a value of false. This covers people clicking on the "Cancel" button as well as closing the dialog through other means.

Finally we return the promise from our deferred object.

Going from window.confirm to confirmDialog

It's very simple to move from using window.confirm to confirmDialog. Take this example:


if (window.confirm("Are you sure?")) {
    // Do something
}

Becomes:


confirmDialog("Are you sure?").then(function(confirmed) {
    if (confirmed) {
        // Do something
    }
});

There's no more to it than that.

And finally a demo...

With the JSFiddle below you can create your own custom dialogs and see the result of clicking on either the "OK" or "Cancel" buttons.

Monday 4 November 2013

TypeScript: Don't forget Build Action for Implicit Referencing...

As part of the known breaking changes between 0.9 and 0.9.1 there was this subtle but significant switch:

In Visual Studio, all TypeScript files in a project are considered to be referencing each other

Description: Previously, all TypeScript files in a project had to reference each other explicitly. With 0.9.1, they now implicitly reference all other TypeScript files in the project. For existing projects that fit multiple projects into a single projects, these will now have to be separate projects.

Reason: This greatly simplifies using TypeScript in the project context.

Having been initially resistant to this change I recently decided to give it a try. That is to say I started pulling out the /// <reference's from my TypeScript files. However, to my surprise, pulling out these references stopped my TypeScript from compiling and killed my Intellisense. After wrestling with this for a couple of hours I finally filed an issue on the TypeScript CodePlex site. (Because clearly the problem was with TypeScript and not how I was using it, right?)

Wrong!

When I looked through my typing files (*.d.ts) I found that, pretty much without exception, all had a Build Action of "Content" and not "TypeScriptCompile". I went through the project and switched the files over to being "TypeScriptCompile". This resolved the issue and I was then able to pull out the remaining /// <reference comments from the codebase (though I did have to restart Visual Studio to get the Intellisense working).

Most, if not all, of the typing files had been pulled in from NuGet and are part of the DefinitelyTyped project on GitHub. Unfortunately, at present, when TypeScript NuGet packages are added they are added without the "TypeScriptCompile" Build Action. I was going to post an issue there and ask if it's possible for NuGet packages to pull in typings files as "TypeScriptCompile" from the off - fortunately a chap called Natan Vivo already has.

So until this issue is resolved it's probably a good idea to check that your TypeScript files are set to the correct Build Action in your project. And every time you upgrade your TypeScript NuGet packages double check that you still have the correct Build Action afterwards (and to get Intellisense working in VS 2012 at least you'll need to close and re-open the solution as well).