From 4521530227b3b99e0b5ca1c9736fb2c37d346224 Mon Sep 17 00:00:00 2001 From: petrovalex Date: Sun, 20 May 2012 22:35:39 +0300 Subject: [PATCH] - regexp support for $route params --- src/ng/route.js | 98 +++++++++++++++++++++++++++-------- test/ng/routeSpec.js | 120 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 20 deletions(-) diff --git a/src/ng/route.js b/src/ng/route.js index fd54b1c51ad1..85e1f72b20ef 100644 --- a/src/ng/route.js +++ b/src/ng/route.js @@ -56,8 +56,12 @@ 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) { @@ -65,7 +69,14 @@ function $RouteProvider(){ ? 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; @@ -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) { @@ -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; @@ -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; + } } diff --git a/test/ng/routeSpec.js b/test/ng/routeSpec.js index b66cbb8edeec..c7431e75e788 100644 --- a/test/ng/routeSpec.js +++ b/test/ng/routeSpec.js @@ -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'});