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 cf.Objective() 2012 (Minneapolis, MN) with:

Creating Asynchronous Alerts, Prompts, And Confirms In AngularJS

By Ben Nadel on

When you first get into JavaScript, using the native alert(), prompt(), and confirm() methods feels comfortable because they are "blocking" actions. This means that when those methods execute, the world stops and your code pauses until the user responds. The problem with this is that as user interfaces (UIs) get more complex and more richly designed, you often have to move to some sort of non-blocking form of these methods (such as Bootstrap modals). This can be a difficult transition. As such, I thought it would be a fun exercise to create promise-driven, non-blocking versions of alert(), prompt(), and confirm() in AngularJS so as to force an asynchronous mindset from the get-go.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Promises are guaranteed to be asynchronous. This means that even if the execution of the deferred value is blocking (as it will be in our case with the native alert methods), the resolution and rejection of the promise will happen in a future tick of the event loop (generally speaking). This allows our control flow to be consistent, regardless of how the deferred value is being evaluated.

In the following code, I'm simply defining three AngularJS factory values that wrap alert(), prompt(), and confirm() inside a promise. These versions of the native methods can then be injected into any of your other AngularJS components.

Since there is no divergent behavior with alert(), the alert() promise will always be resolved. Prompt() and confirm(), on the other hand, can be cancelled by the user, which will reject the promise.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="AppController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Creating Asynchronous Alerts, Prompts, And Confirms In AngularJS
  • </title>
  • </head>
  • <body>
  •  
  • <h1>
  • Creating Asynchronous Alerts, Prompts, And Confirms In AngularJS
  • </h1>
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.16.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope, alert, prompt, confirm ) {
  •  
  • // Use new, injected alert.
  • alert( "Hecks to the yea!" ).then(
  • function() {
  •  
  • console.log( "Alert accomplished" );
  •  
  • }
  • );
  •  
  •  
  • // Use new, injected prompt.
  • prompt( "Are you beginning to see the possibilities?", "Yes" ).then(
  • function( response ) {
  •  
  • console.log( "Prompt accomplished with", response );
  •  
  • },
  • function() {
  •  
  • console.log( "Prompt failed :(" );
  •  
  • }
  • );
  •  
  •  
  • // Use new, injected confirm.
  • confirm( "Has it come to this?" ).then(
  • function( response ) {
  •  
  • console.log( "Confirm accomplished with", response );
  •  
  • },
  • function() {
  •  
  • console.log( "Confirm failed :(" );
  •  
  • }
  • );
  •  
  •  
  • // This last console.log is here to demonstrate that the asynchronous
  • // alerts, prompts, and confirms will be rejected in a future tick of
  • // the event loop and will invoke logging AFTER this line of code.
  • console.log( "Testing completed." );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I define an asynchronous wrapper to the native alert() method. It returns a
  • // promise that will be resolved in a future tick of the event loop.
  • // --
  • // NOTE: This promise will never be "rejected" since there is no divergent
  • // behavior available to the user with the alert() method.
  • app.factory(
  • "alert",
  • function( $window, $q ) {
  •  
  • // Define promise-based alert() method.
  • function alert( message ) {
  •  
  • var defer = $q.defer();
  •  
  • $window.alert( message );
  •  
  • defer.resolve();
  •  
  • return( defer.promise );
  •  
  • }
  •  
  • return( alert );
  •  
  • }
  • );
  •  
  •  
  • // I define an asynchronous wrapper to the native prompt() method. It returns a
  • // promise that will be "resolved" if the user submits the prompt; or will be
  • // "rejected" if the user cancels the prompt.
  • app.factory(
  • "prompt",
  • function( $window, $q ) {
  •  
  • // Define promise-based prompt() method.
  • function prompt( message, defaultValue ) {
  •  
  • var defer = $q.defer();
  •  
  • // The native prompt will return null or a string.
  • var response = $window.prompt( message, defaultValue );
  •  
  • if ( response === null ) {
  •  
  • defer.reject();
  •  
  • } else {
  •  
  • defer.resolve( response );
  •  
  • }
  •  
  • return( defer.promise );
  •  
  • }
  •  
  • return( prompt );
  •  
  • }
  • );
  •  
  •  
  • // I define an asynchronous wrapper to the native confirm() method. It returns a
  • // promise that will be "resolved" if the user agrees to the confirmation; or
  • // will be "rejected" if the user cancels the confirmation.
  • app.factory(
  • "confirm",
  • function( $window, $q ) {
  •  
  • // Define promise-based confirm() method.
  • function confirm( message ) {
  •  
  • var defer = $q.defer();
  •  
  • // The native confirm will return a boolean.
  • if ( $window.confirm( message ) ) {
  •  
  • defer.resolve( true );
  •  
  • } else {
  •  
  • defer.reject( false );
  •  
  • }
  •  
  • return( defer.promise );
  •  
  • }
  •  
  • return( confirm );
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

This is a really minor post; mostly done for fun. But, getting into an asynchronous mindset is important because so much of the user interaction in web applications happens in an asynchronous maner. It's awesome that AngularJS' dependency injection framework makes it so easy to define asynchronous equivalents of native, blocking methods.




Reader Comments

Hi Ben great post, however:

These new methods actually aren't asynchronous. Only the promise handlers are asynchronous because they go trough an "async trampoline".

Your methods are still blocking.

See her for a demo / proof:
http://plnkr.co/edit/WvrBQiOt5KdbqV4LQhEJ?p=preview

Reply to this Comment

@Willem,

Correct - there's not way actually make the underlying native methods non-blocking (that I know of). If from the calling context, the workflow *is* asynchronous. If you look at the output in the console, you'll see that the various resolution/rejection callbacks were invoked *after* the rest of the code in the calling context.

So, while the native methods aren't actually non-blocking, from a workflow perspective, they are; which will make it seamless to swap out with some other non-blocking UI elements (ex. Bootstrap modal, jQuery UI modal, etc.).

Reply to this Comment

"from the calling context, the workflow *is* asynchronous"

That's incorrect, from a calling context these actually **are** blocking.
While you would expect them to be non blocking

Example:

alert( "Hecks to the yea!" ).then(
function handler() {
console.log( "Alert accomplished" );
}
);

console.log("does not get executed")
/* ^^ this log doesn't evaluate until after the alert is dismissed by the user
only the handler function gets executed asynchronous as it is postponed by the promise library */

You could solve this by postponing the alerts them self to the next tick by using setTimeout( ... , 0);

Reply to this Comment

@Willem,

True, putting the native method call inside of a setTimeout() would make it truly asynchronous. I guess I didn't think to do that because I didn't think it would make a practical difference from the calling context. One you promises are resolved/rejected asynchronously, the actual alert, prompt, and confirm executions didn't seem as relevant.

But, I am not trying to argue - you are correct - the native methods are still being invoked from a synchronous standpoint.

Reply to this Comment

@Willem,

I don't think the point is to be async just to be async, but to be non-blocking when you don't need to block or when there is tangible benefit by not blocking (which is almost always the case with UI programming). What would be the real gain of invoking the custom alert function asynchronously? It's not resource intensive, it just shows a message... The part of all of this that has the potential to really block is the part where the dialog is waiting for the user reply, and that IS solved buy using promises to handle the results. Job's done...

Calling alert asynchronously in this scenario solves a non-existent problem...

Reply to this Comment

@Ben

I used a similar approach in a project at work. The site is architectured as a single page application, with many routes. One thing that you should account for when using promises (or whatever async solution) in single page applications is that, by the time the promise is solved or rejected and its correspondent listeners invoked, the context (view) might have changed for whatever reason (back button pressed? route changed?). In that cause, most of the times the listeners could be ignored, discarded, not executed, specially if they deal with the now abandoned context.

I addressed that by devising a way to keep track of destroyed scopes and conditioning function invocations to their context not being destroyed. Whenever the context changes, the view changes and Angular destroys the scope. Any listener bound to the context that gets called later is just discarded.

Reply to this Comment

Well.. ACTUALLY Willem has a point.

I've read it quickly and did not notice native, blocking, dialog functions were called synchronously by the custom alert and dialog services. I guess I just assumed your solution was similar to mine (I used custom, non blocking UI components).

Anyways, yeah, Willem made a valid point. If your dialog services are going to make use of native, blocking functions like alert/prompt/confirm, and your objective is just do postpone the blocking action to after the current cycle, you should use setTimeout indeed.

I get that this was just an exercise and that what follows may not apply to it, but still, when it comes to UI components, imho, at least for the web, you should go all-in for non-blocking and use custom alert/prompt/confirm dialogs. You can still block the user interactions with the context by mimicking modal dialogs, which is often the case, but to block the thread itself has become a forbidden place in modern web UX for good reasons.

Reply to this Comment

@Diego,

Willem does have a valid point. And while I definitely agree with it in philosophy, I am not sure that it makes a practical difference in this kind of scenario.

The point I was trying to explore was not to make alerts non-blocking, but rather to make your code more asynchronous in "workflow" such that later on, down the road, when you DO need to swap out alert(), prompt(), etc. for some other truly non-blocking UI (like a modal window), your code is already designed to work that way.

Reply to this Comment

@Ben,

hi Ben.
nice post - thanks for sharing.
There is a way to make these - confirm, prompt - async by using timeouts, only then - the response from the confirm/prompt dialogs can be reffered by the promise objects.

Reply to this Comment

@Jamie,

Thanks - I'll take a look at the app. I *think* I started to read that book back when it was in "early release". I don't think I ever finished it. I'd love to get other people's take on this here AngularJS stuff; specifically, I see more people are changing the way the apps are organized - by feature, rather than by MVC.

Reply to this Comment

@Jamie,

Thanks for posting that first link! I had that open in a tab for a while but then lost all my tabs and couldn't find it again. I never got the chance to read it - I just remember at first glance that it looks really interesting. #HighFive!

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.