Bring in the router
This commit is contained in:
parent
78f44eae43
commit
e6e44beeee
@ -13,6 +13,10 @@ state.
|
|||||||
|
|
||||||
A utility to expose an asynchronous API between a web worker and its parent.
|
A utility to expose an asynchronous API between a web worker and its parent.
|
||||||
|
|
||||||
|
## [Router](./packages/router/README.md)
|
||||||
|
|
||||||
|
A slim and unopinionated router.
|
||||||
|
|
||||||
## [Trimkit](./packages/trimkit/README.md)
|
## [Trimkit](./packages/trimkit/README.md)
|
||||||
|
|
||||||
Javascript API abstractions to enhance minification by substituting variables. It's really quite
|
Javascript API abstractions to enhance minification by substituting variables. It's really quite
|
||||||
|
|||||||
48
packages/router/README.md
Normal file
48
packages/router/README.md
Normal 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');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
42
packages/router/package.json
Normal file
42
packages/router/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/router/rollup.config.js
Normal file
10
packages/router/rollup.config.js
Normal 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'
|
||||||
|
};
|
||||||
140
packages/router/src/index.js
Normal file
140
packages/router/src/index.js
Normal 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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user