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 https://smalljs.org/client-side-routing/page/
Why not just use Sammy.js (https://sammyjs.org/). It’s been around for like 6 or 7 years so it’s battle-tested, stable, and there should be a good number of articles and tutorials written about it.
I think the point here is to be able to use a lightweight modern router, as opposed to something like Sammy.
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 🙂