Angular Toast Notification Service

Home / Angular Toast Notification Service

I’ve been using the jQuery plug-in “toastr” for quite some time as a basic growl/notification system. As a basic service, it’s dead simple to wire up. If you don’t know what this plug-in does, check out the demo of it.

But, I found that I needed more interactivity than basic hide/show messaging scenarios. I needed to be able to render full templates inside of the toast messages to allow for interactivity bound to DOM events.


A basic toastr service looks like this:

(function () {
    var toastrOptions = {
        positionClass: 'toast-bottom-right'
    };

    toastr.options = toastrOptions;

    var notificationService = function () {
        var success = function (message) {
            toastr.success(message || 'DefaultSuccess');
        },

        error = function (message) {
            toastr.error(message, "GeneralError");
        },

        info = function (message) {
            toastr.info(message);
        };

        return {
            success: success,
            error: error,
            info: info
        };
    };

    module.config
    angular.module("long2know.services")
        .factory('toastService', toastService)
})()

You can see that it calls the globally scoped ‘toastr’ object to simply display success, error, and info messages. Most of the time, this is all I needed. What I needed recently though was reminiscent of the Angular-ui bootstrap’s $modal service. I needed to render a template, with scope, and pass the compiled element to toastr. toastr, fortunately, supports rendering full HTML via either an HTML string or jQuery DOM element.

To get the ball rolling, I used angular-ui’s bootstrap modules as a guide. I’ve seen other implementations of using toastr which are rolled into a directive. This just doesn’t seem right to me. Just like a modal dialog, the toast isn’t, or shouldn’t, be attached to my main view’s DOM at all. I expect it to be created and rendered completely independently. In this regard, keeping this is a service makes the most sense in my mind.

Keeping in-line with the previous dialogService that I’ve blogged about, I wanted my toastService to have the same functionality and similar form. IE – I’ll pass in a template name, controller name, and let the service work its magic. To facilitate this, I kept the same basic core functionality as I showed in the code snippet above, but added a 4th method called “render.”

This is where things got tricky. I pass in an “options” object that contains a templateUrl, controller (by name), controllerAs (if desired), and a resolves object. This resolves object is just like the $modal services resolver. The objects are resolved and added to the scope of the toast.

One key thing about the resolves is that I utilize these resolves in order to pass in callback functions. This allows easily attaching, for example, a click event in the template to a controller event that has a hook back to the resolved (parent) method. This is what gives me the functionality that I was looking for. Ultimately, I can have a toast that is fully interactive and can call back to methods in the originating controller that requested the toast to begin with. (phew!)

You can see the full source of the render method below. I’ve also included a working sample. The points of interest for me where grabbing the template from templateCache, generating a new scope, pushing that scope, essentially, into a new controller generated by the $controller service, and finally using the $compile service to render everything down to a DOM element. This DOM element is passed to the jQuery toastr plugin.

The toast object that is returned by the toastr plugin provides a handle to itself just like any other jQuery element. This allows for providing generalized access to the toast element, removing it, etc. I’ve also attached an event handler to detect when the jQuery object is destroyed to properly handle scope disposal.

render = function (options) {
    var
        returnSvc = {
            $toast: null,
            close: null
        },
        tplContent = $templateCache.get(options.templateUrl),
        scope = $rootScope.$new(), ctrlInstance, ctrlLocals = {}, resolveIndex = 0,
        promisesArr = [];

    // Define promises that we want to resolve
    angular.forEach(options.resolves, function (value) {
        if (angular.isFunction(value) || angular.isArray(value)) {
            promisesArr.push($q.when($injector.invoke(value)));
        }
    });

    // Resolve any resolves for which we gathered promises
    var resolvePromise = $q.all(promisesArr);

    // Once we finish resolving, get busy!
    resolvePromise.then(function resolveSuccess(vars) {
        //controllers
        if (options.controller) {
            ctrlLocals.scope = scope;
            ctrlLocals.$scope = scope;

            // Set the resolves we got back as members on the controller's locals
            angular.forEach(options.resolves, function (value, key) {
                ctrlLocals[key] = vars[resolveIndex++];
            });

            // Now create our controller with its locals
            ctrlInstance = $controller(options.controller, ctrlLocals);

            // If controllerAs was passed in as an option, set it.
            if (options.controllerAs) {
                scope[controllerAs] = ctrlInstance;
            }
        }

        // Compile our DOM element
        var angularDomEl = $compile(tplContent)(scope);

        // Instantiate the $toast
        var toast = toastr.info(angularDomEl, '', toastrOptions);
        returnSvc.toast = toast;
        
        toast.on('$destroy', function () {
            scope.$destroy();                      
        });
        
        // Create a cloase method accessible by the return object                   
        returnSvc.close = function () {
                toast.fadeOut(500, function() { $(this).remove(); });
        };
    });

    return returnSvc;
};

And in my primary view’s controller, I can instantiate a “complex” toast message in this manner:

var message = "I'm a complex toast.";
var toast = toastSvc.render({
    templateUrl: 'toastTemplate.html',
    controller: 'toastCtrl as tc',
    resolves: {
        toastData: function () {
            return {
                closeToast: function () {
                    closeToast(toast);
                },
                message: message,
                sharedData: vm.sharedData
            };
        }
    }
});

Note the passed in parameters. The “resolves” are what get injected into the toast’s controller/scope. Having what I called “toastData” in the list of resolves means that “toastData” will be injected simply by inference, which is a very handy mechanism, calling $inject.

If you run the demo and view the console, you’ll see that I’m passing around values via a shared object in the resolves called “sharedData.” I did this to illustrate that the scoped callbacks do indeed work and that the toast can share/manipulate a data object with its calling parent controller. I have the calling controller logging vm.sharedData.time, which is what the toast sets based on the current time. It will look something like this in the console:

Start.
Return data: 1440452014840
Return data: 1440452018944
Return data: 1440452020432
Return data: 1440452020912
Return data: 1440452021729
Return data: 1440452035848
Return data: 1440452037128

And there you have it. A Codepen demo is below and the source for the demo can be found on Github.

Leave a Reply