Building a Single Page Application Router With Vanilla JS

in javascript

A good way to understand how complex mechanisms work is to build a simpler version of them from scratch. As most modern JS frameworks provide some sort of client side routing, writing our own will improve our understanding of those frameworks.

The App

Our goal is to build a single page application which uses hash for page navigation. Ajax calls are used to fetch data for each page before navigating.

The finished product will look like this (click on the links to change pages):

Full source code is available in the following link:
https://codepen.io/ynonp/project/editor/XVvNYO/

Each page in the application is identified by the same URL with a different hash. In our case the pages are: #home and #about. Using a hash to identify the page has the advantage that no server side support is required so static sites are supported.

Router Class

The first and main component of the application is the router. A router stores a mapping between a URL pattern and a specific page object that we should show.

A router's main function is show which shows a new page. Let's start with its implementation:

async show(pageName) {
  const page = this.routes[pageName];
  await page.load();
  this.el.innerHTML = '';
  page.show(this.el);
}

The function takes a page name, uses it to reach a page object and calls load on the page object asynchronously. This is the good part, as it allows pages to fetch their own data using ajax.

Each page should implement its own load logic, and the only constraint we have is that load must return a promise. Calling await on the promise waits for it, so we can show the resulting page in the following lines.

The rest of the router code should be easy to implement. Here's my code:

class Router {
  constructor(routes, el) {
    this.routes = routes;
    this.el = el;
    window.onhashchange = this.hashChanged.bind(this);
    this.hashChanged();
  }

  async hashChanged(ev) {
    if (window.location.hash.length > 0) {
      const pageName = window.location.hash.substr(1);
      this.show(pageName);
    } else if (this.routes['#default']) {
      this.show('#default');
    }
  }

  async show(pageName) {
    const page = this.routes[pageName];
    await page.load();
    this.el.innerHTML = '';
    page.show(this.el);
  }
}

Page Class

A page is simply an object that can render itself into an element. We'll build two types of pages: Page and Layout, and it's really easy to expand and build more types.

A normal Page loads its data via ajax from a page url. Remember our Router requires a page to implement load and show, and that load should return a promise:

class Page {
  constructor(url) {
    this.url = 'views/' + url;
  }

  load() {
    return $.get(this.url).then(res => this.html = res);
  }  

  show(el) {
    el.innerHTML = this.html;
  }
}

By returning a Promise from load, the router knows to wait until the page asynchronously loads itself before calling show.

Using Layouts

The second type of page is a Layout. Keeping with the same structure, a layout is a page that contains several other pages. It'll take the list of pages in the constructor and in its load will just call all the pages' load functions:

class Layout {
  constructor(...pages) {
    this.pages = pages;
  }

  load() {
    return Promise.all(this.pages.map(page => page.load()));
  }

  show(el) {
    for (let page of this.pages) {
      const div = document.createElement('div');
      page.show(div);
      el.appendChild(div);
    }
  }
}

Note how our simple layout just puts all the pages one after the other in its show method, but of course more complex layouts are easy to build.

main.js

The file main.js creates the Router and pages:

const r = new Router(
  {
    about: new Layout(new Page('menu.html'), new Page('about.html')),
    home: new Layout(new Page('menu.html'), new Page('home.html')),
    '#default': new Page('menu.html'),
  },
  document.querySelector('main')
);

In this simple application pages are identified by a single string. Modern frameworks usually allow more complex url patterns, for example the pattern /product/:id would lead to a page called Product and pass it the last word as a parameter named id. I'll leave adding that functionality to a future post.

Resources & Extra Reading

My goal here was mainly to show that basic application architecture is possible without using the state of the art framework of the hour. Understanding the simple concepts that frameworks are based on will hopefully help you understand the frameworks themselves.

Of course modern routers provide so much more than described here. If you found this post interesting I suggest to continue reading:

  1. HTML5 Provides browser history API which allows to implement a router without using hash URLs, but it has a price: some changes in server side code are required. Krasimir Tsonev wrote a great post on the subject called Deep dive into client-side routing

  2. I used async/await functions to simplify how the code handles promises. If you're not familiar with them you should definitely read MDN Documentation on async functions

  3. Full source code for the project described here: https://codepen.io/ynonp/project/editor/XVvNYO/

Comments