Bring in the router

This commit is contained in:
Timothy Farrell 2017-01-20 23:20:34 -06:00
commit 2704eb8180
4 changed files with 240 additions and 0 deletions

48
packages/router/README.md Normal file
View File

@ -0,0 +1,48 @@
Usage:
Router(rootURL, routeSpec, onUnmatched):
rootURL (string) the url prefix of all paths.
routeSpec (object)
onUnmatched (optional function(api, badPath, lastGoodRoute)) Called whenever an unmatched route is
navigated. Parameters are: api (Router instance) badpath (string) lastGoodRoute (object) a route
descriptor object for the last good route.
returns an api object: goto(path) - match the path to a route and navigate there href(pathName,
vars) - build a path based on the name and supplied vars listen() - listen to window.onhashchange
for route changes current() - get the current route descriptor object
Example:
```js
const router = Router(
'#',
{
home: {
path: '/',
onenter: (r, route) => {
console.log('At application home');
},
onexit: (r, route, newRoute) => {
console.log(`Exiting from ${route.path} to ${newRoute.path}`);
}
},
article: {
path: '/article/:id',
vars: { id: /[a-f0-9]{6,40}/ },
onenter: (r, route) => {
console.log('Opening Article', route.vars.id);
},
onexit: (r, route, newRoute) => {
console.log('Closing Article', route.vars.id);
}
}
},
(r, path, lastGoodRoute) => {
alert(`Unmatched route "${path}"`);
r.goto(lastGoodRoute.path || 'home');
}
);
```

View File

@ -0,0 +1,42 @@
{
"name": "router",
"version": "1.0.0",
"description": "A slim and unopinionated router",
"main": "lib/index.js",
"jsnext:main": "src/index.js",
"files": ["dist", "lib", "src"],
"scripts": {
"lint": "eslint src",
"clean": "rimraf dist lib",
"build:lib": "NODE_ENV=production babel src --presets=\"stage-0,es2015\" --out-dir lib",
"build:umd": "npm run build:lib && NODE_ENV=production rollup -c",
"build:umd:min":
"npm run build:umd && uglifyjs -m --screw-ie8 -c -o dist/router.min.js dist/router.js",
"build:umd:gzip": "npm run build:umd:min && gzip -c9 dist/router.min.js > dist/router.min.js.gz",
"build": "npm run build:umd:gzip && ls -l dist/",
"prepublish": "npm run clean && npm run build"
},
"repository": {
"type": "git",
"url": "https://gitlab.com/explorigin/router.git"
},
"keywords": ["javascript"],
"author": "Timothy Farrell <tim@thecookiejar.me> (https://github.com/explorigin)",
"license": "Apache-2.0",
"bugs": {
"url": "https://gitlab.com/explorigin/router/issues"
},
"homepage": "https://gitlab.com/explorigin/router",
"devDependencies": {
"babel-cli": "6.18.0",
"babel-core": "6.21.0",
"babel-eslint": "7.1.1",
"babel-preset-es2015": "6.18.0",
"babel-preset-es2015-rollup": "3.0.0",
"babel-preset-stage-0": "6.16.0",
"eslint": "3.2.0",
"rimraf": "2.5.4",
"rollup-plugin-json": "2.1.0",
"uglifyjs": "2.4.10"
}
}

View File

@ -0,0 +1,10 @@
import json from 'rollup-plugin-json';
import babel from 'rollup-plugin-babel';
export default {
entry: 'src/index.js',
format: 'umd',
moduleName: 'Router',
plugins: [json(), babel(babelConfig)],
dest: 'dist/router.js'
};

View File

@ -0,0 +1,140 @@
import { isFunction, isUndefined, Null, ObjectKeys } from 'trimkit';
function nop() {}
export function Router(
baseUrl: string,
routes: RouteSpecType,
unmatched: UnmatchedFunctionType
): RouterApiType {
let listening = false;
let currentRoute = Null;
let reEnterHook = Null;
const VARMATCH_RE = /:([^\/]+)/g;
const ROUTEELEMENT_RE = /^[^\/]+$/;
const routeMatcher = ObjectKeys(routes).map(name => {
const route = routes[name];
const reg = route.path.replace(VARMATCH_RE, (m, varName) => {
const varDef = route.vars || {};
const regExStr = '' + (varDef[varName] || /[^\/]+/);
return '(' + regExStr.substring(1, regExStr.lastIndexOf('/')) + ')';
});
return {
matcher: new RegExp(`^${baseUrl}${reg}$`),
name,
...route
};
});
function goto(urlOrName: string, vars?: RouteVarsType) {
const url = urlOrName.startsWith(baseUrl) ? urlOrName : href(urlOrName, vars);
if (listening && _location() !== url) {
// If we're not there make the change and exit.
location.hash = url;
return;
}
if (currentRoute && currentRoute.path === url) {
// This is only supposed to happen when recovering from a bad path.
return;
}
let routeVars = {};
const routeMatch = routeMatcher.find(({ matcher, path }) => {
const match = url.match(matcher);
if (!match) {
return false;
}
if (path.indexOf(':') !== -1) {
match.shift();
path.replace(VARMATCH_RE, (_, varName) => {
// We're abusing RegExp.replace here.
routeVars[varName] = match.shift();
});
}
return true;
});
if (routeMatch) {
let newRoute = {
name: routeMatch.name,
vars: routeVars,
path: url
};
if (currentRoute && currentRoute.name === newRoute.name && isFunction(reEnterHook)) {
reEnterHook(newRoute);
currentRoute = newRoute;
} else {
let onexit = currentRoute && currentRoute.name ? routes[currentRoute.name].onexit || nop : nop;
Promise.resolve(onexit(api, currentRoute, newRoute)).then(() => {
reEnterHook = routes[routeMatch.name].onenter(api, newRoute);
currentRoute = newRoute;
});
}
} else if (unmatched) {
unmatched(api, url, currentRoute);
} else {
if (currentRoute && listening) {
location.hash = currentRoute.path;
return;
}
throw new Error(`No route for "${url}"`);
}
}
function href(routeName: string, vars: RouteVarsType = {}): string {
const route = routes[routeName];
if (!route) {
throw new Error(`Invalid route ${routeName}.`);
}
let path = '' + route.path;
if (route.vars) {
path = path.replace(VARMATCH_RE, (_, varName) => {
let value = vars[varName];
if ((route.vars[varName] || ROUTEELEMENT_RE).test(value)) {
value = isUndefined(value) ? '' : '' + value;
return value;
}
throw new Error(`Invalid value for route ${path} var ${varName}: ${value}.`);
});
}
return `${baseUrl}${path}`;
}
function _location(): string {
return location.hash;
}
function listen(initialRoute: string): void {
if (listening) {
return;
}
self.addEventListener('hashchange', () => goto(_location()), false);
listening = true;
goto(_location() || initialRoute);
}
function current(): RouteInfoType | null {
return currentRoute;
}
const api = {
goto,
href,
listen,
current
};
return api;
}