Skip to content

Commit f1a39ed

Browse files
committed
Allow overriding url-pattern's compiler. Fixes #140
1 parent cff4951 commit f1a39ed

File tree

6 files changed

+109
-9
lines changed

6 files changed

+109
-9
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ in a declarative manner, directly as a part of your component hierarchy.
2323
* [Capturing Clicks on Anchor Elements][a-elements]
2424
* [A custom Link Component][rec-custom-link]
2525
* [A custom Router Component][rec-custom-router]
26+
* [Overridding URL-Pattern's Compiler][override-url-pattern]
2627

2728
## Project Overview
2829

@@ -82,6 +83,7 @@ See [docs][] for the usage.
8283
[a-elements]: http://strml.viewdocs.io/react-router-component/a-elements
8384
[rec-custom-link]: http://strml.viewdocs.io/react-router-component/recipes/custom-link
8485
[rec-custom-router]: http://strml.viewdocs.io/react-router-component/recipes/custom-router
86+
[override-url-pattern]: http://strml.viewdocs.io/react-router-component/override-url-pattern
8587
[async]: http://strml.viewdocs.io/react-router-component/async
8688
[implementation]: http://strml.viewdocs.io/react-router-component/implementation
8789

docs/override-url-pattern.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Overriding the `url-pattern` compiler
2+
3+
You may have more advanced needs from your url patterns.
4+
5+
The author of `url-pattern`, which does the underlying conversion in RRC from route declaration to RegExp,
6+
[has written documentation on overriding the compiler](https://github.com/snd/url-pattern#modifying-the-compiler).
7+
8+
For example:
9+
10+
var Router = require('react-router-component');
11+
12+
// Create a compiler that only matches alphabetical characters in urls.
13+
//
14+
// This function is passed the props object of the route it is generating a RegExp for.
15+
// While not recommended, you could use this to generate a different compiler per route.
16+
Router.createURLPatternCompiler = function(routeProps) {
17+
var compiler = new URLPattern.Compiler();
18+
compiler.segmentValueCharset = 'a-zA-Z';
19+
return compiler;
20+
}
21+
22+
Overriding the compiler allows you to write very powerful route definitions.
23+
24+
Note that this override is global. At this time, there is no way to define a compiler per Router. If you have
25+
more advanced needs, we recommend using `url-pattern` directly to generate regular expressions to use as route paths,
26+
or using regular expressions directly.

index.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ var environment = require('./lib/environment');
1414

1515
var CaptureClicks = require('./lib/CaptureClicks');
1616

17+
var URLPattern = require('url-pattern');
18+
1719
module.exports = {
1820
Locations: Router.Locations,
1921
Pages: Router.Pages,
@@ -31,5 +33,10 @@ module.exports = {
3133
AsyncRouteRenderingMixin: AsyncRouteRenderingMixin,
3234

3335
NavigatableMixin: NavigatableMixin,
34-
CaptureClicks: CaptureClicks
36+
CaptureClicks: CaptureClicks,
37+
38+
// The fn used to create a compiler for "/user/:id"-style routes is exposed here so it can be overridden.
39+
createURLPatternCompiler: function() {
40+
return new URLPattern.Compiler();
41+
}
3542
};

lib/matchRoutes.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,11 @@ function matchRoutes(routes, path) {
2929
if (process.env.NODE_ENV !== "production") {
3030
invariant(
3131
current.props.handler !== undefined && current.props.path !== undefined,
32-
"Router should contain either Route or NotFound components " +
33-
"as routes")
32+
"Router should contain either Route or NotFound components as routes");
3433
}
3534

3635
if (current.props.path) {
37-
current.props.pattern = current.props.pattern || new URLPattern(current.props.path, createCompiler());
36+
current.props.pattern = current.props.pattern || new URLPattern(current.props.path, createCompiler(current.props));
3837
if (!page) {
3938
match = current.props.pattern.match(path);
4039
if (match) {
@@ -114,6 +113,6 @@ Match.prototype.getHandler = function() {
114113
// Passed a component descriptor - create an element. Assign it the ref
115114
// from the <Location> tag.
116115
return React.createElement(handler, props);
117-
}
116+
};
118117

119118
module.exports = matchRoutes;

tests/browser-jsx.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ describe('JSX + Routing with async components', function() {
5959
var Locations = Router.Locations;
6060
var Location = Router.Location;
6161
return (
62-
React.createElement(Locations, {ref: "router", className: "App"},
63-
React.createElement(Location, {path: "/__zuul", handler: Main, ref: "main"}),
64-
React.createElement(Location, {path: "/__zuul/page1", handler: Page1, ref: "page1"}),
62+
React.createElement(Locations, {ref: "router", className: "App"},
63+
React.createElement(Location, {path: "/__zuul", handler: Main, ref: "main"}),
64+
React.createElement(Location, {path: "/__zuul/page1", handler: Page1, ref: "page1"}),
6565
React.createElement(Location, {path: "/__zuul/:text", handler: Page2, ref: "page2"})
6666
)
6767
);

tests/matchRoutes.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var React = require('react');
55
var Router = require('../');
66
var Location = React.createFactory(Router.Location);
77
var NotFound = React.createFactory(Router.NotFound);
8+
var URLPattern = require('url-pattern');
89

910
describe('matchRoutes', function() {
1011

@@ -14,10 +15,19 @@ describe('matchRoutes', function() {
1415
Location({path: '/mod/*', handler: React.createElement('div', {name: 'mod'})}),
1516
Location({path: /\/regex\/([a-zA-Z]*)$/, handler: React.createElement('div', {name: 'regex'})}),
1617
Location({path: /\/(.*?)\/(\d)\/([a-zA-Z]*)$/, handler: React.createElement('div', {name: 'regexMatch'}),
17-
matchKeys: ['name', 'num', 'text']}),
18+
matchKeys: ['name', 'num', 'text']}),
1819
NotFound({handler: React.createElement('div', {name: 'notfound'})})
1920
];
2021

22+
afterEach(function() {
23+
// Remove any compiled regex patterns from all routes
24+
routes.forEach(function(r) {
25+
r.props.pattern = null;
26+
});
27+
// In case we overrode this, reset it.
28+
Router.createURLPatternCompiler = function () { return new URLPattern.Compiler(); };
29+
});
30+
2131
it('matches ""', function() {
2232
var match = matchRoutes(routes, '');
2333
assert(match.route);
@@ -48,6 +58,62 @@ describe('matchRoutes', function() {
4858
assert.strictEqual(match.unmatchedPath, null);
4959
});
5060

61+
it('fails to match /cat/:id with periods in param', function() {
62+
var match = matchRoutes(routes, '/cat/hello.with.periods');
63+
assert(match.route);
64+
assert.strictEqual(match.route.props.handler.props.name, 'notfound');
65+
assert.deepEqual(match.match, null);
66+
assert.strictEqual(match.path, '/cat/hello.with.periods');
67+
assert.strictEqual(match.matchedPath, '/cat/hello.with.periods');
68+
assert.strictEqual(match.unmatchedPath, null);
69+
});
70+
71+
it('matches /cat/:id with a custom url-pattern compiler and periods in param', function() {
72+
Router.createURLPatternCompiler = function() {
73+
var compiler = new URLPattern.Compiler();
74+
compiler.segmentValueCharset = 'a-zA-Z0-9_\\- %\\.';
75+
return compiler;
76+
};
77+
var match = matchRoutes(routes, '/cat/hello.with.periods');
78+
assert(match.route);
79+
assert.strictEqual(match.route.props.handler.props.name, 'cat');
80+
assert.deepEqual(match.match, {id: 'hello.with.periods'});
81+
assert.strictEqual(match.path, '/cat/hello.with.periods');
82+
assert.strictEqual(match.matchedPath, '/cat/hello.with.periods');
83+
assert.strictEqual(match.unmatchedPath, null);
84+
});
85+
86+
it('matches a very custom url-pattern compiler config', function() {
87+
var route = Location({path: '[http[s]!://][$sub_domain.]$domain.$toplevel-domain[/?]',
88+
handler: React.createElement('div', {name: 'parseDomain'})});
89+
90+
// Lifted from url-pattern docs
91+
Router.createURLPatternCompiler = function(routeProps) {
92+
// Somebody might use this, make sure it works.
93+
assert.strictEqual(routeProps.path, route.props.path);
94+
95+
// Create a very custom compiler.
96+
var compiler = new URLPattern.Compiler();
97+
compiler.escapeChar = '!';
98+
compiler.segmentNameStartChar = '$';
99+
compiler.segmentNameCharset = 'a-zA-Z0-9_-';
100+
compiler.segmentValueCharset = 'a-zA-Z0-9';
101+
compiler.optionalSegmentStartChar = '[';
102+
compiler.optionalSegmentEndChar = ']';
103+
compiler.wildcardChar = '?';
104+
return compiler;
105+
};
106+
107+
var match = matchRoutes([route], 'https://www.github.com/strml/react-router-component');
108+
assert(match.route);
109+
assert.strictEqual(match.route.props.handler.props.name, 'parseDomain');
110+
assert.deepEqual(match.match, {sub_domain: 'www', domain: 'github', 'toplevel-domain': 'com',
111+
_: ['strml/react-router-component']});
112+
assert.strictEqual(match.path, 'https://www.github.com/strml/react-router-component');
113+
assert.strictEqual(match.matchedPath, 'https://www.github.com/');
114+
assert.strictEqual(match.unmatchedPath, 'strml/react-router-component');
115+
});
116+
51117
it('matches /mod/wow/here', function() {
52118
var match = matchRoutes(routes, '/mod/wow/here');
53119
assert(match.route);

0 commit comments

Comments
 (0)