-
-
Save noxecane/95578807237664365eb7 to your computer and use it in GitHub Desktop.
Simple History.js-based client-side router
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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