Skip to content

Commit

Permalink
React 0.14; url-pattern 0.10; breaking url-pattern changes and docs.
Browse files Browse the repository at this point in the history
Adds `urlPatternOptions` prop and removes url-pattern methods on default export.
  • Loading branch information
STRML committed Oct 7, 2015
1 parent 4232da5 commit fb12e4e
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 113 deletions.
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Regular expressions are an easy way to accomplish more advanced routing.

When using a regular expression, parameters extracted from the regex will be
passed as an array with the name `_`. This may be inconvenient if your components
are reused. If you specify an array of keys and pass it as the `matchKeys` prop,
are reused. If you specify an array of keys and pass it as the `urlPatternOptions` prop,
matches from the regex will be translated.

For example, in the App above, the path `/friends/39/wall` would pass the props
Expand Down
43 changes: 15 additions & 28 deletions docs/override-url-pattern.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,23 @@
You may have more advanced needs from your url patterns.

The author of `url-pattern`, which does the underlying conversion in RRC from route declaration to RegExp,
[has written documentation on overriding the compiler](https://github.com/snd/url-pattern#modifying-the-compiler).
[has written documentation on overriding the compiler](https://github.com/snd/url-pattern#customize-the-pattern-syntax).

For example:

var Router = require('react-router-component');
var URLPattern = require('url-pattern');
Compile an object matching these fields and pass it on your `<Location>` or `<Locations>` to override `url-pattern`
defaults.

// Create a compiler that only matches alphabetical characters in urls.
//
// This function is passed the props object of the route it is generating a RegExp for.
// While not recommended, you could use this to generate a different compiler per route.
Router.createURLPatternCompiler = function(routeProps) {
var compiler = new URLPattern.Compiler();
compiler.segmentValueCharset = 'a-zA-Z';
return compiler;
}

Overriding the compiler allows you to write very powerful route definitions.
It's possible to set more specific rules on individual `<Location>` components. These rules will be merged
with parent rules.

Note that this override is global. At this time, there is no way to define a compiler per Router. If you have
more advanced needs, we recommend using `url-pattern` directly to generate regular expressions to use as route paths,
or using regular expressions directly.

A note on ES6; ES6 modules can't modify `module.exports` directly. Instead, use `setCreateURLPatternCompilerFactory`:
For example:

import * as Router from 'react-router-component';
import * as URLPattern from 'url-pattern';
...
<Locations urlPatternOptions={{escapeChar: '|'}}>
<Location path="/" handler={MainPage} />
<Location path="/users/:username" handler={UserPage} urlPatternOptions={{segmentValueCharset: 'a-zA-Z0-9'}} />
<Location path="/search/*" handler={SearchPage} />
<Location path={/\/product\/([0-9]*)/} handler={ProductPage} />
</Locations>
```

Router.setCreateURLPatternCompilerFactory((routeProps) => {
var compiler = new URLPattern.Compiler();
compiler.segmentValueCharset = 'a-zA-Z';
return compiler;
})
Overriding the compiler allows you to write very powerful route definitions.
12 changes: 1 addition & 11 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,7 @@ var exportsObject = {
RouteRenderingMixin: RouteRenderingMixin,

NavigatableMixin: NavigatableMixin,
CaptureClicks: CaptureClicks,

// The fn used to create a compiler for "/user/:id"-style routes is exposed here so it can be overridden.
createURLPatternCompiler: function() {
return new URLPattern.Compiler();
},

// For ES6 imports, which can't modify module.exports directly
setCreateURLPatternCompilerFactory: function(fn) {
exportsObject.createURLPatternCompiler = fn;
}
CaptureClicks: CaptureClicks
};

module.exports = exportsObject;
6 changes: 5 additions & 1 deletion lib/Route.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ function createClass(name) {
: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.instanceOf(RegExp)
]).isRequired
]).isRequired,
urlPatternOptions: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.string),
React.PropTypes.object
])
},
getDefaultProps: function() {
if (name === 'NotFound') {
Expand Down
8 changes: 6 additions & 2 deletions lib/RouterMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ var RouterMixin = {
path: React.PropTypes.string,
contextual: React.PropTypes.bool,
onBeforeNavigation: React.PropTypes.func,
onNavigation: React.PropTypes.func
onNavigation: React.PropTypes.func,
urlPatternOptions: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.string),
React.PropTypes.object
])
},

childContextTypes: {
Expand Down Expand Up @@ -151,7 +155,7 @@ var RouterMixin = {
* @param {Callback} cb
*/
setPath: function(path, navigation, cb) {
var match = matchRoutes(this.getRoutes(this.props), path);
var match = matchRoutes(this.getRoutes(this.props), path, this.props);

var state = {
match: match,
Expand Down
74 changes: 41 additions & 33 deletions lib/matchRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

var URLPattern = require('url-pattern');
var invariant = require('./util/invariant');
var warning = require('./util/warning');
var React = require('react');
var assign = Object.assign || require('object.assign');
var qs = require('qs');
Expand All @@ -13,12 +14,15 @@ var patternCache = {};
*
* @param {Array.<Route>} routes
* @param {String} path
* @param {[Object]} properties of parent router
*/
function matchRoutes(routes, path) {
var createCompiler = require('../index').createURLPatternCompiler;

function matchRoutes(routes, path, routerProps) {
var match, page, notFound, queryObj;

if (!routerProps) { // optional
routerProps = {};
}

if (!Array.isArray(routes)) {
routes = [routes];
}
Expand All @@ -35,28 +39,49 @@ function matchRoutes(routes, path) {
// Simply skip null or undefined to allow ternaries in route definitions
if (!current) continue;

if (process.env.NODE_ENV !== "production") {
invariant(
current.props.handler !== undefined && current.props.path !== undefined,
"Router should contain either Route or NotFound components as routes");
}
invariant(
current.props.handler !== undefined && current.props.path !== undefined,
"Router should contain either Route or NotFound components as routes");

if (current.props.path) {
// Technically, this cache will incorrectly be used if a user defines two routes
// with identical paths but different compilers. FIXME?
var pattern = patternCache[current.props.path] ||
new URLPattern(current.props.path, createCompiler(current.props));
// Allow passing compiler options to url-pattern, see
// https://github.com/snd/url-pattern#customize-the-pattern-syntax
// Note that this blows up if you provide an empty object on a regex path
var urlPatternOptions;
if (Array.isArray(current.props.urlPatternOptions) || current.props.path instanceof RegExp) {
// If an array is passed, it takes precedence - assumed these are regexp keys
urlPatternOptions = current.props.urlPatternOptions;
} else if (routerProps.urlPatternOptions || current.props.urlPatternOptions) {
urlPatternOptions = assign({}, routerProps.urlPatternOptions, current.props.urlPatternOptions);
}

// matchKeys is deprecated
// FIXME remove this block in next minor version
if(current.props.matchKeys) {
urlPatternOptions = current.props.matchKeys;
warning(false,
'`matchKeys` is deprecated; please use the prop `urlPatternOptions` instead. See the CHANGELOG for details.');
}

var cacheKey = current.props.path + (urlPatternOptions ? JSON.stringify(urlPatternOptions) : '');

var pattern = patternCache[cacheKey];
if (!pattern) {
pattern = patternCache[cacheKey] = new URLPattern(current.props.path, urlPatternOptions);
}

if (!page) {
match = pattern.match(pathToMatch);
if (match) {
page = current;
}
// Parse RegExp matches, which are returned as an array rather an an object.
if (Array.isArray(match)) {
match = parseMatch(current, match);

// Backcompat fix in 0.27: regexes in url-pattern no longer return {_: matches}
if (match && current.props.path instanceof RegExp && !match._ && Array.isArray(match)) {
match = {_: match};
}

// Backcompat fix; url-pattern removed the array wrapper
// Backcompat fix; url-pattern removed the array wrapper on wildcards
if (match && match._ && !Array.isArray(match._)) {
match._ = [match._];
}
Expand All @@ -76,23 +101,6 @@ function matchRoutes(routes, path) {
);
}

/**
* Given the currently matched Location & the match array, transform the matches to named key/value pairs,
* if possible.
* @param {Route} current Matched Route.
* @param {Array} match Array of matches from RegExp.
* @return {Object} Key/value pairs to feed to the route's handler.
*/
function parseMatch(current, match) {
if (Array.isArray(current.props.matchKeys)) {
return current.props.matchKeys.reduce(function(memo, key, i) {
memo[key] = match[i];
return memo;
}, {});
}
return {_: match};
}

/**
* Match object
*
Expand Down
3 changes: 2 additions & 1 deletion lib/util/invariant.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
* will remain to ensure logic does not differ in production.
*/

var __ENV__ = process.env.NODE_ENV; // env lookup is slow in Node
var invariant = function (condition, format, a, b, c, d, e, f) {
if (process.env.NODE_ENV !== 'production') {
if (__ENV__ !== 'production') {
if (format === undefined) {
throw new Error('invariant requires an error message argument');
}
Expand Down
47 changes: 47 additions & 0 deletions lib/util/warning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright 2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule warning
*/

"use strict";

/**
* Similar to invariant but only logs a warning if the condition is not met.
* This can be used to log issues in development environments in critical
* paths. Removing the logging code for production environments will keep the
* same logic and follow the same code paths.
*/

var warning = function(){};

if ("production" !== process.env.NODE_ENV) {
warning = function(condition, format ) {var args=Array.prototype.slice.call(arguments,2);
if (format === undefined) {
throw new Error(
'`warning(condition, format, ...args)` requires a warning ' +
'message argument'
);
}

if (!condition) {
var argIndex = 0;
/*eslint no-console:0*/
console.warn('Warning: ' + format.replace(/%s/g, function() {return args[argIndex++];}));
}
};
}

module.exports = warning;
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@
"description": "Declarative router component for React",
"main": "index.js",
"dependencies": {
"object.assign": "^3.0.0",
"qs": "^4.0.0",
"url-pattern": "~0.9.0",
"object.assign": "^4.0.1",
"qs": "^5.2.0",
"url-pattern": "~0.10.2",
"urllite": "~0.5.0"
},
"devDependencies": {
"babel": "^5.8.23",
"browserify": "^10.2.3",
"browserify-shim": "^3.8.7",
"browserify": "^11.2.0",
"browserify-shim": "^3.8.10",
"envify": "^3.4.0",
"eslint": "^1.3.1",
"eslint-plugin-react": "^3.3.2",
"jsxhint": "~0.15.1",
"mocha": "~2.2.5",
"zuul": "^3.4.0"
"eslint": "^1.6.0",
"eslint-plugin-react": "^3.5.1",
"jsxhint": "^0.15.1",
"mocha": "^2.3.3",
"zuul": "^3.6.0"
},
"peerDependencies": {
"react": ">=0.14.0-rc1 <0.15.0",
"react-dom": ">=0.14.0-rc1 <0.15.0"
"react": ">=0.14.0 <0.15.0",
"react-dom": ">=0.14.0 <0.15.0"
},
"browserify": {
"transform": [
Expand Down
6 changes: 5 additions & 1 deletion tests/browser-jsx.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
'use strict';
var assert = require('assert');
var React = require('react');
var ReactDOM = require('react-dom');
var ReactDOM = React; // For 0.13
if (parseInt(React.version.split('.')[1], 10) >= 14) {
// For 0.14+
ReactDOM = require('react-dom');
}
var Router = require('../index');

var historyAPI = window.history !== undefined && window.history.pushState !== undefined;
Expand Down
39 changes: 16 additions & 23 deletions tests/matchRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,10 @@ describe('matchRoutes', function() {
assert.strictEqual(match.unmatchedPath, null);
});

it('matches /cat/:id with a custom url-pattern compiler and periods in param', function() {
Router.createURLPatternCompiler = function() {
var compiler = new URLPattern.Compiler();
compiler.segmentValueCharset = 'a-zA-Z0-9_\\- %\\.';
return compiler;
};
var match = matchRoutes(routes, '/cat/hello.with.periods');
it('matches /cat/:id with a custom url-pattern options and periods in param', function() {
var match = matchRoutes(routes, '/cat/hello.with.periods', {urlPatternOptions: {
segmentValueCharset: 'a-zA-Z0-9_\\- %\\.'
}});
assert(match.route);
assert.strictEqual(match.route.props.handler.props.name, 'cat');
assert.deepEqual(match.match, {id: 'hello.with.periods'});
Expand All @@ -84,23 +81,19 @@ describe('matchRoutes', function() {
handler: React.createElement('div', {name: 'parseDomain'})});

// Lifted from url-pattern docs
Router.setCreateURLPatternCompilerFactory(function(routeProps) {
// Somebody might use this, make sure it works.
assert.strictEqual(routeProps.path, route.props.path);

// Create a very custom compiler.
var compiler = new URLPattern.Compiler();
compiler.escapeChar = '!';
compiler.segmentNameStartChar = '$';
compiler.segmentNameCharset = 'a-zA-Z0-9_-';
compiler.segmentValueCharset = 'a-zA-Z0-9';
compiler.optionalSegmentStartChar = '[';
compiler.optionalSegmentEndChar = ']';
compiler.wildcardChar = '?';
return compiler;
});
var urlPatternOptions = {
escapeChar: '!',
segmentNameStartChar: '$',
segmentNameCharset: 'a-zA-Z0-9_-',
segmentValueCharset: 'a-zA-Z0-9',
optionalSegmentStartChar: '[',
optionalSegmentEndChar: ']',
wildcardChar: '?'
};

var match = matchRoutes([route], 'https://www.github.com/strml/react-router-component');
var match = matchRoutes([route], 'https://www.github.com/strml/react-router-component', {
urlPatternOptions: urlPatternOptions
});
assert(match.route);
assert.strictEqual(match.route.props.handler.props.name, 'parseDomain');
assert.deepEqual(match.match, {sub_domain: 'www', domain: 'github', 'toplevel-domain': 'com',
Expand Down

0 comments on commit fb12e4e

Please sign in to comment.