AngularJS Tip: Adding App-Wide Messaging

Yesterday, someone left a comment in my Angular Best Practices post, asking the following question (paraphrasing):

If a promise is rejected during the UI Router resolve, how do you let the user know?

I thought about it a bit, and decided to answer this question instead:

What is a quick and easy way to enable app-wide messaging?

By “app-wide messaging” I mean the ability for any module in the app to display a message to the user. So if an error happens in a service, or an error happens in a directive, there should be a consistent way to tell the app to display a message to the user. I’m not a fan of using dialogs or modals for this, so my solution was to use an AlertΒ to display messages. I came up with a solution that feels pretty simple (note the solution below has been cleaned up, thanks to commenters on this post and on reddit/angularjs). Basically, there will be a new directive (called app-messages) and a new service (appMessagesService). When a message needs to be shown, a module will call appMessageService.addMessage(). The app-messages directive has a reference to the list of messages maintained by the service. So when a message is added to the service, the app-messages directive will display an alert to the user. If you want to see this code in the angularBPSeed project, check out the app.js file, services/locationLookupService.js, and all the files in components/appMessages. I’m also going to explain the solution in more detail below.

The first thing we’ll do is create the directive, called appMessages (app-messages in the html file). I’m not going to show the full contents of all of the directive files (you can see the full files in the seed project), but I’ll include the main logic from the appMessagesCtrl below:

    
var self = this;

self.alerts = appMessagesService.getMessages();

/**
 * closes the alert and removes it from the list of alerts.
 * @param alertIndex
 */
self.closeAlert = function (alertIndex) {
    appMessagesService.removeMessage(alertIndex);
};

The alerts array is used to store the messages to display. This array is actually a reference to the array stored within the appMessagesService. The service will be explained below, but it contains logic for adding and removing messages, as well as getting a reference to the list of messages. When the service modifies its internal array, the directive’s array will also change (since JavaScript objects are ‘pass by reference’). And since Angular is ‘watching’ our self.alerts array, when it is changed, our view will be updated to reflect those changes.

The html for the directive looks like this:

    
    
    <alert ng-repeat="alert in appMessagesCtrl.alerts" type="alert.type"
        close="appMessagesCtrl.closeAlert($index)">{{alert.msg}}
    </alert>

We are using the alert directive from the Angular UI Bootstrap project. The type attribute specifies the type of alert to display (warning, success, etc), the close attribute is the method to call when the ‘x’ is clicked, and alert.msg is the message text to display.

Next, lets look at appMessagesService:

var self,
    messages = [];

self.addMessage = function (message) {
    messages.push({
        type: message.type,
        text: message.text
    });
};

self.getMessages = function() {
    return messages;
};

self.removeMessage = function(messageIndex) {
    messages.splice(messageIndex, 1);
};
 

You’ll notice that the service has a private reference to a messages array. It also exposes methods to add a message, remove a message, and get the list of message. Unlike previous version of this code, the $rootScope and events are no longer necessary. Everything is encapsulated within the service.

An example of using the service to send a message (from locationLookupService.js):

    
    appMessagesService.addMessage({
        type: 'warning',
        text: "Error looking up location with locId: " + locId
    });

As you can see, the code is fairly simple. We use the appMessagesService to add our message. That’s it..nothing to it!

Finally, a snippet from the index.html, to show where the directive is added to the html:

    <div app-messages></div>
<div class="container" ui-view="page"></div>

You’ll notice that I added the appMessages directive to the div ABOVE the ui-view. If you are unfamiliar with Angular UI Router, the ui-view is similar to ng-view. In other words, the ui-view div is where the views for our SPA live. So, by adding the app-messages directive above the ui-view, it means the app-messages directive can be shown at the top of every view within our app. Sorta like a global message center. Obviously, if you only need to display messages within certain views, you can add the appMessages directive to those views instead of making it a global directive like I did in the seed project.

Before my next post, I think I need to find a plugin to make code snippets look nice πŸ™‚

 

Edit: Changed the $emit to a $broadcast.

Edit 2: Per a comment here on the blog, and on reddit, I added a appMessagesService, which is used to send the messages. Post has been updated to reflect this change.

Edit 3: Per further comments and discussion, I’ve removed the dependency on rootScope and event broadcasting.

9 thoughts on “AngularJS Tip: Adding App-Wide Messaging

  1. Why would one do this instead of just creating an alert service? Could you add a pro/con list that compares this approach to using just a service? How does this approach affect testing?

    Like

    • Actually, using a service to send the message is a good idea. I’ve updated the seed project to add a message sending service, as that does make the code cleaner, and keeps the details about message sending hidden from modules that need to send a message. Was this what you were thinking of when you mentioned an alert service? Or did you have something else in mind?

      As for testing..that is still my weak area. I needly to greatly improve my testing knowledge/ability. Working on that now.

      Like

      • Not exactly. I just use toastr to present alert messages to my users, and use a service similar to this example by John Papa to wrap my toastr calls: https://github.com/johnpapa/ng-demos/blob/7d2be341a8ccfd165ec9504c73c611edb9e3fa98/modular/src/client/app/blocks/logger/logger.js (with my own version adding localization using Angular Translate).

        No events or fiddling with the $rootScope necessary. I just wondered why you went with using events instead of just a plain old service? Is there some benefit using one over the other, or is it just plainly two ways of getting the same job done?

        Like

        • Honestly, for some reason I just didn’t think about using a service initially. I’m not sure why. And I haven’t done a lot of event-based programming in my career, so I’m not sure why events were the first thing I thought of. But using an event simple like a simple way to solve this issue. I’m still having trouble imagining how to get rid of the rootScope to use only a service. But I’m trying to think it about it to understand how a pure service solution would work. I took a look at the logger service you mentioned, but it didn’t spark any ideas for me.

          Like

            • No problem, glad to help – although it wasn’t my initial intention to question your method. πŸ™‚ I was merely being curious as to whether there was some specific reason (such as optimization) to using events over a pure service. I’d think using the $rootScope and events would make it slightly more difficult to test, although I’m not exactly certain of this.

              Like

              • While one of the reasons I started the blog was to present some ideas, the other big hope was to get feedback when one of the ideas could be improved. I had read more than 1 post suggesting the use of services instead of events, but I never quite figured out what they meant. Thanks to the feedback on this blog post, I think I finally understand (assuming my solution is ‘correct’). The weird thing is that I have used services to maintain state, so I’m not sure why I had such a mental hangup when trying use a service in place of pub/sub. I know events still have their place, but now at least I know how both events and services can be used to solve the same problem.

                I kept updating the original text of this post today as I changed the solution. Now I wonder if I should create a another post, showing how the two solutions compare. Early readers might not see this new solution, and newer readers might not have seen what the original code. And maybe showing how the code evolved from events to a service might be helpful to someone.

                Like

    • Dave…thanks for the feedback. You are right, I forget to mention that in this article…I wasn’t sure if it would be confusing to bring UI Router resolving into the discussion. But I did add code in app.js to listen for the $stateChangeError event, so that I could hide the loading bar when the resolve failed. When I was updating the code, I noticed very quickly that the loading bar wasn’t going away when I rejected the resolve. Which then lead me to the $stateChangeError you mentioned πŸ™‚

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s