Single-Page Applications with the Page Micro Library

|

0

By Toby Ho

Page is a small client-side routing library that can be used to build single page applications (SPAs). It has a simple API which is inspired by Express. It utilizes the HTML5 history API under the hood, which is what allows you to build smooth user interfaces while still having linkable URLs for different pages within the app.

Routing

Page supplies you a page function that has a few different roles:

var page = require('page');

The first of those roles is specifying routes. If you’ve use Ruby on Rails, Express or a similar framework, this should look familiar:

page('/', function(){
  // Do something to set up the index page
});

page('/about', function(){
  // Set up the about page
});

Your routes can contain parameters, which you can assess through the context parameter to your route handler.

page('/user/:id', function(context){
  var userId = context.params.id;
  console.log('Loading details for user', userId);
});

You can use wildcards as parameters too, in which case you will need to use array indexing to access the parameters:

page('/files/*', function(context){
  var filePath = context.params[0];
  console.log('Loading file', filePath);
});

A key difference between using a wildcard versus named parameters is that a wildcard can match the character “/”, while a named parameter cannot. In our file path example, using a wildcard allows filePath to contain arbitrarily nested subdirectories.

Another useful thing you can do with a wildcard is to define a fallback route:

page('*', function(){
  console.error('Page not found :(');
});

Starting The Router

Once you have all your routes defined, you need to start the router, which is done with another call to page, but this time with no parameters:

page();

If you are weirded out by this, and prefer to be more explicit, you can instead write the equivalent:

page.start();

Both of these can take an optional options object, containing the properties:

  • click – whether to automatically bind to click events on the page and intercept link clicks and handle them using page’s router – defaults to true.
  • popstate – whether to bind to and utilize the popstate event – defaults to true.
  • dispatch – whether to perform initial route dispatch based on the current URL – defaults to true.

Programmatic Navigation

As I mentioned above, by default page will automatically intercept clicks on links on the page and try to handle them using the routes you’ve defined. Only if it can’t match the URL with any of the defined routes will it default back to the browser’s default behavior. Sometimes though, you may want to change the URL based on other events. Maybe the element clicked happens to be something other than a link. Or, if you are building a search page, you may want to allow users to share the URL to their search results. You can do this by just calling page function with the page you want to navigate to:

$(form).on('submit', function(e){
  e.preventDefault();
  page('/search?' + form.serialize());
});

If you prefer to be more explicit, you can use page.show(path) instead.

Route Handler Chaining

A cool feature of page is that it allows for route handler chaining, which is similar to Express’s middlewares. A route definition can take more than one handler:

page('user/:id', loadUser, showUser);

Here, when the path user/1 is navigated, page will first call the loadUser handler. When the user is done loading, it will call the showUser handler to display it. How does it know when the user is done loading? A callback is provided to the handlers as the second parameter – here is what loadUser might look like:

function loadUser(ctx, next){
  var id = ctx.params.id;
  $.getJSON('/user/' + id + '.json', function(user){
    ctx.user = user;
    next();
  });
}

Then, in showUser you can get the user object through ctx.user. This is nice because you can reuse the loadUser function for, say, the user/:id/edit route.

States

The History API supports saving states along with each history entry. This allows you to cache information along with previously navigated URLs, so that if the user navigates back to them via the back button, you don’t have to to re-fetch the information, making the UI much smoother. Page exposes this via the state property of the context object. To make the above loadUser function utilize this cache, you would write this:

function loadUser(ctx, next){
  if (ctx.state.user){
    next();
  }else{
    var id = ctx.params.id;
    $.getJSON('/user/' + id + '.json', function(user){
      ctx.state.user = user;
      ctx.save();  // saves the state via history.replaceState()
      next();
    });
  }
}

Putting It All Together

Now that you know what you need to know about page, let’s build an example application. The app will render a list of the earliest Github users. You can click on an individual user and get more details about him or her. The back button should work seamlessly and should use caching. This will use the modules page, superagent, and mustache.

var page = require('page');
var request = require('superagent');
var mustache = require('mustache');

These are route definitions:

page('/', loadUsers, showUsers);
page('/user/:id', loadUser, showUser);

The implementation of loadUsers and loadUser look like this, much like the previous state-caching example:

function loadUsers(ctx, next){
  if (ctx.state.users){
    // cache hit!
    next();
  }else{
    // not cached by state, make the request
    request('https://api.github.com/users', function(reply){
      var users = reply.body;
      ctx.state.users = users;
      ctx.save();
      next();
    });
  }
}

function loadUser(ctx, next){
  if (ctx.state.user){
    next();
  }else{
    var id = ctx.params.id;
    request('https://api.github.com/user/' + id, function(reply){
      var user = reply.body;
      ctx.state.user = user;
      ctx.save();
      next();
    });
  }
}

The application will use mustache for rendering the pages. I’ve made the following templates:

var listTemplate = 
  '<h1>Early Github Users</h1>
  <ul>
    {{#.}}
    <li>
      <a href="/user/{{id}}">{{login}}</a>
    </li>
    {{/.}}
  </ul>';


var showTemplate = 
  '<h1>User {{login}}</h1>
  <p>{{name}} is user number {{id}}. 
  He has {{followers}} followers, 
  {{public_repos}} public repos and writes a blog at
  <a href="{{blog}}">{{blog}}</a>.
  <a href="/">Back to list</a>.</p>
  ';

There are ways to lift the markup into .html files, but I’ll save that for another day. Rendering these templates is the job of the showUser and showUsers functions:

var users = ctx.state.users;

  content.innerHTML = 
      mustache.render(listTemplate, users);
}

function showUser(ctx){
  var user = ctx.state.user;
  content.innerHTML = 
    mustache.render(showTemplate, user);
};

And finally, we need to start the router:

page.start();

And there you have it! A multi-view single page application. If you want to poke around with this code, take a look at the full source code, which has been modularized into small files.

This article was originally published at http://smalljs.org/client-side-routing/page/

Previous

Using the AWS JavaScript SDK in the Browser

Expose Yourself with ngrok

Next

5 thoughts on “Single-Page Applications with the Page Micro Library”

  1. 0

    Hey, thanks for the writeup. I recently started using pages to migrate existing server-side code into a client, and the transition from express has been amazingly smooth. I wish I had been aware of the ctx.state property a while back 🙂

    Reply

Leave a Comment