Home

Oct. 19, 2018, 3 min read

Vanilla JS Routing with Navigo

I am currently working at adding client-side routing to a CMS-like web application that is partially rendered server-side but has most of its UI interaction implemented using jQuery handlers to dynamically replace parts of its DOM with fresh HTML coming in via AJAX from the server.

The application has a two column layout and the left and right content zones are populated differently depending on internal state that changes by clicking UI elements. As the application grows it is becoming harder to track how many of these states, or "pages", actually exist and how we can reproduce them.

There is also some lack of UX, as we cannot use the browser's builtin back/forth buttons and reloading the page will not reproduce our current screen but instead load a default start page. This is where routing can help, as it defines URLs that map to a certain page and thereby state, as well as allowing us to use the browser's history API to remember URLs we have been to.

This also makes it easier to link between pages inside the application. While there are many libraries to help with client-side routing, I found https://github.com/krasimir/navigo to have nice documentation and be easy to use.

I can recommend it if you embark on a similar journey. There are a few small additions I had to implement which may be useful for you as well:

/**
 * Navigate to route, even if already on it.
 *
 * https://github.com/krasimir/navigo/issues/196#issuecomment-430548874
 */
Navigo.prototype.forceNavigate = function(path, absolute) {
  if (absolute === undefined) {
    absolute = false;
  }
  // Change the query so it is always detected as a new route.
  if (this._lastRouteResolved) {
    this._lastRouteResolved.query = '_=' + Number(new Date());
  }
  this.navigate(path, absolute);
}

/**
 * Reload the current route.
 */
Navigo.prototype.reload = function() {
  var current = this.lastRouteResolved();
  if (!current) {
    return;
  }
  this.forceNavigate(current.url);
}

/**
 * Check if previous and current recorded route are different.
 *
 * Checks route name and ignores params by default. If params
 * should be compared as well they must be passed in individually
 * as an Array of parameter names.
 */
Navigo.prototype.routeChanged = function(paramNames) {
  if (!this.history) {
    return true;
  }
  var current = this.lastRouteResolved();
  var previous = this.history[0];
  var nameChanged = current.name !== previous.name;
  if (!paramNames) {
    return nameChanged;
  }
  var bothHaveParams = current.params !== null && previous.params !== null;
  if (!bothHaveParams) {
    return true;
  }
  var paramsChanged = false;
  for (var i = 0; i < paramNames.length; i++) {
    var paramName = paramNames[i]
    if (current.params[paramName] !== previous.params[paramName]) {
      paramsChanged = true;
      break;
    }
  }
  return nameChanged || paramsChanged;
}

var rootUrl = window.location.origin + "/cms";
var useHash = false;
var hash = "#";
var router = new Navigo(rootUrl, useHash, hash);

router.hooks({
  /** Ensure new data-navigo links work and record navigated routes. */
  after: function() {
    router.updatePageLinks()
    
    if (!router.history) {
      router.history = [];
    }
    var current = router.lastRouteResolved();
    router.history.unshift(current);
  },
});

Force-routing or reloading the current route is useful e.g. for the main navigation buttons, as I'd expect my content zones to show refreshed information on repeated clicks.

Recording the history allows me to check if a route has changed in general, or just in details (e.g. an object id), so that for example I can skip loading a list of items that is already displayed.

Happy routing!

routing