├── .babelrc ├── .gitignore ├── .travis.yml ├── README.md ├── build └── index.js ├── lib └── index.js ├── package.json └── test ├── index.html ├── named-routes-test.js └── setup.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": "transform-es2015-modules-umd", 3 | "presets": ["react","es2015","stage-0","stage-1","stage-2","stage-3"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | sudo: false 5 | script: npm test 6 | branches: 7 | - master -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-router-named-routes 2 | 3 | [![Build Status](https://travis-ci.org/adamziel/react-router-named-routes.svg?branch=master)](https://travis-ci.org/adamziel/react-router-named-routes) 4 | 5 | This package provides support for named routes for React-Router 1, 2, 3, 4, and 5. 6 | 7 | The support for named routes was once part of a core, but got removed at some point: 8 | 9 | https://github.com/rackt/react-router/issues/1840#issue-105240108 10 | 11 | ## Installation 12 | 13 | `npm install react-router-named-routes` 14 | 15 | ## React-router 4.0+ 16 | 17 | React-router v4 changed the game and we no longer have a single config file with all `` components inside. Because of that we cannot map all routing patterns to resolve them based on their name. You will have to express all your routes using constants, like this: 18 | 19 | routes.js: 20 | ```javascript 21 | export const USER_SHOW = '/user/:id' 22 | ``` 23 | 24 | and then import it whenever you need to use named routes: 25 | 26 | ```javascript 27 | import { USER_SHOW } from 'routes'; 28 | import { formatRoute } from 'react-router-named-routes'; 29 | 30 | 31 | 32 | ``` 33 | 34 | Additional benefit of this approach is that you get all the juice of static analysis if you use tools like flow or typescript. 35 | 36 | (thanks to @Sawtaytoes for an idea) 37 | 38 | ## React-router 3.0 39 | 40 | Use `` component provided by this package instead 41 | of the one provided by `react-router`. This requires some refactoring but 42 | not that much: 43 | 44 | 1. Define all your routes in a single module. You probably do it like this anyway. 45 | 1. Use this package before you `render()` anything: 46 | 47 | ```js 48 | var routes = require("myproject/routes"); 49 | var {Link, NamedURLResolver} = require("react-router-named-routes"); 50 | NamedURLResolver.mergeRouteTree(routes, "/"); 51 | 52 | 53 | Edit 54 | Edit 55 | ``` 56 | 57 | 58 | ## React-router 2.0 and older 59 | 60 | 1. Define all your routes in a single module. You probably do it like this anyway. 61 | 1. Use this package before you `render()` anything: 62 | 63 | ```js 64 | var routes = require("myproject/routes"); 65 | var {FixNamedRoutesSupport} = require("react-router-named-routes"); 66 | FixNamedRoutesSupport(routes); 67 | ``` 68 | 69 | That's it, with three lines of code you saved yourself hours of refactoring! You may now use react-router just like in react-router 0.13: 70 | ```js 71 | 72 | 73 | Edit 74 | ``` 75 | 76 | ## Caveats 77 | 78 | This probably breaks the shiny new `async routes` feature introduced in ReactRouter `1.0.0`. 79 | If you come straight from 0.13 you don't use it anyway. 80 | 81 | ## Contributing 82 | 83 | A pull request or an issue report is always welcome. If this package saved you some 84 | time, starring this repo is a nice way to say "thank you". 85 | 86 | ## Advanced stuff and implementation details 87 | 88 | Named Routes are resolved by a simple class called `NamedURLResolverClass`. 89 | 90 | If you want some custom logic involved in resolving your named routes, or routes in general, 91 | you may extend the class `NamedURLResolverClass` from this package and replace the global resolver 92 | like this: 93 | 94 | ``` 95 | var {NamedURLResolverClass, setNamedURLResolver} = require("react-router-named-routes"); 96 | class CustomURLResolver extends NamedURLResolverClass { 97 | resolve(name, params) { 98 | // ...do some fancy stuff 99 | } 100 | } 101 | setNamedURLResolver(new CustomURLResolver()); 102 | ``` 103 | 104 | Also, a `` component will accept a `resolver` property if you don't want to use 105 | a default one for any reason: 106 | 107 | `` 108 | 109 | ## License 110 | 111 | New BSD and MIT. 112 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define(['exports', 'react', 'react-router', 'create-react-class'], factory); 4 | } else if (typeof exports !== "undefined") { 5 | factory(exports, require('react'), require('react-router'), require('create-react-class')); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(mod.exports, global.react, global.reactRouter, global.createReactClass); 11 | global.index = mod.exports; 12 | } 13 | })(this, function (exports, React, ReactRouter, createReactClass) { 14 | 'use strict'; 15 | 16 | Object.defineProperty(exports, "__esModule", { 17 | value: true 18 | }); 19 | 20 | var _extends = Object.assign || function (target) { 21 | for (var i = 1; i < arguments.length; i++) { 22 | var source = arguments[i]; 23 | 24 | for (var key in source) { 25 | if (Object.prototype.hasOwnProperty.call(source, key)) { 26 | target[key] = source[key]; 27 | } 28 | } 29 | } 30 | 31 | return target; 32 | }; 33 | 34 | function _objectWithoutProperties(obj, keys) { 35 | var target = {}; 36 | 37 | for (var i in obj) { 38 | if (keys.indexOf(i) >= 0) continue; 39 | if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; 40 | target[i] = obj[i]; 41 | } 42 | 43 | return target; 44 | } 45 | 46 | var OriginalLink = ReactRouter.Link; 47 | 48 | 49 | // Deliberately not using ES6 classes - babel spits out too much boilerplate 50 | // and I don't want to add a dependency on babel 51 | // runtime 52 | function NamedURLResolverClass() { 53 | this.routesMap = {}; 54 | } 55 | 56 | function toArray(val) { 57 | return Object.prototype.toString.call(val) !== '[object Array]' ? [val] : val; 58 | } 59 | 60 | // Cached regexps: 61 | 62 | var reRepeatingSlashes = /\/+/g; // "/some//path" 63 | var reSplatParams = /\*{1,2}/g; // "/some/*/complex/**/path" 64 | var reResolvedOptionalParams = /\(([^:*?#]+?)\)/g; // "/path/with/(resolved/params)" 65 | var reUnresolvedOptionalParams = /\([^:?#]*:[^?#]*?\)/g; // "/path/with/(groups/containing/:unresolved/optional/:params)" 66 | var reUnresolvedOptionalParamsRR4 = /(\/[^\/]*\?)/g; // "/path/with/groups/containing/unresolved?/optional/params?" 67 | var reTokens = /<(.*?)>/g; 68 | var reSlashTokens = /_!slash!_/g; 69 | 70 | NamedURLResolverClass.prototype.resolve = function (name, params) { 71 | if (name && name in this.routesMap) { 72 | var routePath = this.routesMap[name]; 73 | return formatRoute(routePath, params); 74 | } 75 | 76 | return name; 77 | }; 78 | 79 | NamedURLResolverClass.prototype.mergeRouteTree = function (routes) { 80 | var _this = this; 81 | 82 | var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; 83 | 84 | routes = toArray(routes); 85 | 86 | routes.forEach(function (route) { 87 | if (!route) return; 88 | 89 | var newPrefix = ""; 90 | if (route.props) { 91 | var routePath = route.props.path || ""; 92 | var newPrefix = (routePath != null && routePath[0] === "/" ? routePath : [prefix, routePath].filter(function (x) { 93 | return x; 94 | }).join("/")).replace(reRepeatingSlashes, "/"); 95 | if (route.props.name) { 96 | _this.routesMap[route.props.name] = newPrefix; 97 | } 98 | 99 | React.Children.forEach(route.props.children, function (child) { 100 | _this.mergeRouteTree(child, newPrefix); 101 | }); 102 | } 103 | }); 104 | }; 105 | 106 | NamedURLResolverClass.prototype.reset = function () { 107 | this.routesMap = {}; 108 | }; 109 | 110 | var NamedURLResolver = new NamedURLResolverClass(); 111 | 112 | var Link = createReactClass({ 113 | render: function render() { 114 | var _props = this.props, 115 | to = _props.to, 116 | resolver = _props.resolver, 117 | params = _props.params, 118 | rest = _objectWithoutProperties(_props, ['to', 'resolver', 'params']); 119 | 120 | if (!resolver) resolver = NamedURLResolver; 121 | 122 | var finalTo = resolveTo(resolver, to, params); 123 | return React.createElement(OriginalLink, _extends({ to: finalTo }, rest)); 124 | } 125 | }); 126 | 127 | function resolveTo(resolver, to, params) { 128 | if (typeof to === "string") { 129 | return resolver.resolve(to, params); 130 | } 131 | 132 | if (typeof to === "function") { 133 | return function (location) { 134 | return resolveTo(resolver, to(location), params); 135 | }; 136 | } 137 | 138 | if (!to.name) { 139 | return to; 140 | } 141 | 142 | if (to.pathname) { 143 | throw new Error('Cannot specify both "pathname" and "name" options in location descriptor.'); 144 | } 145 | 146 | var name = to.name, 147 | rest = _objectWithoutProperties(to, ['name']); 148 | 149 | return _extends({}, rest, { 150 | pathname: resolver.resolve(name, params) 151 | }); 152 | } 153 | 154 | function MonkeyPatchNamedRoutesSupport(routes) { 155 | var basename = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "/"; 156 | 157 | NamedURLResolver.mergeRouteTree(routes, basename); 158 | ReactRouter.Link = Link; 159 | }; 160 | 161 | function setNamedURLResolver(resolver) { 162 | exports.NamedURLResolver = NamedURLResolver = resolver; 163 | }; 164 | 165 | function formatRoute(routePath, params) { 166 | if (params) { 167 | var tokens = {}; 168 | 169 | for (var paramName in params) { 170 | if (params.hasOwnProperty(paramName)) { 171 | var paramValue = params[paramName]; 172 | 173 | if (paramName === "splat") { 174 | // special param name in RR, used for "*" and "**" placeholders 175 | paramValue = toArray(paramValue); // when there are multiple globs, RR defines "splat" param as array. 176 | var i = 0; 177 | routePath = routePath.replace(reSplatParams, function (match) { 178 | var val = paramValue[i++]; 179 | if (val == null) { 180 | return ""; 181 | } else { 182 | var tokenName = 'splat' + i; 183 | tokens[tokenName] = match === "*" ? encodeURIComponent(val) 184 | // don't escape slashes for double star, as "**" considered greedy by RR spec 185 | : encodeURIComponent(val.toString().replace(/\//g, "_!slash!_")).replace(reSlashTokens, "/"); 186 | return '<' + tokenName + '>'; 187 | } 188 | }); 189 | } else { 190 | // Rougly resolve all named placeholders. 191 | // Cases: 192 | // - "/path/:param" 193 | // - "/path/(:param)" 194 | // - "/path(/:param)" 195 | // - "/path(/:param/):another_param" 196 | // - "/path/:param(/:another_param)" 197 | // - "/path(/:param/:another_param)" 198 | var paramRegex = new RegExp('(\/|\\(|\\)|^):' + paramName + '(\/|\\)|\\(|$)'); 199 | routePath = routePath.replace(paramRegex, function (match, g1, g2) { 200 | tokens[paramName] = encodeURIComponent(paramValue); 201 | return g1 + '<' + paramName + '>' + g2; 202 | }); 203 | var paramRegexRR4 = new RegExp('(.*):' + paramName + '\\?(.*)'); 204 | routePath = routePath.replace(paramRegexRR4, function (match, g1, g2) { 205 | tokens[paramName] = encodeURIComponent(paramValue); 206 | return g1 + '<' + paramName + '>' + g2; 207 | }); 208 | } 209 | } 210 | } 211 | } 212 | 213 | return routePath 214 | // Remove braces around resolved optional params (i.e. "/path/(value)") 215 | .replace(reResolvedOptionalParams, "$1") 216 | // Remove all sequences containing at least one unresolved optional param 217 | .replace(reUnresolvedOptionalParams, "") 218 | // Remove all sequences containing at least one unresolved optional param in RR4 219 | .replace(reUnresolvedOptionalParamsRR4, "") 220 | // After everything related to RR syntax is removed, insert actual values 221 | .replace(reTokens, function (match, token) { 222 | return tokens[token]; 223 | }) 224 | // Remove repeating slashes 225 | .replace(reRepeatingSlashes, "/") 226 | // Always remove ending slash for consistency 227 | .replace(/\/+$/, "") 228 | // If there was a single slash only, keep it 229 | .replace(/^$/, "/"); 230 | } 231 | 232 | var resolve = NamedURLResolver.resolve.bind(NamedURLResolver); 233 | 234 | exports.Link = Link; 235 | exports.NamedLink = Link; 236 | exports.NamedURLResolver = NamedURLResolver; 237 | exports.NamedURLResolverClass = NamedURLResolverClass; 238 | exports.MonkeyPatchNamedRoutesSupport = MonkeyPatchNamedRoutesSupport; 239 | exports.FixNamedRoutesSupport = MonkeyPatchNamedRoutesSupport; 240 | exports.setNamedURLResolver = setNamedURLResolver; 241 | exports.resolve = resolve; 242 | exports.formatRoute = formatRoute; 243 | }); 244 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | var React = require('react'); 3 | var ReactRouter = require('react-router'); 4 | var OriginalLink = ReactRouter.Link; 5 | var createReactClass = require('create-react-class'); 6 | 7 | // Deliberately not using ES6 classes - babel spits out too much boilerplate 8 | // and I don't want to add a dependency on babel 9 | // runtime 10 | function NamedURLResolverClass() { 11 | this.routesMap = {}; 12 | } 13 | 14 | function toArray(val) { 15 | return Object.prototype.toString.call(val) !== '[object Array]' ? [ val ] : val; 16 | } 17 | 18 | // Cached regexps: 19 | 20 | var reRepeatingSlashes = /\/+/g; // "/some//path" 21 | var reSplatParams = /\*{1,2}/g; // "/some/*/complex/**/path" 22 | var reResolvedOptionalParams = /\(([^:*?#]+?)\)/g; // "/path/with/(resolved/params)" 23 | var reUnresolvedOptionalParams = /\([^:?#]*:[^?#]*?\)/g; // "/path/with/(groups/containing/:unresolved/optional/:params)" 24 | var reUnresolvedOptionalParamsRR4 = /(\/[^\/]*\?)/g; // "/path/with/groups/containing/unresolved?/optional/params?" 25 | var reTokens = /<(.*?)>/g; 26 | var reSlashTokens = /_!slash!_/g; 27 | 28 | NamedURLResolverClass.prototype.resolve = function(name, params) { 29 | if(name && (name in this.routesMap)) { 30 | var routePath = this.routesMap[name]; 31 | return formatRoute(routePath, params); 32 | } 33 | 34 | return name; 35 | }; 36 | 37 | NamedURLResolverClass.prototype.mergeRouteTree = function(routes, prefix="") { 38 | routes = toArray(routes); 39 | 40 | routes.forEach((route) => { 41 | if(!route) return; 42 | 43 | var newPrefix = ""; 44 | if(route.props) { 45 | var routePath = (route.props.path || ""); 46 | var newPrefix = ((routePath != null && routePath[0] === "/") 47 | ? routePath 48 | : [prefix, routePath].filter(x=>x).join("/") 49 | ).replace(reRepeatingSlashes, "/"); 50 | if (route.props.name) { 51 | this.routesMap[route.props.name] = newPrefix; 52 | } 53 | 54 | React.Children.forEach(route.props.children, (child) => { 55 | this.mergeRouteTree(child, newPrefix); 56 | }); 57 | } 58 | }); 59 | }; 60 | 61 | NamedURLResolverClass.prototype.reset = function() { 62 | this.routesMap = {}; 63 | }; 64 | 65 | var NamedURLResolver = new NamedURLResolverClass(); 66 | 67 | var Link = createReactClass({ 68 | 69 | render() { 70 | var {to, resolver, params, ...rest} = this.props; 71 | if(!resolver) resolver = NamedURLResolver; 72 | 73 | var finalTo = resolveTo(resolver, to, params); 74 | return ; 75 | } 76 | 77 | }); 78 | 79 | function resolveTo(resolver, to, params) { 80 | if(typeof to === "string") { 81 | return resolver.resolve( 82 | to, 83 | params 84 | ); 85 | } 86 | 87 | if(typeof to === "function") { 88 | return function(location) { 89 | return resolveTo(resolver, to(location), params); 90 | }; 91 | } 92 | 93 | if(!to.name) { 94 | return to; 95 | } 96 | 97 | if(to.pathname) { 98 | throw new Error('Cannot specify both "pathname" and "name" options in location descriptor.'); 99 | } 100 | 101 | var {name, ...rest} = to; 102 | return { 103 | ...rest, 104 | pathname: resolver.resolve( 105 | name, 106 | params 107 | ) 108 | }; 109 | } 110 | 111 | 112 | function MonkeyPatchNamedRoutesSupport(routes, basename="/") { 113 | NamedURLResolver.mergeRouteTree(routes, basename); 114 | ReactRouter.Link = Link; 115 | }; 116 | 117 | function setNamedURLResolver(resolver) { 118 | NamedURLResolver = resolver; 119 | }; 120 | 121 | function formatRoute(routePath, params) { 122 | if (params) { 123 | var tokens = {}; 124 | 125 | for (var paramName in params) { 126 | if (params.hasOwnProperty(paramName)) { 127 | var paramValue = params[paramName]; 128 | 129 | if (paramName === "splat") { // special param name in RR, used for "*" and "**" placeholders 130 | paramValue = toArray(paramValue); // when there are multiple globs, RR defines "splat" param as array. 131 | var i = 0; 132 | routePath = routePath.replace(reSplatParams, (match) => { 133 | var val = paramValue[i++]; 134 | if (val == null) { 135 | return ""; 136 | } else { 137 | var tokenName = `splat${i}`; 138 | tokens[tokenName] = match === "*" 139 | ? encodeURIComponent(val) 140 | // don't escape slashes for double star, as "**" considered greedy by RR spec 141 | : encodeURIComponent(val.toString().replace(/\//g, "_!slash!_")).replace(reSlashTokens, "/"); 142 | return `<${tokenName}>`; 143 | } 144 | }); 145 | } else { 146 | // Rougly resolve all named placeholders. 147 | // Cases: 148 | // - "/path/:param" 149 | // - "/path/(:param)" 150 | // - "/path(/:param)" 151 | // - "/path(/:param/):another_param" 152 | // - "/path/:param(/:another_param)" 153 | // - "/path(/:param/:another_param)" 154 | var paramRegex = new RegExp('(\/|\\(|\\)|^):' + paramName + '(\/|\\)|\\(|$)'); 155 | routePath = routePath.replace(paramRegex, (match, g1, g2) => { 156 | tokens[paramName] = encodeURIComponent(paramValue); 157 | return `${g1}<${paramName}>${g2}`; 158 | }); 159 | var paramRegexRR4 = new RegExp('(.*):' + paramName + '\\?(.*)'); 160 | routePath = routePath.replace(paramRegexRR4, (match, g1, g2) => { 161 | tokens[paramName] = encodeURIComponent(paramValue); 162 | return `${g1}<${paramName}>${g2}`; 163 | }); 164 | } 165 | } 166 | } 167 | } 168 | 169 | return routePath 170 | // Remove braces around resolved optional params (i.e. "/path/(value)") 171 | .replace(reResolvedOptionalParams, "$1") 172 | // Remove all sequences containing at least one unresolved optional param 173 | .replace(reUnresolvedOptionalParams, "") 174 | // Remove all sequences containing at least one unresolved optional param in RR4 175 | .replace(reUnresolvedOptionalParamsRR4, "") 176 | // After everything related to RR syntax is removed, insert actual values 177 | .replace(reTokens, (match, token) => tokens[token]) 178 | // Remove repeating slashes 179 | .replace(reRepeatingSlashes, "/") 180 | // Always remove ending slash for consistency 181 | .replace(/\/+$/, "") 182 | // If there was a single slash only, keep it 183 | .replace(/^$/, "/"); 184 | } 185 | 186 | const resolve = NamedURLResolver.resolve.bind(NamedURLResolver); 187 | 188 | export { 189 | Link, 190 | Link as NamedLink, 191 | NamedURLResolver, 192 | NamedURLResolverClass, 193 | MonkeyPatchNamedRoutesSupport, 194 | MonkeyPatchNamedRoutesSupport as FixNamedRoutesSupport, 195 | 196 | setNamedURLResolver, 197 | 198 | resolve, 199 | formatRoute 200 | }; 201 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "adam@adamziel.com", 4 | "name": "Adam Zielinski", 5 | "url": "http://github.com/adamziel" 6 | }, 7 | "bugs": { 8 | "url": "https://github.com/adamziel/react-router-named-routes/issues" 9 | }, 10 | "dependencies": { 11 | "create-react-class": "^15.6.0" 12 | }, 13 | "description": "Adds support for named routes to React-Router 1, 2, 3, and 4.", 14 | "devDependencies": { 15 | "babel": "^6.0.15", 16 | "babel-cli": "^6.1.1", 17 | "babel-core": "^6.1.2", 18 | "babel-plugin-transform-es2015-modules-umd": "^6.0.15", 19 | "babel-preset-es2015": "^6.0.15", 20 | "babel-preset-react": "^6.0.15", 21 | "babel-preset-stage-0": "^6.0.15", 22 | "babel-preset-stage-1": "^6.0.15", 23 | "babel-preset-stage-2": "^6.0.15", 24 | "babel-preset-stage-3": "^6.0.15", 25 | "babel-runtime": "^6.0.14", 26 | "chai": "^3.4.0", 27 | "compare-versions": "^3.1.0", 28 | "history": "^3.3.0", 29 | "jsdom": "^7.0.2", 30 | "mocha": "^2.3.3", 31 | "mocha-babel": "^3.0.0", 32 | "mocha-loader": "^0.7.1", 33 | "node-localstorage": "^0.6.0", 34 | "react": "^0.14.2", 35 | "react-addons-test-utils": "^0.14.2", 36 | "react-dom": "^0.14.2", 37 | "react-router": "^3.0.5", 38 | "sessionstorage": "0.0.1" 39 | }, 40 | "peerDependencies": { 41 | "react": ">=0.14", 42 | "react-router": ">=3" 43 | }, 44 | "engines": { 45 | "node": ">=0.4.2" 46 | }, 47 | "homepage": "https://github.com/adamziel/react-router-named-routes/issues", 48 | "license": "BSD-3-Clause AND MIT", 49 | "main": "./build/index.js", 50 | "name": "react-router-named-routes", 51 | "optionalDependencies": {}, 52 | "readmeFilename": "README.md", 53 | "repository": { 54 | "type": "git", 55 | "url": "git+https://github.com/adamziel/react-router-named-routes.git" 56 | }, 57 | "scripts": { 58 | "test": "mocha --require test/setup.js", 59 | "test-browser": "echo 'open http://localhost:8080/webpack-dev-server/test'; webpack-dev-server 'mocha!./test/named-routes-test.js' --output-filename test.js", 60 | "build": "node_modules/.bin/babel --presets react,es2015,stage-0,stage-1,stage-2,stage-3 lib/index.js --out-file build/index.js" 61 | }, 62 | "version": "0.0.23" 63 | } 64 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/named-routes-test.js: -------------------------------------------------------------------------------- 1 | 2 | var React = require('react'); 3 | var ReactDOM = require('react-dom'); 4 | var ReactTestUtils = require('react-addons-test-utils'); 5 | var ReactRouter = require('react-router'); 6 | var Router = ReactRouter.Router; 7 | var Route = ReactRouter.Route; 8 | var IndexRoute = ReactRouter.IndexRoute; 9 | 10 | var ReactRouterNamedRoutes = require("../build/index"); 11 | var NamedURLResolverClass = ReactRouterNamedRoutes.NamedURLResolverClass; 12 | var NamedURLResolver = ReactRouterNamedRoutes.NamedURLResolver; 13 | var Link = ReactRouterNamedRoutes.Link; 14 | var formatRoute = ReactRouterNamedRoutes.formatRoute; 15 | var createBrowserHistory = require('history/lib/createBrowserHistory').default; 16 | 17 | var expect = require('chai').expect; 18 | 19 | var compareVersions = require('compare-versions'); 20 | var ReactRouterVersion = require('react-router/package.json').version; 21 | var preV4 = compareVersions(ReactRouterVersion, '4.0.0') === -1; 22 | 23 | if(preV4) { 24 | var Component = React.createClass({ 25 | render: function () { 26 | } 27 | }); 28 | 29 | var createComplexRouteTree = function () { 30 | return ( 31 | React.createElement(Route, {component: Component, name: 'root', path: '/'}, [ 32 | React.createElement(Route, {component: Component, path: '/app'}, [ 33 | React.createElement(IndexRoute, {name: 'app.index'}), 34 | React.createElement(Route, {name: 'app.list'}) 35 | ]), 36 | 37 | React.createElement(Route, {name: 'users', path: '/users'}, [ 38 | React.createElement(IndexRoute, {name: 'users.index'}), 39 | React.createElement(Route, {name: 'users.list', path: 'list'}), 40 | React.createElement(Route, {name: 'users.show', path: ':id'}), 41 | React.createElement(Route, {path: ':id/edit'}) 42 | ]) 43 | ]) 44 | ); 45 | }; 46 | 47 | describe('NamedURLResolver', function () { 48 | 49 | var resolver; 50 | beforeEach(() => { 51 | resolver = new NamedURLResolverClass(); 52 | }); 53 | 54 | it('correctly maps route tree #1', function () { 55 | resolver = new NamedURLResolverClass(); 56 | resolver.mergeRouteTree( 57 | React.createElement(Route, {}, [ 58 | React.createElement(Route, {name: 'users.show', path: '/users/:id'}), 59 | React.createElement(Route, {path: '/user/:id-parent/:id', name: 'test1'}), 60 | React.createElement(Route, {path: '/user/semi:colon/:colon', name: 'test2'}) 61 | ]) 62 | ); 63 | 64 | expect(resolver.routesMap).to.deep.equal({ 65 | 'users.show': '/users/:id', 66 | 'test1': '/user/:id-parent/:id', 67 | 'test2': '/user/semi:colon/:colon' 68 | }); 69 | }); 70 | 71 | it('correctly maps route tree #2', function () { 72 | resolver = new NamedURLResolverClass(); 73 | resolver.mergeRouteTree([ 74 | React.createElement(Route, {name: 'users.show', path: '/users/:id'}), 75 | React.createElement(Route, {path: '/user/:id-parent/:id', name: 'test1'}), 76 | React.createElement(Route, {path: '/user/semi:colon/:colon', name: 'test2'}) 77 | ]); 78 | 79 | expect(resolver.routesMap).to.deep.equal({ 80 | 'users.show': '/users/:id', 81 | 'test1': '/user/:id-parent/:id', 82 | 'test2': '/user/semi:colon/:colon' 83 | }); 84 | }); 85 | 86 | it('correctly maps route tree #3', function () { 87 | resolver = new NamedURLResolverClass(); 88 | resolver.mergeRouteTree(( 89 | React.createElement(Route, {component: Component, path: '/'}, [ 90 | React.createElement(Route, {component: Component}, [ 91 | React.createElement(IndexRoute, {component: Component}), 92 | React.createElement(Route, {name: 'deeply-nested', component: Component}) 93 | ]), 94 | React.createElement(Route, {path: '*', component: Component}), 95 | React.createElement(Route) 96 | ]) 97 | )); 98 | 99 | expect(resolver.routesMap).to.deep.equal({'deeply-nested': '/'}); 100 | }); 101 | 102 | it('correctly maps route tree #4', function () { 103 | resolver.mergeRouteTree(createComplexRouteTree()); 104 | expect(resolver.routesMap).to.deep.equal({ 105 | 'root': '/', 106 | 'app.index': '/app', 107 | 'app.list': '/app', 108 | 109 | 'users': '/users', 110 | 'users.index': '/users', 111 | 'users.list': '/users/list', 112 | 'users.show': '/users/:id' 113 | }); 114 | }); 115 | 116 | it('correctly maps absolute paths for nested routes', function () { 117 | resolver = new NamedURLResolverClass(); 118 | resolver.mergeRouteTree( 119 | React.createElement(Route, {path: "/users"}, [ 120 | React.createElement(Route, {name: 'users.show', path: '/:id'}), 121 | React.createElement(Route, {name: 'users.list', path: '/list'}) 122 | ]) 123 | ); 124 | 125 | expect(resolver.routesMap).to.deep.equal({ 126 | 'users.show': '/:id', 127 | 'users.list': '/list' 128 | }); 129 | 130 | expect(resolver.resolve("users.list")).to.equal("/list"); 131 | }); 132 | 133 | it('correctly resolve named routes', function () { 134 | resolver.mergeRouteTree(createComplexRouteTree()); 135 | expect(resolver.resolve("root")).to.equal("/"); 136 | expect(resolver.resolve("app.index")).to.equal("/app"); 137 | expect(resolver.resolve("app.list")).to.equal("/app"); 138 | 139 | expect(resolver.resolve("users")).to.equal("/users"); 140 | expect(resolver.resolve("users.index")).to.equal("/users"); 141 | expect(resolver.resolve("users.list")).to.equal("/users/list"); 142 | expect(resolver.resolve("users.show")).to.equal("/users/:id"); 143 | }); 144 | 145 | it('correctly formats named routes', function () { 146 | resolver.mergeRouteTree([ 147 | React.createElement(Route, {name: 'users.show', path: '/users/:id'}), 148 | React.createElement(Route, {path: '/users/:id-parent/:id', name: 'test1'}), 149 | React.createElement(Route, {path: '/users/semi:colon/:colon', name: 'test2'}) 150 | ]); 151 | 152 | expect(resolver.resolve("users.show", {id: 4})).to.equal("/users/4"); 153 | expect(resolver.resolve("test1", {id: 4, 'id-parent': 5})).to.equal("/users/5/4"); 154 | expect(resolver.resolve("test2", {colon: 7})).to.equal("/users/semi:colon/7"); 155 | 156 | expect(resolver.resolve("users.show", {id: 'id/:id'})).to.equal("/users/" + encodeURIComponent("id/:id")); 157 | expect(resolver.resolve("test1", { 158 | id: 'id/:id', 159 | 'id-parent': 'idp:id' 160 | })).to.equal("/users/" + encodeURIComponent("idp:id") + "/" + encodeURIComponent("id/:id")); 161 | expect(resolver.resolve("test2", {colon: 'colon:colon'})).to.equal("/users/semi:colon/" + encodeURIComponent("colon:colon")); 162 | }); 163 | 164 | it('correctly resolves optional params', function () { 165 | resolver.mergeRouteTree([ 166 | React.createElement(Route, {name: 'test1', path: '/path(/:param)'}), 167 | React.createElement(Route, {name: 'test2', path: '/path/(:param)'}), 168 | 169 | React.createElement(Route, {name: 'test3', path: '/path/(:param1)/:param2'}), 170 | React.createElement(Route, {name: 'test4', path: '/path/(:param1/):param2'}), 171 | React.createElement(Route, {name: 'test5', path: '/path(/:param1/):param2'}), 172 | React.createElement(Route, {name: 'test6', path: '/path(/:param1)/:param2'}), 173 | 174 | React.createElement(Route, {name: 'test7', path: '/path/:param1/(:param2)'}), 175 | React.createElement(Route, {name: 'test8', path: '/path/:param1(/:param2)'}), 176 | 177 | React.createElement(Route, {name: 'test9', path: '/path(/:param1/:param2/)'}), 178 | ]); 179 | 180 | expect(resolver.resolve("test1", {param: 1})).to.equal("/path/1", "Substitute case 1"); 181 | expect(resolver.resolve("test2", {param: 1})).to.equal("/path/1", "Substitute case 2"); 182 | 183 | expect(resolver.resolve("test1")).to.equal("/path", "Simple omit case 1"); 184 | expect(resolver.resolve("test2")).to.equal("/path", "Simple omit case 2"); 185 | 186 | expect(resolver.resolve("test3", {param2: 2})).to.equal("/path/2", "Middle omit case 1"); 187 | expect(resolver.resolve("test4", {param2: 2})).to.equal("/path/2", "Middle omit case 2"); 188 | expect(resolver.resolve("test5", {param2: 2})).to.equal("/path2", "Middle omit case 3"); 189 | expect(resolver.resolve("test6", {param2: 2})).to.equal("/path/2", "Middle omit case 4"); 190 | 191 | expect(resolver.resolve("test7", {param1: 1})).to.equal("/path/1", "Trailing omit case 1"); 192 | expect(resolver.resolve("test8", {param1: 1})).to.equal("/path/1", "Trailing omit case 2"); 193 | 194 | expect(resolver.resolve("test8", { 195 | param1: "p(1)", 196 | param2: "p(2)" 197 | })).to.equal("/path/p(1)/p(2)", "Params with parentheses"); 198 | 199 | // if any part of optional sequence is omitted, entire sequence is omitted 200 | expect(resolver.resolve("test9", {param1: 1})).to.equal("/path", "Omitted sequence"); 201 | expect(resolver.resolve("test9", {param1: 1, param2: 2})).to.equal("/path/1/2", "Resolved sequence"); 202 | }); 203 | 204 | it('correctly resolves splat params', function () { 205 | resolver.mergeRouteTree([ 206 | React.createElement(Route, {name: 'test1', path: '/some/*'}), 207 | React.createElement(Route, {name: 'test2', path: '/some/*/path'}), 208 | 209 | React.createElement(Route, {name: 'test3', path: '/some/**'}), 210 | React.createElement(Route, {name: 'test4', path: '/some/**/path'}), 211 | 212 | React.createElement(Route, {name: 'test5', path: '/some/*/path/**'}) 213 | ]); 214 | 215 | expect(resolver.resolve("test1", {splat: 1})).to.equal("/some/1", "Trailing single star"); 216 | expect(resolver.resolve("test2", {splat: 1})).to.equal("/some/1/path", "Middle single star"); 217 | expect(resolver.resolve("test1", {splat: "slash/here"})).to.equal("/some/" + encodeURIComponent("slash/here"), "Slash with single star"); 218 | 219 | expect(resolver.resolve("test3", {splat: 1})).to.equal("/some/1", "Trailing double star"); 220 | expect(resolver.resolve("test4", {splat: 1})).to.equal("/some/1/path", "Middle double star"); 221 | expect(resolver.resolve("test3", {splat: "slash/here"})).to.equal("/some/slash/here", "Slash with double star"); 222 | 223 | expect(resolver.resolve("test5", {splat: "first/splat"})).to.equal("/some/" + encodeURIComponent("first/splat") + "/path", "Multiple globs 1"); 224 | expect(resolver.resolve("test5", {splat: ["first/splat", "second/splat"]})).to.equal("/some/" + encodeURIComponent("first/splat") + "/path/second/splat", "Multiple globs 2"); 225 | }); 226 | }); 227 | 228 | describe('Link', function() { 229 | 230 | afterEach(() => { 231 | NamedURLResolver.reset(); 232 | // document.body.removeChild(document.body.children[0]); 233 | }); 234 | 235 | function render(element, tag) { 236 | var DOMComponent = ReactTestUtils.renderIntoDocument( 237 | element 238 | ); 239 | var RenderedComponent = ReactTestUtils.findRenderedDOMComponentWithTag( 240 | DOMComponent, 241 | tag 242 | ); 243 | return RenderedComponent; 244 | }; 245 | 246 | it('correctly renders elements', function() { 247 | NamedURLResolver.mergeRouteTree(createComplexRouteTree()); 248 | 249 | var TestComponent = React.createClass({ 250 | render: function() { 251 | return ( 252 | React.createElement('div', {}, [ 253 | React.createElement(Link, {to: 'root'}), 254 | React.createElement(Link, {to: 'app.index'}), 255 | React.createElement(Link, {to: 'app.list'}), 256 | React.createElement(Link, {to: 'users'}), 257 | React.createElement(Link, {to: 'users.index'}), 258 | React.createElement(Link, {to: 'users.list'}), 259 | React.createElement(Link, {to: 'users.show'}), 260 | React.createElement(Link, {to: 'users.show', params: {id: 4}}), 261 | React.createElement(Link, {to: 'users.show', params: {id: ':mal/ici/:ous'}}), 262 | 263 | React.createElement(Link, {to: '/some-unnamed-path'}), 264 | React.createElement(Link, {to: '/'}), 265 | React.createElement(Link, {to: '/users'}), 266 | React.createElement(Link, {to: '/users/5'}) 267 | ]) 268 | ) 269 | } 270 | }); 271 | 272 | var root = render( 273 | React.createElement(Router, {history: createBrowserHistory()}, [ 274 | React.createElement(Route, {path: '/', component: TestComponent}) 275 | ]), 276 | 'div' 277 | ); 278 | expect(root).to.be.ok; 279 | 280 | expect(root.children.length).to.equal(13); 281 | var i = 0; 282 | expect(root.children[i++].getAttribute('href')).to.equal('/'); 283 | expect(root.children[i++].getAttribute('href')).to.equal('/app'); 284 | expect(root.children[i++].getAttribute('href')).to.equal('/app'); 285 | expect(root.children[i++].getAttribute('href')).to.equal('/users'); 286 | expect(root.children[i++].getAttribute('href')).to.equal('/users'); 287 | expect(root.children[i++].getAttribute('href')).to.equal('/users/list'); 288 | expect(root.children[i++].getAttribute('href')).to.equal('/users/:id'); 289 | expect(root.children[i++].getAttribute('href')).to.equal('/users/4'); 290 | expect(root.children[i++].getAttribute('href')).to.equal('/users/' + encodeURIComponent(':mal/ici/:ous')); 291 | 292 | expect(root.children[i++].getAttribute('href')).to.equal('/some-unnamed-path'); 293 | expect(root.children[i++].getAttribute('href')).to.equal('/'); 294 | expect(root.children[i++].getAttribute('href')).to.equal('/users'); 295 | expect(root.children[i++].getAttribute('href')).to.equal('/users/5'); 296 | }); 297 | 298 | it('correctly renders elements with custom resolver', function() { 299 | var resolver = new NamedURLResolverClass(); 300 | resolver.mergeRouteTree([ 301 | React.createElement(Route, {path: '/users/:id', name: 'users.show'}), 302 | React.createElement(Route, {path: '/user/:id-parent/:id', name: 'test1'}), 303 | React.createElement(Route, {path: '/user/semi:colon/:colon', name: 'test2'}) 304 | ]); 305 | 306 | var TestComponent = React.createClass({ 307 | render: function() { 308 | return ( 309 | React.createElement('div', {}, [ 310 | React.createElement(Link, {to: 'users.show', resolver: resolver}), 311 | React.createElement(Link, {to: 'test1', resolver: resolver}), 312 | React.createElement(Link, {to: 'test2', resolver: resolver}) 313 | ]) 314 | ) 315 | } 316 | }); 317 | 318 | var root = render( 319 | React.createElement(Router, {history: createBrowserHistory()}, [ 320 | React.createElement(Route, {path: '/', component: TestComponent}) 321 | ]), 322 | 'div' 323 | ); 324 | expect(root).to.be.ok; 325 | 326 | expect(root.children.length).to.equal(3); 327 | var i = 0; 328 | expect(root.children[i++].getAttribute('href')).to.equal('/users/:id'); 329 | expect(root.children[i++].getAttribute('href')).to.equal('/user/:id-parent/:id'); 330 | expect(root.children[i++].getAttribute('href')).to.equal('/user/semi:colon/:colon'); 331 | }); 332 | 333 | it('correctly renders elements with object descriptor', function() { 334 | NamedURLResolver.mergeRouteTree(createComplexRouteTree()); 335 | 336 | var TestComponent = React.createClass({ 337 | render: function() { 338 | return ( 339 | React.createElement('div', {}, [ 340 | React.createElement(Link, {to: {pathname: '/test'}}), 341 | React.createElement(Link, {to: {pathname: '/test', search: '?param=1'}}), 342 | React.createElement(Link, {to: {name: 'users.show'}}), 343 | React.createElement(Link, {to: {name: 'users.show'}, params: {id: 15}}), 344 | React.createElement(Link, {to: {name: 'users.show', search: '?param=1'}, params: {id: 15}}), 345 | React.createElement(Link, {to: function(location) {return Object.assign({}, location, {search: '?param=1'})}, params: {id: 15}}), 346 | React.createElement(Link, {to: function(location) {delete location.pathname; return Object.assign({}, location, {name: 'users.show', search: '?param=1'})}, params: {id: 15}}), 347 | ]) 348 | ) 349 | } 350 | }); 351 | 352 | var root = render( 353 | React.createElement(Router, {history: createBrowserHistory()}, [ 354 | React.createElement(Route, {path: '/', component: TestComponent}) 355 | ]), 356 | 'div' 357 | ); 358 | expect(root).to.be.ok; 359 | 360 | expect(root.children.length).to.equal(7); 361 | var i = 0; 362 | expect(root.children[i++].getAttribute('href')).to.equal('/test'); 363 | expect(root.children[i++].getAttribute('href')).to.equal('/test?param=1'); 364 | expect(root.children[i++].getAttribute('href')).to.equal('/users/:id'); 365 | expect(root.children[i++].getAttribute('href')).to.equal('/users/15'); 366 | expect(root.children[i++].getAttribute('href')).to.equal('/users/15?param=1'); 367 | expect(root.children[i++].getAttribute('href')).to.equal('/?param=1'); 368 | expect(root.children[i++].getAttribute('href')).to.equal('/users/15?param=1'); 369 | }); 370 | 371 | }); 372 | } 373 | 374 | 375 | 376 | describe('formatRoute', function() { 377 | 378 | it('correctly resolve simple routes', function () { 379 | expect(formatRoute("/")).to.equal("/"); 380 | expect(formatRoute("/app")).to.equal("/app"); 381 | expect(formatRoute("/users/:id")).to.equal("/users/:id"); 382 | }); 383 | 384 | it('correctly formats simple routes', function () { 385 | expect(formatRoute("/users/:id")).to.equal("/users/:id"); 386 | expect(formatRoute("/users/:id-parent/:id", {id: 4, 'id-parent': 5})).to.equal("/users/5/4"); 387 | expect(formatRoute("/users/:id", {id: 'id/:id'})).to.equal("/users/" + encodeURIComponent("id/:id")); 388 | expect(formatRoute("/users/:id-parent/:id", { 389 | id: 'id/:id', 390 | 'id-parent': 'idp:id' 391 | })).to.equal("/users/" + encodeURIComponent("idp:id") + "/" + encodeURIComponent("id/:id")); 392 | expect(formatRoute("/users/semi:colon/:colon", {colon: 'colon:colon'})).to.equal("/users/semi:colon/" + encodeURIComponent("colon:colon")); 393 | }); 394 | 395 | it('correctly resolves optional params', function () { 396 | expect(formatRoute("/path(/:param)", {param: 1})).to.equal("/path/1", "Substitute case 1"); 397 | expect(formatRoute("/path/(:param)", {param: 1})).to.equal("/path/1", "Substitute case 2"); 398 | expect(formatRoute("/path/(:param)")).to.equal("/path", "Substitute case 3"); 399 | }); 400 | 401 | it('correctly resolves optional params', function () { 402 | expect(formatRoute("/path(/:param)", {param: 1})).to.equal("/path/1", "Substitute case 1"); 403 | expect(formatRoute("/path/(:param)", {param: 1})).to.equal("/path/1", "Substitute case 2"); 404 | 405 | expect(formatRoute("/path(/:param)")).to.equal("/path", "Simple omit case 1"); 406 | expect(formatRoute("/path/(:param)")).to.equal("/path", "Simple omit case 2"); 407 | 408 | expect(formatRoute("/path/(:param1)/:param2", {param2: 2})).to.equal("/path/2", "Middle omit case 1"); 409 | expect(formatRoute("/path/(:param1/):param2", {param2: 2})).to.equal("/path/2", "Middle omit case 2"); 410 | expect(formatRoute("/path(/:param1/):param2", {param2: 2})).to.equal("/path2", "Middle omit case 3"); 411 | expect(formatRoute("/path(/:param1)/:param2", {param2: 2})).to.equal("/path/2", "Middle omit case 4"); 412 | 413 | expect(formatRoute("/path/:param1/(:param2)", {param1: 1})).to.equal("/path/1", "Trailing omit case 1"); 414 | expect(formatRoute("/path/:param1(/:param2)", {param1: 1})).to.equal("/path/1", "Trailing omit case 2"); 415 | 416 | expect(formatRoute("/path/:param1(/:param2)", { 417 | param1: "p(1)", 418 | param2: "p(2)" 419 | })).to.equal("/path/p(1)/p(2)", "Params with parentheses"); 420 | 421 | // if any part of optional sequence is omitted, entire sequence is omitted 422 | expect(formatRoute("/path(/:param1/:param2/)", {param1: 1})).to.equal("/path", "Omitted sequence"); 423 | expect(formatRoute("/path(/:param1/:param2/)", {param1: 1, param2: 2})).to.equal("/path/1/2", "Resolved sequence"); 424 | }); 425 | 426 | it('correctly resolves React Router 4 optional params', function() { 427 | expect(formatRoute("/path/:param?", {param: 1})).to.equal("/path/1", "Substitute case"); 428 | expect(formatRoute("/path/:param?")).to.equal("/path", "Simple omit case"); 429 | expect(formatRoute("/path/:param1?/:param2?", {param2: 2})).to.equal("/path/2", "Middle omit case 1"); 430 | expect(formatRoute("/path/:param1/:param2?", {param1: 1})).to.equal("/path/1", "Trailing omit case 1"); 431 | }); 432 | 433 | it('correctly resolves splat params', function () { 434 | expect(formatRoute("/some/*", {splat: 1})).to.equal("/some/1", "Trailing single star"); 435 | expect(formatRoute("/some/*/path", {splat: 1})).to.equal("/some/1/path", "Middle single star"); 436 | expect(formatRoute("/some/*", {splat: "slash/here"})).to.equal("/some/" + encodeURIComponent("slash/here"), "Slash with single star"); 437 | 438 | expect(formatRoute("/some/**", {splat: 1})).to.equal("/some/1", "Trailing double star"); 439 | expect(formatRoute("/some/**/path", {splat: 1})).to.equal("/some/1/path", "Middle double star"); 440 | expect(formatRoute("/some/**", {splat: "slash/here"})).to.equal("/some/slash/here", "Slash with double star"); 441 | 442 | expect(formatRoute("/some/*/path/**", {splat: "first/splat"})).to.equal("/some/" + encodeURIComponent("first/splat") + "/path", "Multiple globs 1"); 443 | expect(formatRoute("/some/*/path/**", {splat: ["first/splat", "second/splat"]})).to.equal("/some/" + encodeURIComponent("first/splat") + "/path/second/splat", "Multiple globs 2"); 444 | }); 445 | }); 446 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | 2 | var jsdom = require('jsdom'); 3 | global.document = jsdom.jsdom(''); 4 | global.window = document.defaultView; 5 | global.window.sessionStorage = require('sessionstorage'); 6 | global.navigator = {userAgent: 'node.js'}; 7 | --------------------------------------------------------------------------------