diff --git a/docs/api.md b/docs/api.md index e3e55ea..3914747 100644 --- a/docs/api.md +++ b/docs/api.md @@ -33,6 +33,61 @@ Where `action` is just a regular function that may, or may not, return any arbit — a string, a React component, anything! +## `href(routes, routeName[, routeParams])` ⇒ `String|null` + +Traverses the list of routes in the order they are defined until it finds the first route that +matches provided name string. + +```js +// ./routes/index.js +import { href } from 'universal-router'; + +const routes = { + path: '/', + children: [ + { name: 'one', path: '/one', action: () => {} }, + { path: '/two', action: () => {}, children: [ + { path: '/three', action: () => {} } + { name: 'four', path: '/four/:four', action: () => {} } + ] } + ] +}; + +export default routes; + +console.log(href(routes, 'one')); +// => /one + +console.log(href(routes, 'three')); +// => null + +console.log(href(routes, 'four', { four: 'a' })); +// => /two/four/a +``` + +```js +// ./components/Link/index.js +import React from 'react'; +import { href } from 'universal-router'; +import routes from '../../routes'; + +const Link = ({ routeName, routeParams, children }) => ( + {children} +); + +// ./components/Navigation.js +import React from 'react'; +import Link from './components/Link'; + +const Navigation = () => ( + +); +``` + + ## Nested Routes Each route may have an optional `children: [ ... ]` property containing the list of child routes: diff --git a/docs/getting-started.md b/docs/getting-started.md index 2ae1b5b..746189e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -54,3 +54,26 @@ resolve(routes, { path: '/one' }).then(component => { // renders:

Page One

}); ``` + +## Use href method to generate a path by name + +```js +import React from 'react'; +import { href } from 'universal-router'; + +const routes = [ + { name: 'one', path: '/one', action: () =>

Page One

}, + { name: 'two', path: '/two/:two', action: () =>

Page Two

}, + { path: '*', action: () =>

Not Found

} +]; + +const LinkOne = () => ( + LinkOne +); +// LinkOne + +const LinkTwo = () => ( + LinkTwo +); +// LinkTwo +``` diff --git a/src/href.js b/src/href.js new file mode 100644 index 0000000..36cbb4f --- /dev/null +++ b/src/href.js @@ -0,0 +1,44 @@ +/** + * Universal Router (https://www.kriasoft.com/universal-router/) + * + * Copyright © 2015-2016 Konstantin Tarkus, Kriasoft LLC. All rights reserved. + * + * This source code is licensed under the Apache 2.0 license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +import toRegExp from 'path-to-regexp'; + +function resolvePath(route, routeName) { + const routePath = (route.path === '/') ? '' : route.path; + + if (route.name && route.name === routeName) { + return routePath; + } + + if (route.children) { + for (let i = 0; i < route.children.length; i += 1) { + let resultPath = resolvePath(route.children[i], routeName); + + if (resultPath !== null) { + resultPath = routePath + resultPath; + return resultPath.startsWith('/') ? resultPath : `/${resultPath}`; + } + } + } + + return null; +} + +function href(routes, routeName, routeParams = {}, options = { pretty: false }) { + const root = Array.isArray(routes) ? { path: '/', children: routes } : routes; + const path = resolvePath(root, routeName); + + if (path === null) { + return null; + } + + return toRegExp.compile(path)(routeParams, options); +} + +export default href; diff --git a/src/main.js b/src/main.js index d34d890..557b050 100644 --- a/src/main.js +++ b/src/main.js @@ -8,6 +8,7 @@ */ import resolve from './resolve'; +import href from './href'; -export { resolve }; -export default { resolve }; +export { resolve, href }; +export default { resolve, href }; diff --git a/test/href.spec.js b/test/href.spec.js new file mode 100644 index 0000000..6c99354 --- /dev/null +++ b/test/href.spec.js @@ -0,0 +1,90 @@ +/** + * Universal Router (https://www.kriasoft.com/universal-router/) + * + * Copyright © 2015-2016 Konstantin Tarkus, Kriasoft LLC. All rights reserved. + * + * This source code is licensed under the Apache 2.0 license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +import { expect } from 'chai'; +import { href } from '../src/main'; + +const emptyRoutes = []; +const simpleRoutes = [{ name: 'root', path: '/', children: [{ name: 'a', path: '/a' }] }]; +const nestRoutes = [{ + name: 'root', + path: '/', + children: [ + { name: 'a', path: '/a/:a' }, + { name: 'b', path: '/b/:b?' }, + { + name: 'c', + path: '/c/:c(\\d+)?', + children: [ + { + name: 'd', + path: '/d', + children: [ + { name: 'f', path: '/f/:f' }, + ], + }, + ], + }, + { name: 'a', path: '/never-mounted-by-name' }, + ], +}]; + +describe('href(routes, routeName, routeParams, [options = { pretty: false }])', () => { + + it('should return false if no route found', async () => { + let path; + + path = href(emptyRoutes, 'z'); + expect(path).to.be.null; + + path = href(simpleRoutes, 'z'); + expect(path).to.be.null; + + path = href(nestRoutes, 'z'); + expect(path).to.be.null; + }); + + it('should find and mount the first route by name', () => { + let path; + + path = href(simpleRoutes, 'root'); + expect(path).to.be.equal('/'); + + path = href(nestRoutes, 'root'); + expect(path).to.be.equal('/'); + + path = href(nestRoutes, 'a', { a: 'a' }); + expect(path).to.be.equal('/a/a'); + + path = href(nestRoutes, 'b'); + expect(path).to.be.equal('/b'); + + path = href(nestRoutes, 'b', { b: 'b' }); + expect(path).to.be.equal('/b/b'); + + path = href(nestRoutes, 'c'); + expect(path).to.be.equal('/c'); + + path = href(nestRoutes, 'c', { c: '1' }); + expect(path).to.be.equal('/c/1'); + + path = href(nestRoutes, 'd'); + expect(path).to.be.equal('/c/d'); + + path = href(nestRoutes, 'd', { c: '1' }); + expect(path).to.be.equal('/c/1/d'); + + path = href(nestRoutes, 'f', { f: 'f' }); + expect(path).to.be.equal('/c/d/f/f'); + + path = href(nestRoutes, 'f', { c: '1', f: 'f' }); + expect(path).to.be.equal('/c/1/d/f/f'); + }); + +});