Creating Asynchronous Alerts, Prompts, And Confirms In AngularJS
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.
Want to use code from this post? Check out the license.
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
@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.).
"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);
@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.
Great post and better comments!
@Boris,
Thanks a lot :D
@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...
@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.
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.
@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.
@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.
Hi Ben! Fun experiment :) This is very loosely related, but this post reminded me of a nice little notification app design used in the non-trivial AngularJS demo, "angular-app." It implements i18n and services track notifications for the current route and the next route. You can explore the relevant services in this directory: https://github.com/angular-app/angular-app/tree/master/client/src/common/services
@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.
@Ben,
Yes, I definitely dig the structural grouping of files. I'm on board with this:
https://docs.google.com/document/d/1XXMvReO8-Awi1EZXAXS4PzDzdNvV6pGcuaF4Q9821Es/pub
This 3-part blog series also has some good thoughts:
http://www.artandlogic.com/blog/2013/05/ive-been-doing-it-wrong-part-1-of-3/
I haven't had a chance to take a closer look at ng-boilerplate recently, but it seems well worth a little study as well...
@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!
Great example but I still wish you could cut down on the blank lines in your code:
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
Maybe everyone else loves all those lines but at least for me it makes it a pain to read when things are so spaced out.
Thanks
Thanks for sharing this example! I actually used your code on a legacy project I was migrating from pure JS to angular. The problem was that on Firefox, when I used native prompt and native confirm, I was getting errors for [$rootScope:inprog]. I made a custom factory with your code and the setTimeout solution brought by @Willem and, voilà, everything works again.
If you want to see what I've done, it is on https://github.com/joaomoraes/angular-asyncLib.js