├── .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 | [](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 |
--------------------------------------------------------------------------------