diff --git a/packages/router/README.md b/packages/router/README.md index e42f057..59ba52a 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -11,8 +11,8 @@ navigated. Parameters are: api (Router instance) badpath (string) lastGoodRoute 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 +vars) - build a path based on the name and supplied vars start(initialRoute) - listen to +window.onhashchange for route changes current() - get the current route descriptor object Example: @@ -45,4 +45,5 @@ const router = Router( r.goto(lastGoodRoute.path || 'home'); } ); +router.start('/'); ``` diff --git a/packages/router/package.json b/packages/router/package.json index f21271a..6e29e86 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "router", - "version": "1.0.0", + "version": "1.1.0", "description": "A slim and unopinionated router", "main": "lib/index.js", "jsnext:main": "src/index.js", @@ -14,7 +14,8 @@ "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" + "prepublish": "npm run clean && npm run build", + "test": "npm run build:lib && jasmine --verbose" }, "repository": { "type": "git", @@ -41,6 +42,7 @@ "uglifyjs": "2.4.10" }, "dependencies": { + "jasmine": "^2.5.3", "trimkit": "^1.0.2" } } diff --git a/packages/router/spec/helpers/globals.js b/packages/router/spec/helpers/globals.js new file mode 100644 index 0000000..c75fc65 --- /dev/null +++ b/packages/router/spec/helpers/globals.js @@ -0,0 +1,3 @@ +self = window = { + location: {} +}; diff --git a/packages/router/spec/router.spec.js b/packages/router/spec/router.spec.js new file mode 100644 index 0000000..c23326b --- /dev/null +++ b/packages/router/spec/router.spec.js @@ -0,0 +1,91 @@ +const { Router } = require('../lib/index.js'); + +describe('router builds urls', () => { + describe('for hashed routes', () => { + const router = Router( + '#', + { + home: { + path: '/', + onenter: (r, route) => {}, + onexit: (r, route, newRoute) => {} + }, + article: { + path: '/article/:id', + vars: { id: /[a-f0-9]{6,40}/ }, + onenter: (r, route) => {}, + onexit: (r, route, newRoute) => {} + } + }, + (r, path, lastGoodRoute) => {} + ); + + it('at the root', () => { + expect(router.href('home')).toEqual('#/'); + }); + + it('with variables', () => { + expect(router.href('article', { id: 156234 })).toEqual('#/article/156234'); + }); + + describe('but throws an error', () => { + it("if the route doesn't exist", () => { + expect(() => { + router.href('artcle', { id: 156 }); + }).toThrowError(Error, 'Invalid route artcle.'); + }); + + it("if the vars don't match", () => { + expect(() => { + router.href('article', { id: 156 }); + }).toThrowError(Error, 'Invalid value for route /article/:id var id: 156.'); + }); + }); + }); +}); + +describe('router goes to routes', () => { + describe('for hashed routes', () => { + const router = Router( + '#', + { + home: { + path: '/', + onenter: (r, route) => {}, + onexit: (r, route, newRoute) => {} + }, + article: { + path: '/article/:id', + vars: { id: /[a-f0-9]{6,40}/ }, + onenter: (r, route) => {}, + onexit: (r, route, newRoute) => {} + } + }, + (r, path, lastGoodRoute) => {} + ); + it('at the root', done => { + router.goto('home').then(() => { + const current = router.current(); + expect(current.name).toEqual('home'); + expect(current.path).toEqual('#/'); + done(); + }); + }); + it('with vars', done => { + router.goto('article', { id: 156234 }).then(() => { + const current = router.current(); + expect(current.name).toEqual('article'); + expect(current.path).toEqual('#/article/156234'); + done(); + }); + }); + it('with a url', done => { + router.goto('#/article/156233').then(() => { + const current = router.current(); + expect(current.name).toEqual('article'); + expect(current.path).toEqual('#/article/156233'); + done(); + }); + }); + }); +}); diff --git a/packages/router/spec/support/jasmine.json b/packages/router/spec/support/jasmine.json new file mode 100644 index 0000000..b52c9e6 --- /dev/null +++ b/packages/router/spec/support/jasmine.json @@ -0,0 +1,7 @@ +{ + "spec_dir": "spec", + "spec_files": ["**/*[sS]pec.js"], + "helpers": ["helpers/**/*.js"], + "stopSpecOnExpectationFailure": false, + "random": false +} diff --git a/packages/router/src/index.js b/packages/router/src/index.js index bc998a2..b0b39e5 100644 --- a/packages/router/src/index.js +++ b/packages/router/src/index.js @@ -1,16 +1,10 @@ import { isFunction, isUndefined, Null, ObjectKeys } from 'trimkit'; -function nop() {} - -export function Router(baseUrl, routes, unmatched) { - let listening = false; - let currentRoute = Null; - let reEnterHook = Null; - - const VARMATCH_RE = /:([^\/]+)/g; - const ROUTEELEMENT_RE = /^[^\/]+$/; - - const routeMatcher = ObjectKeys(routes).map(name => { +const VARMATCH_RE = /:([^\/]+)/g; +const ROUTEELEMENT_RE = /^[^\/]+$/; +const nop = () => 1; +const digestRoutes = (routes, baseUrl) => + ObjectKeys(routes).map(name => { const route = routes[name]; const reg = route.path.replace(VARMATCH_RE, (m, varName) => { const varDef = route.vars || {}; @@ -25,6 +19,13 @@ export function Router(baseUrl, routes, unmatched) { }; }); +export function Router(baseUrl, routes, unmatched) { + let listening = false; + let currentRoute = Null; + let reEnterHook = Null; + + let routeMatcher = digestRoutes(routes, baseUrl); + function goto(urlOrName, vars) { const url = urlOrName.startsWith(baseUrl) ? urlOrName : href(urlOrName, vars); @@ -69,10 +70,12 @@ export function Router(baseUrl, routes, unmatched) { 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; - }); + return Promise.resolve(onexit(api, currentRoute, newRoute)) + .catch(() => {}) + .then(() => { + reEnterHook = routes[routeMatch.name].onenter(api, newRoute); + currentRoute = newRoute; + }); } } else if (unmatched) { unmatched(api, url, currentRoute); @@ -111,16 +114,24 @@ export function Router(baseUrl, routes, unmatched) { return location.hash; } - function listen(initialRoute) { + function _handler() { + goto(_location()); + } + + function start(initialRoute) { if (listening) { return; } - self.addEventListener('hashchange', () => goto(_location()), false); + self.addEventListener('hashchange', _handler, false); listening = true; goto(_location() || initialRoute); } + function stop() { + self.removeEventListener('hashchange', _handler); + } + function current() { return currentRoute; } @@ -128,7 +139,8 @@ export function Router(baseUrl, routes, unmatched) { const api = { goto, href, - listen, + start, + stop, current };