Last week I created a small library for creating Toast messages. Here, I thought I would share my process in creating it and the code behind it.

The HTML

The Toast is made up of two elements. The Toast message itself, .toastjs, and a container element, .toastjs-container.

<div class="toastjs-container" role="alert" aria-hidden="true">  
    <div class="toastjs"><!-- Content here --></div>
</div>  

Until a Toast is being displayed, the container is hidden using the aria-hidden attribute.

The CSS

To create the sliding in/out animation of the Toast, a transform is applied to the Toast container element. By default, the .toastjs-container is positioned off-screen to the left.

.toastjs-container {
    transform: translateX(-150%);
    transition: transform 1s;

    /* Other styles */
    position: fixed;
    bottom: 30px;
    left: 30px;
    width: calc(100% - 60px);
    max-width: 400px;
}

When the Toast message is being displayed, as is determined by it's aria-hidden attribute, the element is positioned to a normal state.

.toastjs-container[aria-hidden="false"] {
    transform: translateX(0%);
}

The JavaScript

The Toast is a custom Toast prototype. There are five main methods behind it.

Toast.prototype._createElements

This function creates the Toast element and it's container, with all the appropriate attributes and classes. It also appends it to the body element.

Toast.prototype._createElements = function() {  
    return new Promise((resolve, reject) => {

        this.toastContainerEl = document.createElement('div');
        this.toastContainerEl.classList.add('toastjs-container');
        this.toastContainerEl.setAttribute('role', 'alert');
        this.toastContainerEl.setAttribute('aria-hidden', true); 

        this.toastEl = document.createElement('div');
        this.toastEl.classList.add('toastjs');

        this.toastContainerEl.appendChild(this.toastEl);
        document.body.appendChild(this.toastContainerEl);

        setTimeout(() => resolve(), 500);

    })
};

Toast.prototype._addEventListeners

This function adds event listeners to the buttons within the toast. By default, there is at least a close button. Optionally, there are custom buttons with custom callback functions that should be applied.

Toast.prototype._addEventListeners = function() {

    document.querySelector('.toastjs-btn--close').addEventListener('click', () => {
        this._close();
    })

    if ( this.options.customButtons ) {
        const customButtonsElArray = Array.prototype.slice.call( document.querySelectorAll('.toastjs-btn--custom') );
        customButtonsElArray.map( (el, index) => {
            el.addEventListener('click', (event) => this.options.customButtons[index].onClick(event) );
        });
    }

};

Toast.prototype._close

This is the function that is called when the Toast is closed. It first sets the correct aria-hidden attribute on the Toast container, then resets the main Toast element.

Toast.prototype._close = function() {  
    return new Promise((resolve, reject) => {
        this.toastContainerEl.setAttribute('aria-hidden', true); 
        setTimeout(() => {

            this.toastEl.innerHTML = '';
            this.toastEl.classList.remove('default', 'success', 'warning', 'danger');

            if ( this.focusedElBeforeOpen ) {
                this.focusedElBeforeOpen.focus();
            }

            resolve();

        }, 1000); 
    });     
};

Toast.prototype._open

This is the function that is called when the Toast is opened. It does the following things -

  • Sets the correct aria-hidden attribute to the Toast container
  • Adds the correct class to the Toast element
  • Adds the correct message content to the Toast element
  • Creates any custom buttons if specified
Toast.prototype._open = function() {

    this.toastEl.classList.add(this.options.type);
    this.toastContainerEl.setAttribute('aria-hidden', false); 

    let customButtons = '';
    if ( this.options.customButtons ) {
        customButtons = this.options.customButtons.map( (customButton, index) => {
            return `<button type="button" class="toastjs-btn toastjs-btn--custom">${customButton.text}</button>`
        } )
        customButtons = customButtons.join('');
    }

    this.toastEl.innerHTML = `
        <p>${this.options.message}</p>
        <button type="button" class="toastjs-btn toastjs-btn--close">Close</button>
        ${customButtons}
    `;

    this.focusedElBeforeOpen = document.activeElement;
    document.querySelector('.toastjs-btn--close').focus();
};

Toast.prototype._init

Finally, this is the function that initialises the Toast. It calls all the other functions in their correct sequence.

Toast.prototype._init = function() {  
    Promise.resolve()
    .then(() => {
        if ( this.toastContainerEl ) { 
            return Promise.resolve();
        }
        return this._createElements();
    })
    .then(() => {
        if ( this.toastContainerEl.getAttribute('aria-hidden') == 'false'  ) {
            return this._close();
        }
        return Promise.resolve();
    })
    .then(() => {
        this._open();
        this._addEventListeners();
    })
};

 

You can view the full source for the library here, or view a demo.