Skip to content

Blogging On Rails

Everything on Rails!

Leaving Breadcrumbs in your Progressive Web App with Stimulus.js

One of the features of a Progressive Web App is the lack of browser chrome on a mobile device. iOS and Android hide little things like the address bar and the browser history buttons. It’s up to the PWA itself to provide the navigation to previous pages.

Breadcrumbs are one technique of displaying previously visited pages on the web page, and they are usually stored on the server side and rendered into the HTML sent over the wire. Another technique is to store them in the browser tab’s sessionStorage and render them on the page. This technique fits neatly inside a Stimulus.js controller that listens for page changes from the beforeunload and the turbolinks:before-visit events. The history_controller.js listens for page change events, and records the current page into a history of the session. This history is stored and read from the browser’s sessionStorage, a key-value store that is kept around only during the lifetime of a tab.

A little breadcrumb demo

The HTML

Our breadcrumb HTML is really simple. The example uses an unordered list, and when connected, loads the items from history into individual list items. The only HTML needed is:

<ul data-controller="history" 
    data-target="history.links"
    class="history">
</ul> 

The ulhosts the controller, has a target which is used to append the entries, and a css class in order to style the breadcrumbs to fit the pages style.

Some CSS

In order for the list to appear horizontal, this demo uses the following CSS:

ul.history li {
  display: inline;
  list-style-type: none;
  padding-right: 1em;
}

Stimulus Templating

Inside the Stimulus controller, there is a function that will generate the individual list items, and the HTML is:

<li>
    <a href="${ historyItemPath }" 
       data-action="history#visitPageInHistory" 
       data-history-location="${ historyLocation }"> <strong>></strong> ${ historyItemTitle }</a>
</li>

historyItemPath is the index of the entry in the history list, historyLocation is the path of the page when it was visited, and historyItemTitle is the title of the page, which is good to see when figuring out which page was visited. Note the data-action, history#visitPageInHistory, which Stimulus picks up when the html is changed on the page.

The Controller

The history_controller.js Stimulus controller is responsible for listening for page changes, and recording the page into a history list with leavingPage(). The controller then stores the list in the browser’s sessionStorage, and the controller loads that list when it is initialized again. When connected to the page, the controller reads each entry, generates a list item, and inserts the history into the page. Then, if one of those history items is clicked, visitPageInHistory() is called and the controller clears out history entries that were visited in the list after the selected entry.

The Code

The localSessionKey is the key used store a JSON representation of the history array into sessionStorage.

const localSessionKey = "history.history"

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "links" ]

  initialize() {
    // Listen for page changes
    this.leavingPage = this.leavingPage.bind(this)
    window.addEventListener("turbolinks:before-visit", this.leavingPage);
    window.addEventListener("beforeunload", this.leavingPage);

    // Load history from session store
    let historyValue = window.sessionStorage.getItem(localSessionKey)
    if ( historyValue ) {
      this.history = JSON.parse(historyValue)
    } else {
      this.history = []
    }

    // Used to prevent current page from being entered into the history list when going back
    this.recordVisit = true;
  }

  connect() {
    // Load in the history and display it on the page
    var links = ""
    this.history.forEach( (historyEntry, historyLocation  ) => {
       links += linkHTML(historyEntry.title, historyEntry.path, historyLocation)
    });
    this.linksTarget.innerHTML = links; 
  }

  disconnect() {
    // Unload page change listener
    window.removeEventListener("turbolinks:before-visit", this.leavingPage);
    window.removeEventListener("beforeunload", this.leavingPage);
  }


  visitPageInHistory(event) {
    // When going back to a page on our bread crumb list, remove items visited after desired page
    let historyItemIndex = event.target.getAttribute('data-history-location');
    this.history = this.history.slice(0, historyItemIndex);
    this.recordVisit = false;
  }

  leavingPage(event) {
    // Record page into history when leaving
    if (this.recordVisit) {
      let lastVisitedItem = this.history[this.history.length - 1];
      if (lastVisitedItem == null || lastVisitedItem.path != this.pagePath ) {
        this.history.push({ title: document.title, path: this.pagePath });
      }
    }
    window.sessionStorage.setItem(localSessionKey, JSON.stringify(this.history));
  }

  get pagePath() {
    return `${window.location.pathname}${window.location.search}`;
  }
}

function linkHTML(historyItemTitle, historyItemPath, historyLocation) {
  return `<li><a href="${ historyItemPath }" data-action="history#visitPageInHistory" data-history-location="${ historyLocation }"> <strong>></strong> ${ historyItemTitle }</a></li>`;
}

Better Web App Usability

Progressive Web Apps don’t require a lot more work than a regular web app. They only require realizing the subtle differences between the two and filling in any deficiencies. Adding breadcrumbs to a web page will enhance navigation on the site, especially more complicated sites with deep menus. Cutting down on clicks makes everyone happy.

Feel free to leave a comment or question below.

Want To Learn More?

Try out some more of my Stimulus.js Tutorials.

Make Interactivity Default 

Make your web app interactive now with easy to implement and simple to add HOTWire integrations. 

Enter your email and get a free sample of my HOTWire Tutorials ebook.

We won’t send you spam. Unsubscribe at any time.

Leave a Reply

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