Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 04cebcc

Browse files
joshrtayjbdeboer
authored andcommitted
feat($route): express style route matching
Added new route matching capabilities: - optional param Changed route matching syntax: - named wildcard BREAKING CHANGE: the syntax for named wildcard parameters in routes has changed from *wildcard to :wildcard* To migrate the code, follow the example below. Here, *highlight becomes :highlight*: Before: $routeProvider.when('/Book1/:book/Chapter/:chapter/*highlight/edit', {controller: noop, templateUrl: 'Chapter.html'}); After: $routeProvider.when('/Book1/:book/Chapter/:chapter/:highlight*/edit', {controller: noop, templateUrl: 'Chapter.html'});
1 parent c173ca4 commit 04cebcc

File tree

3 files changed

+151
-51
lines changed

3 files changed

+151
-51
lines changed

src/ngRoute/route.js

+82-47
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ function $RouteProvider(){
3535
*
3636
* * `path` can contain named groups starting with a colon (`:name`). All characters up
3737
* to the next slash are matched and stored in `$routeParams` under the given `name`
38-
* after the route is resolved.
39-
* * `path` can contain named groups starting with a star (`*name`). All characters are
40-
* eagerly stored in `$routeParams` under the given `name` after the route is resolved.
38+
* when the route matches.
39+
* * `path` can contain named groups starting with a colon and ending with a star (`:name*`).
40+
* All characters are eagerly stored in `$routeParams` under the given `name`
41+
* when the route matches.
42+
* * `path` can contain optional named groups with a question mark (`:name?`).
4143
*
42-
* For example, routes like `/color/:color/largecode/*largecode/edit` will match
44+
* For example, routes like `/color/:color/largecode/:largecode*\/edit` will match
4345
* `/color/brown/largecode/code/with/slashs/edit` and extract:
4446
*
4547
* * `color: brown`
@@ -117,20 +119,66 @@ function $RouteProvider(){
117119
* Adds a new route definition to the `$route` service.
118120
*/
119121
this.when = function(path, route) {
120-
routes[path] = extend({reloadOnSearch: true, caseInsensitiveMatch: false}, route);
122+
routes[path] = extend(
123+
{reloadOnSearch: true},
124+
route,
125+
path && pathRegExp(path, route)
126+
);
121127

122128
// create redirection for trailing slashes
123129
if (path) {
124130
var redirectPath = (path[path.length-1] == '/')
125131
? path.substr(0, path.length-1)
126132
: path +'/';
127133

128-
routes[redirectPath] = {redirectTo: path};
134+
routes[redirectPath] = extend(
135+
{redirectTo: path},
136+
pathRegExp(redirectPath, route)
137+
);
129138
}
130139

131140
return this;
132141
};
133142

143+
/**
144+
* @param path {string} path
145+
* @param opts {Object} options
146+
* @return {?Object}
147+
*
148+
* @description
149+
* Normalizes the given path, returning a regular expression
150+
* and the original path.
151+
*
152+
* Inspired by pathRexp in visionmedia/express/lib/utils.js.
153+
*/
154+
function pathRegExp(path, opts) {
155+
var insensitive = opts.caseInsensitiveMatch,
156+
ret = {
157+
originalPath: path,
158+
regexp: path
159+
},
160+
keys = ret.keys = [];
161+
162+
path = path
163+
.replace(/([().])/g, '\\$1')
164+
.replace(/(\/)?:(\w+)([\?|\*])?/g, function(_, slash, key, option){
165+
var optional = option === '?' ? option : null;
166+
var star = option === '*' ? option : null;
167+
keys.push({ name: key, optional: !!optional });
168+
slash = slash || '';
169+
return ''
170+
+ (optional ? '' : slash)
171+
+ '(?:'
172+
+ (optional ? slash : '')
173+
+ (star && '(.+)?' || '([^/]+)?') + ')'
174+
+ (optional || '');
175+
})
176+
.replace(/([\/$\*])/g, '\\$1');
177+
178+
ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : '');
179+
return ret;
180+
}
181+
134182
/**
135183
* @ngdoc method
136184
* @name ngRoute.$routeProvider#otherwise
@@ -362,50 +410,37 @@ function $RouteProvider(){
362410

363411
/**
364412
* @param on {string} current url
365-
* @param when {string} route when template to match the url against
366-
* @param whenProperties {Object} properties to define when's matching behavior
413+
* @param route {Object} route regexp to match the url against
367414
* @return {?Object}
415+
*
416+
* @description
417+
* Check if the route matches the current url.
418+
*
419+
* Inspired by match in
420+
* visionmedia/express/lib/router/router.js.
368421
*/
369-
function switchRouteMatcher(on, when, whenProperties) {
370-
// TODO(i): this code is convoluted and inefficient, we should construct the route matching
371-
// regex only once and then reuse it
372-
373-
// Escape regexp special characters.
374-
when = '^' + when.replace(/[-\/\\^$:*+?.()|[\]{}]/g, "\\$&") + '$';
375-
376-
var regex = '',
377-
params = [],
378-
dst = {};
379-
380-
var re = /\\([:*])(\w+)/g,
381-
paramMatch,
382-
lastMatchedIndex = 0;
383-
384-
while ((paramMatch = re.exec(when)) !== null) {
385-
// Find each :param in `when` and replace it with a capturing group.
386-
// Append all other sections of when unchanged.
387-
regex += when.slice(lastMatchedIndex, paramMatch.index);
388-
switch(paramMatch[1]) {
389-
case ':':
390-
regex += '([^\\/]*)';
391-
break;
392-
case '*':
393-
regex += '(.*)';
394-
break;
422+
function switchRouteMatcher(on, route) {
423+
var keys = route.keys,
424+
params = {};
425+
426+
if (!route.regexp) return null;
427+
428+
var m = route.regexp.exec(on);
429+
if (!m) return null;
430+
431+
var N = 0;
432+
for (var i = 1, len = m.length; i < len; ++i) {
433+
var key = keys[i - 1];
434+
435+
var val = 'string' == typeof m[i]
436+
? decodeURIComponent(m[i])
437+
: m[i];
438+
439+
if (key && val) {
440+
params[key.name] = val;
395441
}
396-
params.push(paramMatch[2]);
397-
lastMatchedIndex = re.lastIndex;
398-
}
399-
// Append trailing path part.
400-
regex += when.substr(lastMatchedIndex);
401-
402-
var match = on.match(new RegExp(regex, whenProperties.caseInsensitiveMatch ? 'i' : ''));
403-
if (match) {
404-
forEach(params, function(name, index) {
405-
dst[name] = match[index + 1];
406-
});
407442
}
408-
return match ? dst : null;
443+
return params;
409444
}
410445

411446
function updateRoute() {
@@ -489,7 +524,7 @@ function $RouteProvider(){
489524
// Match a route
490525
var params, match;
491526
forEach(routes, function(route, path) {
492-
if (!match && (params = switchRouteMatcher($location.path(), path, route))) {
527+
if (!match && (params = switchRouteMatcher($location.path(), route))) {
493528
match = inherit(route, {
494529
params: extend({}, $location.search(), params),
495530
pathParams: params});

test/ngRoute/routeParamsSpec.js

+33
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,37 @@ describe('$routeParams', function() {
4545
expect($routeParams).toEqual({barId: 'barvalue', fooId: 'foovalue'});
4646
});
4747
});
48+
49+
it('should correctly extract the params when an optional param name is part of the route', function() {
50+
module(function($routeProvider) {
51+
$routeProvider.when('/bar/:foo?', {});
52+
$routeProvider.when('/baz/:foo?/edit', {});
53+
$routeProvider.when('/qux/:bar?/:baz?', {});
54+
});
55+
56+
inject(function($rootScope, $route, $location, $routeParams) {
57+
$location.path('/bar');
58+
$rootScope.$digest();
59+
expect($routeParams).toEqual({});
60+
61+
$location.path('/bar/fooValue');
62+
$rootScope.$digest();
63+
expect($routeParams).toEqual({foo: 'fooValue'});
64+
65+
$location.path('/baz/fooValue/edit');
66+
$rootScope.$digest();
67+
expect($routeParams).toEqual({foo: 'fooValue'});
68+
69+
$location.path('/baz/edit');
70+
$rootScope.$digest();
71+
expect($routeParams).toEqual({});
72+
73+
$location.path('/qux//bazValue');
74+
$rootScope.$digest();
75+
expect($routeParams).toEqual({baz: 'bazValue', bar: undefined});
76+
77+
});
78+
});
79+
80+
4881
});

test/ngRoute/routeSpec.js

+36-4
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ describe('$route', function() {
6868
nextRoute;
6969

7070
module(function($routeProvider) {
71-
$routeProvider.when('/Book1/:book/Chapter/:chapter/*highlight/edit',
71+
$routeProvider.when('/Book1/:book/Chapter/:chapter/:highlight*/edit',
7272
{controller: noop, templateUrl: 'Chapter.html'});
73-
$routeProvider.when('/Book2/:book/*highlight/Chapter/:chapter',
73+
$routeProvider.when('/Book2/:book/:highlight*/Chapter/:chapter',
7474
{controller: noop, templateUrl: 'Chapter.html'});
7575
$routeProvider.when('/Blank', {});
7676
});
@@ -127,9 +127,9 @@ describe('$route', function() {
127127
nextRoute;
128128

129129
module(function($routeProvider) {
130-
$routeProvider.when('/Book1/:book/Chapter/:chapter/*highlight/edit',
130+
$routeProvider.when('/Book1/:book/Chapter/:chapter/:highlight*/edit',
131131
{controller: noop, templateUrl: 'Chapter.html', caseInsensitiveMatch: true});
132-
$routeProvider.when('/Book2/:book/*highlight/Chapter/:chapter',
132+
$routeProvider.when('/Book2/:book/:highlight*/Chapter/:chapter',
133133
{controller: noop, templateUrl: 'Chapter.html'});
134134
$routeProvider.when('/Blank', {});
135135
});
@@ -245,6 +245,31 @@ describe('$route', function() {
245245
});
246246

247247

248+
describe('should match a route that contains optional params in the path', function() {
249+
beforeEach(module(function($routeProvider) {
250+
$routeProvider.when('/test/:opt?/:baz/edit', {templateUrl: 'test.html'});
251+
}));
252+
253+
it('matches a URL with optional params', inject(function($route, $location, $rootScope) {
254+
$location.path('/test/optValue/bazValue/edit');
255+
$rootScope.$digest();
256+
expect($route.current).toBeDefined();
257+
}));
258+
259+
it('matches a URL without optional param', inject(function($route, $location, $rootScope) {
260+
$location.path('/test//bazValue/edit');
261+
$rootScope.$digest();
262+
expect($route.current).toBeDefined();
263+
}));
264+
265+
it('not match a URL with a required param', inject(function($route, $location, $rootScope) {
266+
$location.path('///edit');
267+
$rootScope.$digest();
268+
expect($route.current).not.toBeDefined();
269+
}));
270+
});
271+
272+
248273
it('should change route even when only search param changes', function() {
249274
module(function($routeProvider) {
250275
$routeProvider.when('/test', {templateUrl: 'test.html'});
@@ -723,6 +748,8 @@ describe('$route', function() {
723748
module(function($routeProvider) {
724749
$routeProvider.when('/foo/:id/foo/:subid/:extraId', {redirectTo: '/bar/:id/:subid/23'});
725750
$routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'});
751+
$routeProvider.when('/baz/:id/:path*', {redirectTo: '/path/:path/:id'});
752+
$routeProvider.when('/path/:path*/:id', {templateUrl: 'foo.html'});
726753
});
727754

728755
inject(function($route, $location, $rootScope) {
@@ -732,6 +759,11 @@ describe('$route', function() {
732759
expect($location.path()).toEqual('/bar/id1/subid3/23');
733760
expect($location.search()).toEqual({extraId: 'gah'});
734761
expect($route.current.templateUrl).toEqual('bar.html');
762+
763+
$location.path('/baz/1/foovalue/barvalue');
764+
$rootScope.$digest();
765+
expect($location.path()).toEqual('/path/foovalue/barvalue/1');
766+
expect($route.current.templateUrl).toEqual('foo.html');
735767
});
736768
});
737769

0 commit comments

Comments
 (0)