Dismissible banner continued: Storing component state

Journal

A look at how to store the state of a dismissible banner on the client-side using localStorage, enabling the basic function of persisting the hidden state of a banner for the user on their following page sessions.

In my previous article I wrote a basic JS solution for dismissible banners, where with the tap of a close button, the user can dismiss a banner message.

I would like to continue with this example, and extend the component to remember it’s dismissible state.

If we would like to hide this banner from the user for longer than their next page session, we need to store this information. We can do this on the client-side with the use of localStorage.

But our first step here is to add two new data properties for our component: data-id and data-expiry.

<div
    data-component="dismissible-item"
    data-expiry="168"
    data-id="welcome-banner"
    data-type="info"
    data-value="<strong>Welcome message</strong>"
>

The ID attribute provides our banner with a unique identifier for storing the dismissal action and relating it to a specific banner.

Our second attribute, expiry, allows us to pass the period of time the banner will not be displayed for. This can be a set number of hours, or forever.

storage.js

Now that our component markup is ready, the next step will be to create storage.js, a localStorage wrapper simplifying our interactions with window.localStorage.

;(function(window){

    var Storage = function() {

        function deleteItem(item){
            return window.localStorage.removeItem(item);
        }

        function getItem(item){
            return window.localStorage.getItem(item);
        }

        function setItem(item, contents){
            return window.localStorage.setItem(item, contents);
        }

        return {
            delete deleteItem,
            get: getItem,
            set: setItem
        }

    }

    window.Storage = new Storage();

})(window);

From this piece of code we now have a simple API to access and interact with the user’s localStorage. This enables us to store an item in localStorage to log when a banner was dismissed.

As seen above, localStorage is currently supported by 93% of modern browsers. Due to this feature not being available across all browsers, and also that a user can deny permission for websites to access localStore, we need to extend our API to check if the user’s browser supports this feature.

function browserHasSupport() {
  var testItem = 'localStorageTest'
  if (window.localStorage) {
    try {
      window.localStorage.setItem(testItem, testItem)
      window.localStorage.removeItem(testItem)
      return true
    } catch (e) {
      return false
    }
  } else {
    return false
  }
}

By exposing this function via window.Storage.enabled(), we can check if localStorage is available before attempting to read or write any data.

Leveraging window.Store in our dismissibleItem

Now that we have created our storage API with all features in place, we are ready to leverage this within our original dismissible banner component JS.

First, we need to set a global variable to check if localStorage is available:

var storageEnabled = window.Storage.enabled()

We added two new data attributes to our component earlier, so let’s select their values and pass them to our dismissibleItem initialiser.

var dismissibles = Array.prototype.slice.call(
  document.querySelectorAll('[data-component="dismissible-item"]')
)
if (dismissibles.length) {
  for (var i = 0; i < dismissibles.length; i++) {
    var expiry = dismissibles[i].getAttribute('data-expiry'),
      id = dismissibles[i].getAttribute('data-id'),
      type = dismissibles[i].getAttribute('data-type'),
      value = dismissibles[i].getAttribute('data-value')
    new dismissibleItem(dismissibles[i], type, id, value, expiry)
  }
}

Okay, time to integrate our new feature to our dismissibleItem!

var dismissibleItem = function (el, type, id, value, expiry) {
  var hasExpiry = id && expiry ? true : false,
    html =
      '<span>' +
      value +
      ' <button type="button" class="close">X</button></span>'

  el.classList.add('dismissible', 'dismissible-' + type)

  el.removeAttribute('data-component')
  el.removeAttribute('data-expiry')
  el.removeAttribute('data-id')
  el.removeAttribute('data-type')
  el.removeAttribute('data-value')

  el.innerHTML = html
}

Above we pass our new attributes into dismissibleItem, and set another variable, hasExpiry, based on whether the expiry and ID attributes were provided. This gives us a simple flag to check if we need to run storage logic for this banner.

The next step is to use this with our localStorage wrapper to modify our close button event handler, so that if our banner has an expiry, we will store a log of the dismissed banner in localStorage.

if (hasExpiry && storageEnabled) {
  window.Storage.set(id, new Date().getTime())
}

When this code runs, our localStorage will be updated with the following key & value:

Now that we have recorded the ID and a timestamp of when our banner was dismissed, we need to make some changes to our component to handle if the banner has a logged dismissal on initialisation.

if (hasExpiry && storageEnabled) {
  var timestamp = window.Storage.get(id)
  if (timestamp) {
    if (expiry === 'forever') {
      el.remove()
      return
    } else {
      var now = new Date(),
        diffInHours = Math.floor(
          (now.getTime() - parseInt(timestamp)) / (1000 * 60 * 60)
        )
      if (diffInHours >= expiry) {
        window.Storage.delete(id)
      } else {
        el.remove()
        return
      }
    }
  }
}

After checking if the banner has an expiry value, and localStorage is accessible, we try to retrieve a log of our banner with window.Storage.get(id).

If a timestamp exists, let’s check if the banner is to be dismissed forever, and if so, remove from the DOM.

When this is not the case, and a value of hours has been defined, we need to check if the difference in hours from dismissal log is greater or less than the expiry value.

If greater, we can again remove the banner from the DOM. If this is not the case, and the difference is less, we should clear our banner’s dismiss log and continue to display once again.

Code Example

http://codepen.io/matswainson/pen/ozrdZd