Skip to content

Instantly share code, notes, and snippets.

@noxecane
Forked from jbroadway/app.js
Last active January 25, 2016 21:10
Show Gist options
  • Save noxecane/95578807237664365eb7 to your computer and use it in GitHub Desktop.
Save noxecane/95578807237664365eb7 to your computer and use it in GitHub Desktop.
Simple History.js-based client-side router
'use strict';
import {Event} from './event.es6';
import * as func from './func.es6';
import * as classes from './classes.es6';
const paramReg = /:(\w+)/g;
const slashReg = /\//g;
const paramRepl = '(.*?)';
const slashRepl = '\\/';
const linkNormalizer = document.createElement('a');
const allRoutes = {};
const eventName = 'view';
var currentUrl = null;
/**
* Event emmiter for all routes. Supports start, end and *(no route found)
* @type {Event}
*/
export var events = new Event();
export var resolve = state => Promise.resolve(true);
export class Route extends Event {
/**
* @param {String||Regexp} url representation of the route. It could be a
* normal url or one with parameters(put ":" in
* front of the parameter names)
* @param {[[String]]} paramNames names of parameters in the url arranged with
* respect to how they appear in the url.s
* @param {[Function]} resolve any function that returns a promise(s) that needs
* to be fufilled before this route is fired.
* Failing this promise fires the routes error
* event and the global error event. This promise's
* resolved value will be added to data of the state.
*/
constructor(url, paramNames=null, resolve=null) {
super();
this.url = url instanceof RegExp ? url.source: url;
this.regex = url instanceof RegExp ? url: urlToRegex(url);
this.params = paramNames || recogniseParams(url);
this.resolve = typeof resolve === 'function' ?
resolve :
state => Promise.resolve(state.data);
}
/**
* Adds a listener that gets fired once the url and this route match
* @param {Function} cb route handler. It gets called with the url
* state(History) and parameters in that order.
*/
listen(cb) {
this.on(eventName, cb);
}
/**
* Fires all route handlers
* @param {Object} params mapping of parameter names to their values
* @param {State} state History state containing url, state data and state
* title.
*/
fire(state, params) {
this.emit(eventName, state, params);
}
/**
* Fires error event for this route
* @param {Object} params mapping of parameter names to their values
* @param {State} state History state containing url, state data and state
* title.
*/
error(state, params) {
this.emit('error', state, params);
}
}
/**
* Sets up an event handler so the router can listen to push
* state events. The router can work without this.
*/
export function start() {
History.Adapter.bind(window, 'statechange', () => {
changeRoute(History.getState());
});
}
/**
* @param {String} url route registered to go to.
* @param {Object} data to store with this state in case of back button
* @param {String} title What to put as title of the page(pretty irrelivant)
*/
export function go(url, data = {}, title = '') {
History.pushState(data, title, url);
}
/**
* Creates a route and registers the callback or registers the callback to a previous one.
* @param {String || RegExp} url representation of the route. It could be a
* normal url or one with parameters(put ":" in
* front of the parameter names)
* @param {Function} cb handler for the route
* @param {[[String]]} paramNames names of parameters in the url arranged with
* respect to how they appear in the urls
* @param {[Function]} resolve any function that returns a promise that needs
* to be fufilled before this route is fired.
* Failing this promise fires the routes error
* event and the global error event.
*/
export function on(url, cb, paramNames=null, resolve=null) {
var key = url instanceof RegExp ? url.source: url;
if (allRoutes.hasOwnProperty(key)) {
allRoutes[key].listen(cb);
} else {
let route = new Route(url, paramNames, resolve);
route.listen(cb);
allRoutes[key] = route;
}
}
/**
* Saves the current state of the url so new changes will can still
* be accessed when back button is pressed.()
* @param {State} state [description]
*/
export function save(state) {
History.replaceState(state.data, state.title, state.url);
}
function constructState(state) {
classes.makePrivate(state, 'save', func.partialLeft(save, state));
}
const fireRoute = func.asyncP(function* (route, state, params) {
try {
yield resolve(state, params);
let data = yield route.resolve(state, params);
classes.extend(state.data, data);
events.emit('start', state, params);
currentUrl = state.url;
route.fire(state, params);
events.emit('end', state, params);
} catch (e) {
state.error = e;
route.error(state, params);
events.emit('error', state, params);
}
});
function changeRoute(state) {
if (state.url === currentUrl) {return;}
constructState(state);
var path = normalizeLink(state.url);
for (let i in allRoutes) {
let route = allRoutes[i];
var m = match(route, path);
if (m) {
var params = func.isNothing(m) ? {} : m;
fireRoute(route, state, params);
return;
}
}
events.emit('*', state);
}
function urlToRegex(url) {
var modifiedUrl = url.replace(paramReg, paramRepl)
.replace(slashReg, slashRepl);
return new RegExp(`^${modifiedUrl}/?$`, 'g');
}
function recogniseParams(url) {
return (url.match(paramReg) || []).map(function(param) {
return param.substr(1);
});
}
function normalizeLink(url) {
linkNormalizer.href = url;
return `${linkNormalizer.pathname}${linkNormalizer.search}${linkNormalizer.hash}`;
}
function match(route, url) {
route.regex.lastIndex = 0;
var matches = route.regex.exec(url);
if (matches) {
if (matches.length > 1) {
matches = matches.shift();
let params = {};
route.params.map((name, i) => {
params[name] = matches[i];
});
return params;
}
return true;
}
return false;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment