“toast” mini-library

Posted on

Problem

I refactored the code for a library that creates toasts. I have borrowed most of my naming conventions (and constant naming conventions in specific) as well as the design pattern from Bootstrap’s source code.

What could be done to improve the code’s readability? How would it be more maintainable? What do you dislike about the current style? Which parts do you think to be unnecessary?

const Toast = (function(){
  
  const Classes = {
    CONTAINER: 'toastContainer',
    TOAST_NEUTRAL: 'toastNeutral',
    TOAST_SUCCESS: 'toastSuccess',
    TOAST_ERROR: 'toastError',
    TOAST_WARNING: 'toastWarning',
    HEADER: 'toastHeader',
    CONTENT: 'toastContent'
  };
    
  const Ids = {
    TOASTER_ELEMENT: 'toasterElement'
  };
  
  const ToastTypes = {
    NEUTRAL: {
      HEADER_MESSAGE: "Info:",
      CLASS_NAME: Classes.TOAST_NEUTRAL
    },
    SUCCESS: {
      HEADER_MESSAGE: 'Success!',
      CLASS_NAME: Classes.TOAST_SUCCESS
    },
    ERROR: {
      HEADER_MESSAGE: 'Error!',
      CLASS_NAME: Classes.TOAST_ERROR
    },
    WARNING: {
      HEADER_MESSAGE: 'Warning!',
      CLASS_NAME: Classes.TOAST_WARNING
    }
  };
  
  
  const Toast_Utils = (function(){
    const _generateDiv = function _generateDiv(content, ...classNames) {
      return $('<div></div').addClass(classNames.join(' ')).html(content);
    };
    
    const createToastContainer = function createToastContainer(toastType) {
      return _generateDiv(null, Classes.CONTAINER, ToastTypes[toastType].CLASS_NAME)
        .hide();
    };

    const createToastHeader = function createToastHeader(toastType) {
      return _generateDiv(ToastTypes[toastType].HEADER_MESSAGE, Classes.HEADER);
    };

    const createToastContent = function createToastContent(toastMessage) {
      return _generateDiv(toastMessage, Classes.CONTENT);
    };
    
    return {
      createToastContainer: createToastContainer,
      createToastHeader: createToastHeader,
      createToastContent: createToastContent
    };
  })();
  
  const Toast = function Toast(toastType, toastMessage) {
    if(!Toast.toasterElement) {
      throw new Error('You have to call "Toast.init()" before creating "Toast" instances!');
    }
    if(!typeof toastType == 'string' || !ToastTypes[toastType.toUpperCase()]) {
      const acceptedToastTypes = Object.keys(ToastTypes).map(function(el){
        return '"' + el.toLowerCase() + '"';
      }).join(',');
      throw new Error('You must pass one of these strings for the first parameter: ' + acceptedToastTypes);
    }
    toastType = toastType.toUpperCase();
    arguments.length < 2 && (toastMessage = "");
    this.toast = Toast_Utils.createToastContainer(toastType)
      .append(Toast_Utils.createToastHeader(toastType))
      .append(Toast_Utils.createToastContent(toastMessage));
  };
  
  Toast.init = function() {
    Object.defineProperty(this, 'toasterElement', {
      value: $('<div></div>')
        .attr('id', Ids.TOASTER_ELEMENT),
      writable: false,
      configurable: false
    });
    $(document.body).append(this.toasterElement);
    return this.toasterElement;
  };
  
  Toast.prototype.begin = function() {
    Toast.toasterElement.append(this.toast);
    this.toast.fadeIn(500);
  };
    
  Toast.prototype.destroy = function destroy() {
    this.toast.fadeOut(500, () => {
        this.toast.remove();
    });
  };
  
  return Toast;
})();



$(document).ready(function() {

  Toast.init();
  
  $(document).on("click", "#btnNeutral", function() {
    let toast = new Toast("neutral", "Click on the bottom-most icon to change the website theme");
    toast.begin();
    setTimeout(toast.destroy.bind(toast), 3000);
  });
  
  $(document).on("click", "#btnSuccess", function() {
    let toast = new Toast("success", "everything went alright");
    toast.begin();
    setTimeout(toast.destroy.bind(toast), 3000);
  });

  $(document).on("click", "#btnError", function() {
    let toast = new Toast("error", "something went wrong");
    toast.begin();
    setTimeout(toast.destroy.bind(toast), 3000);
  });
  
  $(document).on("click", "#btnWarning", function() {
    let toast = new Toast("warning", "a bomb went off 24km from your location");
    toast.begin();
    setTimeout(toast.destroy.bind(toast), 3000);
  });
  
});
#toasterElement {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 1;
  width: 20%;
}

.toastContainer {
  margin-top: 10px;
  font-weight: bold;
  font-family: arial;
  width: 100%;
  padding: 10px;
  color: #ffffff;
  border-left: 5px solid white;
  box-shadow: -2px 0 2px rgba(0, 0, 0, .2);
  position: relative;
  z-index: 1;
}

.toastNeutral {
  background-color: #242424;
}

.toastSuccess {
  background-color: #32cd32;
}

.toastError {
  background-color: #ff1a1a;
}

.toastWarning {
  background-color: #fcdc47
}

.toastContent {
  margin-top: 5px;
  font-size: 70%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<button id="btnNeutral">
Neutral
</button>

<button id="btnSuccess">
Success
</button>

<button id="btnError">
Error
</button>

<button id="btnWarning">
Warning
</button>

Solution

Small typo, and a simplification

Here’s a small typo:

return $('<div></div').addClass(classNames.join(' ')).html(content);

And a slightly simpler way to write that:

return $('<div/>').addClass(classNames.join(' ')).html(content);

Naming

In Toast_Utils,
the name of the object already implies that they are related to “toast”,
no need to repeat the word “toast” in the functions.
For example instead of createToastContainer,
createContainer should be clear enough,
as it will be used as Toast_Utils.createContainer.

Also, I would inline the definition of all these functions to reduce the boilerplate code.

Arrow functions

You mostly use arrow functions, except in a few places, for example in the map call here:

  const acceptedToastTypes = Object.keys(ToastTypes).map(function(el){
    return '"' + el.toLowerCase() + '"';
  }).join(',');

And as a tiny usability improvement,
I would add a space after the comma when joining,
to make the error message slightly easier to read.

Readability

A common convention is to put spaces between if and the (...) condition,
for example in if(!Toast.toasterElement) {.


Is this a common writing style?

arguments.length < 2 && (toastMessage = "");

I would spell it out as an if statement.

I like the pattern of defining the used CSS IDs and classes at the very beginning.

I don’t see the need for a separate utility scope. I would just write these functions inline, or inline their code as well.

You can replace the error handling by making the public API so simple that it cannot be used incorrectly anymore. The typical use case is not to create a toast for later reuse, but instead to show it immediately. The API should reflect this.

Making the user of the module call init explicitly is not necessary. The toast container should be initialized when the first toast is to be shown.

I prefer a JavaScript style that doesn’t refer to this or prototypes. It looks cleaner and doesn’t require as much thinking about which objects are bound to which functions.

Using these guidelines, I transformed your code to this:

const Toast = (function () {

    const Ids = {
        TOASTER: 'toasterElement'
    };

    const Classes = {
        CONTAINER: 'toastContainer',
        TOAST_NEUTRAL: 'toastNeutral',
        TOAST_SUCCESS: 'toastSuccess',
        TOAST_ERROR: 'toastError',
        TOAST_WARNING: 'toastWarning',
        HEADER: 'toastHeader',
        CONTENT: 'toastContent'
    };

    let toaster = null;

    function show(cssClass, heading, text, duration) {
        if (toaster === null) {
            toaster = $('<div>').attr('id', Ids.TOASTER);
            $(document.body).append(toaster);
        }

        const header = $('<div>').addClass(Classes.HEADER).text(heading);
        const content = $('<div>').addClass(Classes.CONTENT).text(text);
        const container = $('<div>').addClass(Classes.CONTAINER).addClass(cssClass).hide();
        const toast = container.append(header).append(content);
        toaster.append(toast);

        function destroy() {
            toast.fadeOut(500, () => toast.remove());
        }

        toast.fadeIn(500, () => setTimeout(destroy, duration));
    }

    function showNeutral(text, duration) {
        show(Classes.TOAST_NEUTRAL, 'Info:', text, duration);
    }

    function showSuccess(text, duration) {
        show(Classes.TOAST_SUCCESS, 'Success!', text, duration);
    }

    function showWarning(text, duration) {
        show(Classes.TOAST_WARNING, 'Warning!', text, duration);
    }

    function showError(text, duration) {
        show(Classes.TOAST_ERROR, 'Error!', text, duration);
    }

    return {
        showNeutral: showNeutral,
        showSuccess: showSuccess,
        showWarning: showWarning,
        showError: showError,
    };
})();

$(document).ready(function () {
    $(document).on("click", "#btnNeutral", () => {
        Toast.showNeutral("Click on the bottom-most icon to change the website theme", 3000);
    });
    $(document).on("click", "#btnSuccess", () => {
        Toast.showSuccess("everything went alright", 3000);
    });
    $(document).on("click", "#btnError", () => {
        Toast.showError("something went wrong", 3000);
    });
    $(document).on("click", "#btnWarning", () => {
        Toast.showWarning("a bomb went off 24km from your location", 3000);
    });
});

Leave a Reply

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