Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Rails-style optional route segments

  • Loading branch information...
commit 546ad2f4a4fd0481385fd9095b593ef79e8e2c92 1 parent 8739e7e
@smathy smathy authored
View
10 README.md
@@ -227,6 +227,16 @@ For the FAQ route, `'app#faq'` specifies the `faq` function on `BatBelt.AppContr
`@root 'app#index'` is just a shorthand for `@route '/', 'app#index'`.
+You can also mark a segment of your route as optional, allowing URLs to match with or without those optional segments. Eg. `@route 'calendar(/:type(/:date))', calendar#index` will match all of the following URLs:
+
+```
+/calendar
+/calendar/y
+/calendar/m/2012-11
+```
+
+When the segments are missing, your controller action is handed an `undefined` value for that param.
+
The `@resources` macro takes a resource name which should ideally be the underscored-pluralized name of one of your models. It sets up three routes, as if you'd used the `@route` macro like so:
```coffeescript
View
12 lib/batman.js
@@ -6897,7 +6897,9 @@
queryParam: '(?:\\?.+)?',
namedOrSplat: /[:|\*]([\w\d]+)/g,
namePrefix: '[:|\*]',
- escapeRegExp: /[-[\]{}()+?.,\\^$|#\s]/g
+ escapeRegExp: /[-[\]{}+?.,\\^$|#\s]/g,
+ openOptParam: /\(/g,
+ closeOptParam: /\)/g
};
Route.prototype.optionKeys = ['member', 'collection'];
@@ -6913,7 +6915,7 @@
templatePath = "/" + templatePath;
}
pattern = templatePath.replace(regexps.escapeRegExp, '\\$&');
- regexp = RegExp("^" + (pattern.replace(regexps.namedParam, '([^\/]+)').replace(regexps.splatParam, '(.*?)')) + regexps.queryParam + "$");
+ regexp = RegExp("^" + (pattern.replace(regexps.openOptParam, '(?:').replace(regexps.closeOptParam, ')?').replace(regexps.namedParam, '([^\/]+)').replace(regexps.splatParam, '(.*?)')) + regexps.queryParam + "$");
namedArguments = ((function() {
var _results;
_results = [];
@@ -6955,19 +6957,21 @@
};
Route.prototype.pathFromParams = function(argumentParams) {
- var hash, key, name, newPath, params, path, query, regexp, _i, _j, _len, _len1, _ref, _ref1;
+ var hash, key, name, newPath, params, path, query, regexp, regexps, _i, _j, _len, _len1, _ref, _ref1;
params = Batman.extend({}, argumentParams);
path = this.get('templatePath');
+ regexps = this.constructor.regexps;
_ref = this.get('namedArguments');
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
name = _ref[_i];
- regexp = RegExp("" + this.constructor.regexps.namePrefix + name);
+ regexp = RegExp("" + regexps.namePrefix + name);
newPath = path.replace(regexp, (params[name] != null ? params[name] : ''));
if (newPath !== path) {
delete params[name];
path = newPath;
}
}
+ path = path.replace(regexps.openOptParam, '').replace(regexps.closeOptParam, '').replace(/\/+$/, '');
_ref1 = this.testKeys;
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
key = _ref1[_j];
View
12 lib/dist/batman.node.js
@@ -6897,7 +6897,9 @@
queryParam: '(?:\\?.+)?',
namedOrSplat: /[:|\*]([\w\d]+)/g,
namePrefix: '[:|\*]',
- escapeRegExp: /[-[\]{}()+?.,\\^$|#\s]/g
+ escapeRegExp: /[-[\]{}+?.,\\^$|#\s]/g,
+ openOptParam: /\(/g,
+ closeOptParam: /\)/g
};
Route.prototype.optionKeys = ['member', 'collection'];
@@ -6913,7 +6915,7 @@
templatePath = "/" + templatePath;
}
pattern = templatePath.replace(regexps.escapeRegExp, '\\$&');
- regexp = RegExp("^" + (pattern.replace(regexps.namedParam, '([^\/]+)').replace(regexps.splatParam, '(.*?)')) + regexps.queryParam + "$");
+ regexp = RegExp("^" + (pattern.replace(regexps.openOptParam, '(?:').replace(regexps.closeOptParam, ')?').replace(regexps.namedParam, '([^\/]+)').replace(regexps.splatParam, '(.*?)')) + regexps.queryParam + "$");
namedArguments = ((function() {
var _results;
_results = [];
@@ -6955,19 +6957,21 @@
};
Route.prototype.pathFromParams = function(argumentParams) {
- var hash, key, name, newPath, params, path, query, regexp, _i, _j, _len, _len1, _ref, _ref1;
+ var hash, key, name, newPath, params, path, query, regexp, regexps, _i, _j, _len, _len1, _ref, _ref1;
params = Batman.extend({}, argumentParams);
path = this.get('templatePath');
+ regexps = this.constructor.regexps;
_ref = this.get('namedArguments');
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
name = _ref[_i];
- regexp = RegExp("" + this.constructor.regexps.namePrefix + name);
+ regexp = RegExp("" + regexps.namePrefix + name);
newPath = path.replace(regexp, (params[name] != null ? params[name] : ''));
if (newPath !== path) {
delete params[name];
path = newPath;
}
}
+ path = path.replace(regexps.openOptParam, '').replace(regexps.closeOptParam, '').replace(/\/+$/, '');
_ref1 = this.testKeys;
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
key = _ref1[_j];
View
11 src/routing/route.coffee
@@ -8,7 +8,9 @@ class Batman.Route extends Batman.Object
queryParam: '(?:\\?.+)?'
namedOrSplat: /[:|\*]([\w\d]+)/g
namePrefix: '[:|\*]'
- escapeRegExp: /[-[\]{}()+?.,\\^$|#\s]/g
+ escapeRegExp: /[-[\]{}+?.,\\^$|#\s]/g
+ openOptParam: /\(/g
+ closeOptParam: /\)/g
optionKeys: ['member', 'collection']
testKeys: ['controller', 'action']
@@ -21,6 +23,7 @@ class Batman.Route extends Batman.Object
regexp = ///
^
#{pattern
+ .replace(regexps.openOptParam, '(?:').replace(regexps.closeOptParam, ')?') # allow (/:foo) wrappers to indicate optional param
.replace(regexps.namedParam, '([^\/]+)')
.replace(regexps.splatParam, '(.*?)') }
#{regexps.queryParam}
@@ -51,15 +54,19 @@ class Batman.Route extends Batman.Object
pathFromParams: (argumentParams) ->
params = Batman.extend {}, argumentParams
path = @get('templatePath')
+ regexps = @constructor.regexps
# Replace the names in the template with their values from params
for name in @get('namedArguments')
- regexp = ///#{@constructor.regexps.namePrefix}#{name}///
+ regexp = ///#{regexps.namePrefix}#{name}///
newPath = path.replace regexp, (if params[name]? then params[name] else '')
if newPath != path
delete params[name]
path = newPath
+
+ path = path.replace(regexps.openOptParam, '').replace(regexps.closeOptParam, '').replace(/\/+$/, '')
+
for key in @testKeys
delete params[key]
View
21 tests/batman/routes/route_test.coffee
@@ -163,3 +163,24 @@ test "controller/action routes should call the controller's dispatch function",
@route.dispatch params
equal searchSpy.lastCallArguments[0], "duplicate"
equal searchSpy.lastCallArguments[1].id, "20"
+
+test "routes should build paths with optional segments", 3, ->
+ route = new Batman.Route "/calendar(/:type(/:date))", {}
+
+ equal route.pathFromParams({}), "/calendar"
+ equal route.pathFromParams({type: "m"}), "/calendar/m"
+ equal route.pathFromParams({type: "m", date: "2012-11"}), "/calendar/m/2012-11"
+
+test "routes with optional segments should parse params", ->
+ type = 'm'
+ date = '2012-11'
+ route = new Batman.Route "/calendar(/:type(/:date))", {}
+
+ path = "/calendar/#{type}/#{date}"
+ deepEqual route.paramsFromPath(path), { path, type, date }
+
+ path = "/calendar/#{type}"
+ deepEqual route.paramsFromPath(path), { path, type, date: undefined }
+
+ path = "/calendar"
+ deepEqual route.paramsFromPath(path), { path, type: undefined, date: undefined }
Please sign in to comment.
Something went wrong with that request. Please try again.