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

feat($route): regexp support for $route params #972

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 78 additions & 20 deletions src/ng/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,27 @@ function $RouteProvider(){
* @description
* Adds a new route definition to the `$route` service.
*/
this.when = function(path, route) {
routes[path] = extend({reloadOnSearch: true}, route);
this.when = function(path, route, constraints) {
var regex;
routes[path] = extend({reloadOnSearch: true}, route, {
routeParams:extractParams(path, constraints)
});
routes[path].regex = regex = getRouteMatchingRegex(path, routes[path].routeParams);

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

routes[redirectPath] = {redirectTo: path};
routes[redirectPath] = {
redirectTo: path,
routeParams: routes[path].routeParams
};

routes[redirectPath].regex = (regex[regex.length - 2] == '/')
? regex.substr(0, regex.length - 2) + '$'
: regex.substr(0, regex.length - 1) + '/$';
}

return this;
Expand All @@ -89,6 +100,25 @@ function $RouteProvider(){
};


var shortcuts = this.shortcuts = {
"int": {
regex: "\\d+?",
parse: function(value) { return parseInt(value, 10); }
},
"float": {
regex: "\\d+\\.\\d+?",
parse: function(value) { return parseFloat(value); }
},
"bool": {
regex: "true|false",
parse: function(value) { return angular.lowercase(value) === "true"; }
},
"string": {
regex: "\\w+?"
}
};


this.$get = ['$rootScope', '$location', '$routeParams',
function( $rootScope, $location, $routeParams) {

Expand Down Expand Up @@ -264,24 +294,15 @@ function $RouteProvider(){
/////////////////////////////////////////////////////

function switchRouteMatcher(on, when) {
// TODO(i): this code is convoluted and inefficient, we should construct the route matching
// regex only once and then reuse it
var regex = '^' + when.replace(/([\.\\\(\)\^\$])/g, "\\$1") + '$',
params = [],
dst = {};
forEach(when.split(/\W/), function(param) {
if (param) {
var paramRegExp = new RegExp(":" + param + "([\\W])");
if (regex.match(paramRegExp)) {
regex = regex.replace(paramRegExp, "([^\\/]*)$1");
params.push(param);
}
}
});
var match = on.match(new RegExp(regex));
var dst = {};
var match = routes[when].regex && on.match(new RegExp(routes[when].regex));
if (match) {
forEach(params, function(name, index) {
dst[name] = match[index + 1];
forEach(routes[when].routeParams, function(param, index) {
if (shortcuts[param[1]] && isFunction(shortcuts[param[1]].parse)) {
dst[param[0]] = shortcuts[param[1]].parse(match[index + 1]);
} else {
dst[param[0]] = match[index + 1];
}
});
}
return match ? dst : null;
Expand Down Expand Up @@ -355,4 +376,41 @@ function $RouteProvider(){
return result.join('');
}
}];

function extractParams(when, constraints) {
constraints = constraints || {};

if (when == null)
return [];

var params = [];
forEach(when.split(/\W/), function(param) {
if (param && when.match(new RegExp(":" + param + "(\\W|$)"))) {
if (constraints[param] && isString(constraints[param])) {
params.push([param, constraints[param]]);
} else {
params.push([param]);
}
}
});
return params;
}

function getRouteMatchingRegex(when, params) {
if (when == null)
return;

var regex = '^' + when.replace(/([\.\\\(\)\^\$])/g, "\\$1") + '$',
paramRegExp;

forEach(params, function(param) {
if (param[1]) {
paramRegExp = "(" + (shortcuts[param[1]] && shortcuts[param[1]].regex || param[1]) + ")$1";
} else {
paramRegExp = "([^\\/]*)$1";
}
regex = regex.replace(new RegExp(":" + param[0] + "([\\W])"), paramRegExp);
});
return regex;
}
}
120 changes: 120 additions & 0 deletions test/ng/routeSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,126 @@ describe('$route', function() {
});


it("should support regexp constraints", function() {
module(function ($routeProvider) {
$routeProvider.when('/user/:id/:age', {template:'user.html'}, {id:"[0-9]*", age:"[0-9]*"});
});
inject(function ($route, $routeParams, $location, $rootScope) {
$location.path('/user/1/32');
$rootScope.$digest();
expect($route.current).toBeDefined();
expect($routeParams.id).toBe('1');
expect($routeParams.age).toBe('32');

$location.path('/user/id/age');
$rootScope.$digest();
expect($route.current).toBeUndefined();
});
});


it("should support list of values as regexp", function() {
module(function($routeProvider) {
$routeProvider.when('/some/:param', {template:'foo.html'}, {param:"one|two|tree"});
});
inject(function ($route, $routeParams, $location, $rootScope) {
$location.path('/some/two');
$rootScope.$digest();
expect($route.current).toBeDefined();
expect($routeParams.param).toBe('two');

$location.path('/some/four');
$rootScope.$digest();
expect($route.current).toBeUndefined();
});
});

it("should support int constraint shortcut", function() {
module(function ($routeProvider) {
$routeProvider.when('/user/:id/:age', {template:'user.html'}, {id:"int", age:"int"});
});
inject(function ($route, $routeParams, $location, $rootScope) {
$location.path('/user/1/32');
$rootScope.$digest();
expect($route.current).toBeDefined();
expect($routeParams.id).toBe(1);
expect($routeParams.age).toBe(32);

$location.path('/user/id/age');
$rootScope.$digest();
expect($route.current).toBeUndefined();
});
});


it("should support float constraint shortcut", function() {
module(function($routeProvider) {
$routeProvider.when('/some/:param', {template:'foo.html'}, {param:"float"});
});
inject(function($route, $routeParams, $location, $rootScope) {
$location.path('/some/12.3456');
$rootScope.$digest();
expect($route.current).toBeDefined();
expect($routeParams.param).toBe(12.3456);

$location.path('/some/param');
$rootScope.$digest();
expect($route.current).toBeUndefined();
});
});

it("should support bool constraint shortcut", function() {
module(function($routeProvider) {
$routeProvider.when('/some/:param1/:param2', {template:'foo.html'}, {param1:"bool",param2:"bool"});
});
inject(function($route, $routeParams, $location, $rootScope) {
$location.path('/some/true/false');
$rootScope.$digest();
expect($route.current).toBeDefined();
expect($routeParams.param1).toBe(true);
expect($routeParams.param2).toBe(false);

$location.path('/some/footrue');
$rootScope.$digest();
expect($route.current).toBeUndefined();
});
});

it("should support string constraint shortcut", function() {
module(function($routeProvider) {
$routeProvider.when('/some/:param', {template:'foo.html'}, {param:"string"});
});
inject(function($route, $routeParams, $location, $rootScope) {
$location.path('/some/string');
$rootScope.$digest();
expect($route.current).toBeDefined();
expect($routeParams.param).toBe('string');
});
});


it("constraint shortcuts should be extensible", function() {
module(function($routeProvider) {
angular.extend($routeProvider.shortcuts, {
"cardnumber": {
regex: "[0-9]{16}"
}
});
$routeProvider.when('/card/:id', {template:'foo.html'}, {id:"cardnumber"});
});
inject(function($route, $routeParams, $location, $rootScope) {
$location.path('/card/1234567898765432');
$rootScope.$digest();
expect($route.current).toBeDefined();
expect($routeParams.id).toBe('1234567898765432');

$location.path('/card/123456');
$rootScope.$digest();
expect($route.current).toBeUndefined();
});
});


it('should match a route that contains special chars in the path', function() {
module(function($routeProvider) {
$routeProvider.when('/$test.23/foo(bar)/:baz', {template: 'test.html'});
Expand Down