Page

Module Summary

LOC:
195
File size minified:
3.4k/3.4k
Dependencies:
none
Browser Support:
IE8+
Github:
visionmedia/page.js
npm:
page
Component:
visionmedia/page.js
Bower:
page
by Toby Ho on 4/16/2014

Page is a small client-side routing library for use with building 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 of the app.

Routing

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

var page = require('page');

The first of those roles is specifying routes. If you've use Ruby on Rails or 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 vs a named parameter 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:

Programmatic Navigation

As mentioned above, page will by default automatically intercept clicks on links on the page and try to handle it using the routes you've setup. Only if it can't match the url with any of the define 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 at the user through ctx.user. Now 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();
    });
  }
}

For rendering the pages, this will use mustache, and 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. To render these templates is job of showUser and showUsers:

function showUsers(ctx){
  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-page 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.

comments powered by Disqus