lucid-router
Version:
a simple html5-history aware router
213 lines (186 loc) • 6.2 kB
JavaScript
/* @flow */
import UrlPattern from 'url-pattern';
var window: any = global.window;
var history: History = global.history;
var hasHistoryApi: boolean = (
window !== undefined &&
history !== undefined &&
typeof history.pushState === 'function'
);
var locationChangeCallbacks: Array<NavigationCallback> = [];
var routes: Array<Route> = [];
export function addRoutes(newRoutes: ?Array<RouteSpec>): void {
if (!(newRoutes instanceof Array)) throw typeError(routes, 'lucid-router expects to be passed a routing array as its first parameter');
for (var route of newRoutes) {
if (route === null || !(route instanceof Object)) throw typeError(routes, 'lucid-router expects each route definition to be an object');
route.path = route.path || null;
route.name = route.name || null;
route.external = typeof route.external === 'function'
? route.external
: !!route.external;
try {
route.pattern = new UrlPattern(route.path);
} catch (err) {
throw typeError(route.path, 'lucid-router expects route paths to be a string or regex expression');
}
routes.push(route);
}
}
export function removeRoute(name: string): void {
var idx = -1;
for (var i = 0, l = routes.length; i < l; i++) {
if (routes[i].name === name) {
idx = i;
break;
}
}
~idx && routes.splice(idx, 1);
}
function parseQuery(query) {
var queryArgs = {};
if (query) {
query.split('&')
.filter(keyValStr => !!keyValStr)
.map(keyValStr => keyValStr.split('=')
.map(encoded => decodeURIComponent(encoded)))
.forEach(([key,val]) => key && (queryArgs[key] = val));
}
return queryArgs;
}
export function match(path: string): ?RouterMatch {
var [pathnameAndQuery,hashAndHashQuery] = path.split('#');
var [pathname,search] = pathnameAndQuery.split('?');
var [hash,hashSearch] = hashAndHashQuery
? hashAndHashQuery.split('?')
: [];
var queryState = parseQuery([search,hashSearch].join('&'));
for (var route of routes) {
var matchState = route.pattern.match(pathname);
if (!matchState) continue;
return {
route,
pathname,
search: search ? '?'.concat(search) : '',
hash: hash ? '#'.concat(hash) : '',
hashSearch: hashSearch ? '?'.concat(hashSearch) : '',
state: {...queryState, ...matchState}
};
}
return null;
}
export function navigate(path: ?string, e?: Event, replace?: boolean): void {
path = getFullPath(path || '');
if (hasHistoryApi) {
if (typeof path !== 'string' || !path) throw typeError(path, 'lucid-router.navigate expected a non empty string as its first parameter');
var m = match(path);
if (m && notExternal(m)) {
var location: ?RouterLocation = matchAndPathToLocation(m, path);
if (replace) {
history.replaceState(null, '', path);
} else {
history.pushState(null, '', path);
}
if (e && e.preventDefault) {
e.preventDefault();
}
onLocationChange(location);
return;
}
}
if (window) {
if (!e) window.location = path;
else {
const target = ((e.target : any) : ?Element);
if (!target || target.tagName !== 'A') {
window.location = path;
}
}
}
}
export function navigatorFor(path: string, replace?: bool): NavigationCallback {
return e => navigate(path, e, replace);
}
export function pathFor(routeName: string, params?: Object): string {
for (var route of routes) {
if (route.name === routeName) {
return route.pattern.stringify(params);
}
}
throw new Error(`lucid-router.pathFor failed to find a route with the name '${routeName}'`);
}
export function navigateToRoute(routeName: string, params?: Object, e?: Event): void {
navigate(pathFor(routeName, params), e);
}
export function navigatorForRoute(routeName: string, params?: Object): NavigationCallback {
return e => navigateToRoute(routeName, params, e);
}
export function register(callback: RouteMatchCallback): UnregisterLocationChangeCallback {
if (typeof callback !== 'function') throw typeError(callback, 'lucid-router.register expects to be passed a callback function');
locationChangeCallbacks.push(callback);
return function unregister() {
var idx = locationChangeCallbacks.indexOf(callback);
~idx && locationChangeCallbacks.splice(idx, 1);
};
}
function onLocationChange(location: ?RouterLocation): void {
locationChangeCallbacks.forEach(cb => cb(location));
}
function getFullPath(path: string): string {
if (window) {
var a: HTMLAnchorElement = window.document.createElement('a');
a.href = path;
if (!a.host) a.href = a.href; /* IE hack */
if (a.hostname === window.location.hostname) {
path = a.pathname + a.search + a.hash;
if (path[0] !== '/') { /* more IE hacks */
path = '/' + path;
}
} else {
path = a.href;
}
}
return path;
}
function getWindowPathAndQuery(): ?string {
var {location} = window;
if (!location) return null;
return location.pathname + location.search + location.hash;
}
export function getLocation(path: ?string): ?RouterLocation {
path = path || getWindowPathAndQuery() || '';
var m: ?RouterMatch = match(path);
return matchAndPathToLocation(m, path);
}
function matchAndPathToLocation(m: ?RouterMatch, p: string): ?RouterLocation {
return !m
? null
: {
path: p,
name: m.route.name,
pathname: m.pathname,
search: m.search,
hash: m.hash,
hashSearch: m.hashSearch,
state: m.state,
route: m.route
};
}
function notExternal(m: RouterMatch): boolean {
var {external} = m.route;
if (typeof external === 'function') {
return !external(m);
} else return !external;
}
if (hasHistoryApi && window) {
window.addEventListener('popstate', function(e: Event) {
var path = getWindowPathAndQuery() || '';
var m: ?RouterMatch = match(path);
if (m && notExternal(m)) {
var location = matchAndPathToLocation(m, path);
onLocationChange(location);
}
}, false);
}
function typeError(type: any, msg: string): TypeError {
return new TypeError(msg + ' but got type `' + typeof type + '`!');
}