Toastr code review

toastrToastr is a small notifications library written in javascript / jquery.  It’s a nice utility web developers can use to alert users to certain events, tasks, progress, errors, etc…

In in this review I’m going to talk about the single responsibility and open-closed principals.  I’ll also pitch the idea that in object oriented programming you don’t need “if” statements.

Before we dive into this, let me put the brakes on for second.

At ~400 lines of code this library is very tiny.  It’s core function is to show a pretty notification.  It’s going to be difficult not to come across as pedantic in reviewing a code library this small.  Code reviews are often thought of as an exercise in identifying potential problems with a given code base.  Code reviews can be much more than this, they are a great way of learning and sharing ideas even if the code is very small and trivial.

The source code used for this review can be found at the following url: https://github.com/CodeSeven/toastr/blob/master/toastr.js

Let’s first look at how this library is initialized.

; (function (define) {
    define(['jquery'], function ($) {
        return (function (){
        ....
        })();
    });
}(typeof define === 'function' && define.amd ? define : function (deps, factory) {
    if (typeof module !== 'undefined' && module.exports) { //Node
        module.exports = factory(require('jquery'));
    } else {
        window['toastr'] = factory(window['jQuery']);
    }
}));

So there’s a few things in here that are interesting.  That leading semi-colon isn’t a typo.  It’s there so that when javascript files are minified and concatenated together you get two statements instead of one.  The other thing to notice is that the authors play of a bit of code golf to support three load scenarios

1) AMD (dojo,backbone,etc.)

require(['toastr'], function (toastr) {
    ...
});

2) Node.js

var toastr = require('toastr');

3) Browser + jquery

<script src="jquery.js"></script>
<script src="toastr.js"></script>

Another thing that stood out to me is that the authors use an extra closure than I would have.

return (function () { ... })()

They do this because of their writing style. They call their functions before they write them.  \_(ツ)_/¯

Shrugs aside, I do like the start of the library, it’s very clear which functions are exposed by toastr.

var toastr = {
                clear: clear,
                remove: remove,
                error: error,
                getContainer: getContainer,
                info: info,
                options: {},
                subscribe: subscribe,
                success: success,
                version: '2.1.1',
                warning: warning
            };
 
            var previousToast;
 
            return toastr;

OK, let’s start looking at the guts of this thing. We’ll start with the notify function.

function notify(map) {
    var options = getOptions();
    var iconClass = map.iconClass || options.iconClass;
 
    if (typeof (map.optionsOverride) !== 'undefined') {
        options = $.extend(options, map.optionsOverride);
        iconClass = map.optionsOverride.iconClass || iconClass;
    }
 
    if (shouldExit(options, map)) { return; }

Outside from the struggle in setting an iconClass everything seems pretty innocent here.  I do have a bit of an issue with shouldExit.  The notify function should have one responsibility, show a notification.  Let’s take a peek at the shouldExit function.

function shouldExit(options, map) {
    if (options.preventDuplicates) {
        if (map.message === previousToast) {
            return true;
        } else {
            previousToast = map.message;
        }
    }
    return false;
}

I’m not crazy about this.  I don’t like that we’re setting state in this method. We also learn that preventDuplicates doesn’t prevent duplicates in all cases.  We would get three toasts from the following statements “toastr.info(1);toastr.info(2).toastr.info(1)”.

I would have preferred to see the existing code written something like the following:

function request(map){
    var options = getOptions();
    if(shouldNotify(options,map,activeToasts)){
        notify(options,map);
        activeToasts.push(map.message);
    }
}

OK, so preventDuplicates a little more robust, there’s value in that.  The shouldExit function was moved outside of the notify function because as I said, notify should have one responsibility.  But is this one responsibility thing important?  It’s certainly a concept espoused by many through the single responsibility principle. Let’s look at a few more functions.

function setCloseButton() {
    if (options.closeButton) {
        $closeElement.addClass('toast-close-button').attr('role', 'button');
        $toastElement.prepend($closeElement);
    }
}
function handleEvents() {
    $toastElement.hover(stickAround, delayedHideToast);
    if (!options.onclick && options.tapToDismiss) {
        $toastElement.click(hideToast);
    }
 
    if (options.closeButton && $closeElement) {
        $closeElement.click(function (event) {
            ...
        });
    }
    ...
}
function displayToast() {
    ...
    if (options.timeOut > 0) {
        intervalId = setTimeout(hideToast, options.timeOut);
        ...
        if (options.progressBar) {
            progressBar.intervalId = ...
        }
    }
}

If options are added or removed, several areas of the code need to be modified.  Everything would need to be retested. If several developers were working on the code base, they would be tripping over each other fighting all kinds of code merge issues.

So how can this toastr library be improved?  Notice that all the options add/enable functionality to this thing called a toast. We could say the options are decorating the toast.  Hey there’s a pattern for that!

Using the decorator pattern every toast option could be used to decorate toasts with new functionality.  In doing so, the code would be aligned with the open-closed principle.  The core of the library is closed to modifications, but open for extension.  The notify function shouldn’t have to be modified every time a new option is added, but it should be extensible so that new functionality can be added.

I’m going to walk through how a decorator pattern could be implemented for this library.  But first I want to talk technique.  Glance over the code and you’ll see “if” statements plastered all over the place.  It’s been said that in object oriented programming no “if” statements are needed.  This might sound a little crazy, and you could point to a scenario like the following and say you definitely need an “if” statement for that code.

if($('#' + options.containerId).length)
    return getContainer();
else
    return createContainer();

In a language like C# we could easily write this without an “if” statement.

public class ContainerState { };
public class CreatedState:ContainerState { };
public class NoContainerState : ContainerState { };
public Container GetContainer(CreatedState state) { ... };
public Container GetContainer(NoContainerState state){ ... }

This technique is useful in keeping functions to a single responsibility.

OK back to the decorator, I’m going to use this technique in the decorator implementation.  As I run the decorator, I’m going to override functions for the given state of the options.  Look at the setSequence function.

function setSequence() {
    if (options.newestOnTop) {
        $container.prepend($toastElement);
    } else {
        $container.append($toastElement);
    }
}

In my decorator, when the newestOnTop option is set, I will override setSequence to be

function(){$container.prepend($toastElement);}

When the option isn’t set, setSequence will be this function

function(){ $container.append($toastElement); }

The next step in creating my decorator involves creating a “decorate” function for every toast option.   To do this I’m going to go through the code library, any place I find functionality specific to an option, I will add this code to a “decorate” function that is specific to that option.  These “decorate” functions will end up doing various things depending on the option; they’ll include adding html elements to the toast, overloading library functions, adding event listeners for actions like “show toast”, “hide toast”, etc…

Once completed these functions should look something like this.

title: function (value, toast) {
    var titleElement = $('<div/>');
    titleElement.append(value).addClass(toast.options.titleClass);
    toast.element.append(titleElement);
},
message: function (value, toast) {
    var messageElement = $('<div/>');
    messageElement.append(toast.options.message)
        .addClass(toast.options.messageClass);
    toast.element.append(messageElement);
},
iconClass: function (value, toast) {
    toast.element.addClass(toast.options.toastClass)
        .addClass(toast.options.iconClass);
},
timeOut: function (value, toast) {
    toast.element.on('toastDisplayed', timeoutToastDisplayed);
    toast.element.on('stickAround', function () {
        clearTimeout(toast.intervalId);                          
    });
    toast.element.on('delayedHideToast', timeoutDelayedHideToast)                
},

With the list of functions created, once a toast is requested, I can iterate through all the options for that toast and call the “decorate” methods that apply for the given options.

I could also make use of a simple “Decorator” class in javascript

function Decorator(action) {
    this.action = action || function () { };
    this.decorate = function (fn) {
        var t = this.action;
        this.action = function () { t(); fn() }
    }
}

With the use of that class I can now replace the existing personalizeToast function

function personalizeToast() {
    setIcon();
    setTitle();
    setMessage();
    setCloseButton();
    setProgressBar();
    setSequence();
}

With the call below

personalizations.action();

The toastr website states “The goal is to create a simple core library that can be customized and extended”By implementation this type of decorator into the toastr library it could very easily be adapted to a plugin architecture.  I can imagine several extensions for things like media, video, effects, forms, etc…

That’s it for my review of the toastr library, I hope it was informative.  I personally learned a few things by looking at this library and felt I could have written a lot more on it.  Feel free to contact me with any questions or comments.

Leave a Reply

Your email address will not be published. Required fields are marked *