Skip to content

Commit 07b3029

Browse files
committed
feat(UrlMatcher): add per-param config support
Implements optional parameters and default parameter values. [BC-BREAK]: the `params` option in state configurations must now be an object keyed by parameter name.
1 parent c27a0ee commit 07b3029

File tree

5 files changed

+149
-68
lines changed

5 files changed

+149
-68
lines changed

src/common.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,9 @@ function inheritParams(currentParams, newParams, $current, $to) {
9696
var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = [];
9797

9898
for (var i in parents) {
99-
if (!parents[i].params || !parents[i].params.length) continue;
100-
parentParams = parents[i].params;
99+
if (!parents[i].params) continue;
100+
parentParams = keys(parents[i].params);
101+
if (!parentParams.length) continue;
101102

102103
for (var j in parentParams) {
103104
if (arraySearch(inheritList, parentParams[j]) >= 0) continue;

src/state.js

+19-23
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,14 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
4848

4949
// Build a URLMatcher if necessary, either via a relative or absolute URL
5050
url: function(state) {
51-
var url = state.url;
51+
var url = state.url, config = { params: state.params || {} };
5252

5353
if (isString(url)) {
54-
if (url.charAt(0) == '^') {
55-
return $urlMatcherFactory.compile(url.substring(1));
56-
}
57-
return (state.parent.navigable || root).url.concat(url);
54+
if (url.charAt(0) == '^') return $urlMatcherFactory.compile(url.substring(1), config);
55+
return (state.parent.navigable || root).url.concat(url, config);
5856
}
5957

60-
if ($urlMatcherFactory.isMatcher(url) || url == null) {
61-
return url;
62-
}
58+
if (!url || $urlMatcherFactory.isMatcher(url)) return url;
6359
throw new Error("Invalid url '" + url + "' in state '" + state + "'");
6460
},
6561

@@ -71,10 +67,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
7167
// Derive parameters for this state and ensure they're a super-set of parent's parameters
7268
params: function(state) {
7369
if (!state.params) {
74-
return state.url ? state.url.parameters() : state.parent.params;
70+
return state.url ? state.url.params : state.parent.params;
7571
}
76-
if (!isArray(state.params)) throw new Error("Invalid params in state '" + state + "'");
77-
if (state.url) throw new Error("Both params and url specicified in state '" + state + "'");
7872
return state.params;
7973
},
8074

@@ -94,16 +88,18 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
9488
},
9589

9690
ownParams: function(state) {
91+
state.params = state.params || {};
92+
9793
if (!state.parent) {
98-
return state.params;
94+
return keys(state.params);
9995
}
100-
var paramNames = {}; forEach(state.params, function (p) { paramNames[p] = true; });
96+
var paramNames = {}; forEach(state.params, function (v, k) { paramNames[k] = true; });
10197

102-
forEach(state.parent.params, function (p) {
103-
if (!paramNames[p]) {
104-
throw new Error("Missing required parameter '" + p + "' in state '" + state.name + "'");
98+
forEach(state.parent.params, function (v, k) {
99+
if (!paramNames[k]) {
100+
throw new Error("Missing required parameter '" + k + "' in state '" + state.name + "'");
105101
}
106-
paramNames[p] = false;
102+
paramNames[k] = false;
107103
});
108104
var ownParams = [];
109105

@@ -782,8 +778,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
782778
toState = findState(to, options.relative);
783779

784780
if (!isDefined(toState)) {
785-
if (options.relative) throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'");
786-
throw new Error("No such state '" + to + "'");
781+
if (!options.relative) throw new Error("No such state '" + to + "'");
782+
throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'");
787783
}
788784
}
789785
if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'");
@@ -808,14 +804,14 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
808804
// TODO: We may not want to bump 'transition' if we're called from a location change
809805
// that we've initiated ourselves, because we might accidentally abort a legitimate
810806
// transition initiated from code?
811-
if (shouldTriggerReload(to, from, locals, options) ) {
807+
if (shouldTriggerReload(to, from, locals, options)) {
812808
if (to.self.reloadOnSearch !== false) $urlRouter.update();
813809
$state.transition = null;
814810
return $q.when($state.current);
815811
}
816812

817813
// Filter parameters before we pass them to event handlers etc.
818-
toParams = filterByKeys(to.params, toParams || {});
814+
toParams = filterByKeys(keys(to.params), toParams || {});
819815

820816
// Broadcast start event and cancel the transition if requested
821817
if (options.notify) {
@@ -1102,7 +1098,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
11021098
if (!nav || !nav.url) {
11031099
return null;
11041100
}
1105-
return $urlRouter.href(nav.url, filterByKeys(state.params, params || {}), { absolute: options.absolute });
1101+
return $urlRouter.href(nav.url, filterByKeys(keys(state.params), params || {}), { absolute: options.absolute });
11061102
};
11071103

11081104
/**
@@ -1132,7 +1128,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
11321128
// necessary. In addition to being available to the controller and onEnter/onExit callbacks,
11331129
// we also need $stateParams to be available for any $injector calls we make during the
11341130
// dependency resolution process.
1135-
var $stateParams = (paramsAreFiltered) ? params : filterByKeys(state.params, params);
1131+
var $stateParams = (paramsAreFiltered) ? params : filterByKeys(keys(state.params), params);
11361132
var locals = { $stateParams: $stateParams };
11371133

11381134
// Resolve 'global' dependencies for the state, i.e. those not specific to a view.

src/urlMatcherFactory.js

+57-33
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@
3838
* path into the parameter 'path'.
3939
* * `'/files/*path'` - ditto.
4040
*
41-
* @param {string} pattern the pattern to compile into a matcher.
42-
* @param {bool} caseInsensitiveMatch true if url matching should be case insensitive, otherwise false, the default value (for backward compatibility) is false.
41+
* @param {string} pattern The pattern to compile into a matcher.
42+
* @param {config} config A configuration object hash:
43+
*
44+
* * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`.
45+
* * `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`.
4346
*
4447
* @property {string} prefix A static prefix of this pattern. The matcher guarantees that any
4548
* URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns
@@ -54,9 +57,10 @@
5457
* @property {string} regex The constructed regex that will be used to match against the url when
5558
* it is time to determine which url will match.
5659
*
57-
* @returns {Object} New UrlMatcher object
60+
* @returns {Object} New `UrlMatcher` object
5861
*/
59-
function UrlMatcher(pattern, caseInsensitiveMatch) {
62+
function UrlMatcher(pattern, config) {
63+
config = angular.isObject(config) ? config : {};
6064

6165
// Find all placeholders and create a compiled pattern, using either classic or curly syntax:
6266
// '*' name
@@ -76,32 +80,41 @@ function UrlMatcher(pattern, caseInsensitiveMatch) {
7680
segments = this.segments = [],
7781
params = this.params = {};
7882

79-
function addParameter(id, type) {
83+
function addParameter(id, type, config) {
8084
if (!/^\w+(-+\w+)*$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'");
8185
if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'");
82-
params[id] = angular.isNumber(type) ? new Type() : type;
86+
params[id] = extend({ type: type || new Type() }, config);
87+
}
88+
89+
function quoteRegExp(string, pattern, isOptional) {
90+
var result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
91+
if (!pattern) return result;
92+
var flag = isOptional ? '?' : '';
93+
return result + flag + '(' + pattern + ')' + flag;
8394
}
8495

85-
function quoteRegExp(string) {
86-
return string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
96+
function paramConfig(param) {
97+
if (!config.params || !config.params[param]) return {};
98+
return config.params[param];
8799
}
88100

89101
this.source = pattern;
90102

91103
// Split into static segments separated by path parameter placeholders.
92104
// The number of segments is always 1 more than the number of parameters.
93-
var id, regexp, segment, type;
105+
var id, regexp, segment, type, cfg;
94106

95107
while ((m = placeholder.exec(pattern))) {
96108
id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null
97109
regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*');
98110
segment = pattern.substring(last, m.index);
99111
type = this.$types[regexp] || new Type({ pattern: new RegExp(regexp) });
112+
cfg = paramConfig(id);
100113

101114
if (segment.indexOf('?') >= 0) break; // we're into the search part
102115

103-
compiled += quoteRegExp(segment) + '(' + type.$subPattern() + ')';
104-
addParameter(id, type);
116+
compiled += quoteRegExp(segment, type.$subPattern(), isDefined(cfg.value));
117+
addParameter(id, type, cfg);
105118
segments.push(segment);
106119
last = placeholder.lastIndex;
107120
}
@@ -116,7 +129,9 @@ function UrlMatcher(pattern, caseInsensitiveMatch) {
116129
this.sourcePath = pattern.substring(0, last + i);
117130

118131
// Allow parameters to be separated by '?' as well as '&' to make concat() easier
119-
forEach(search.substring(1).split(/[&?]/), addParameter);
132+
forEach(search.substring(1).split(/[&?]/), function(key) {
133+
addParameter(key, null, paramConfig(key));
134+
});
120135
} else {
121136
this.sourcePath = pattern;
122137
this.sourceSearch = '';
@@ -125,7 +140,7 @@ function UrlMatcher(pattern, caseInsensitiveMatch) {
125140
compiled += quoteRegExp(segment) + '$';
126141
segments.push(segment);
127142

128-
this.regexp = (caseInsensitiveMatch) ? new RegExp(compiled, 'i') : new RegExp(compiled);
143+
this.regexp = RegExp(compiled, config.caseInsensitive ? 'i' : undefined);
129144
this.prefix = segments[0];
130145
}
131146

@@ -148,13 +163,14 @@ function UrlMatcher(pattern, caseInsensitiveMatch) {
148163
* ```
149164
*
150165
* @param {string} pattern The pattern to append.
166+
* @param {object} config An object hash of the configuration for the matcher.
151167
* @returns {ui.router.util.type:UrlMatcher} A matcher for the concatenated pattern.
152168
*/
153-
UrlMatcher.prototype.concat = function (pattern) {
169+
UrlMatcher.prototype.concat = function (pattern, config) {
154170
// Because order of search parameters is irrelevant, we can add our own search
155171
// parameters to the end of the new pattern. Parse the new pattern by itself
156172
// and then join the bits together, but it's much easier to do this on a string level.
157-
return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch);
173+
return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, config);
158174
};
159175

160176
UrlMatcher.prototype.toString = function () {
@@ -189,14 +205,14 @@ UrlMatcher.prototype.exec = function (path, searchParams) {
189205

190206
var params = this.parameters(), nTotal = params.length,
191207
nPath = this.segments.length - 1,
192-
values = {}, i, type, param;
208+
values = {}, i, cfg, param;
193209

194210
if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'");
195211

196212
for (i = 0; i < nPath; i++) {
197213
param = params[i];
198-
type = this.params[param];
199-
values[param] = type.decode(m[i + 1]);
214+
cfg = this.params[param];
215+
values[param] = cfg.type.decode(isDefined(m[i + 1]) ? m[i + 1] : cfg.value);
200216
}
201217
for (/**/; i < nTotal; i++) values[params[i]] = searchParams[params[i]];
202218

@@ -214,8 +230,9 @@ UrlMatcher.prototype.exec = function (path, searchParams) {
214230
* @returns {Array.<string>} An array of parameter names. Must be treated as read-only. If the
215231
* pattern has no parameters, an empty array is returned.
216232
*/
217-
UrlMatcher.prototype.parameters = function () {
218-
return keys(this.params);
233+
UrlMatcher.prototype.parameters = function (param) {
234+
if (!isDefined(param)) return keys(this.params);
235+
return this.params[param] || null;
219236
};
220237

221238
/**
@@ -231,11 +248,13 @@ UrlMatcher.prototype.parameters = function () {
231248
* @returns {Boolean} Returns `true` if `params` validates, otherwise `false`.
232249
*/
233250
UrlMatcher.prototype.validates = function (params) {
234-
var result = true, self = this;
251+
var result = true, isOptional, cfg, self = this;
235252

236253
forEach(params, function(val, key) {
237254
if (!self.params[key]) return;
238-
result = result && self.params[key].is(val);
255+
cfg = self.params[key];
256+
isOptional = !val && isDefined(cfg.value);
257+
result = result && (isOptional || cfg.type.is(val));
239258
});
240259
return result;
241260
};
@@ -261,18 +280,21 @@ UrlMatcher.prototype.validates = function (params) {
261280
*/
262281
UrlMatcher.prototype.format = function (values) {
263282
var segments = this.segments, params = this.parameters();
264-
if (!values) return segments.join('');
283+
284+
if (!values) return segments.join('').replace('//', '/');
265285

266286
var nPath = segments.length - 1, nTotal = params.length,
267-
result = segments[0], i, search, value, param, type;
287+
result = segments[0], i, search, value, param, cfg;
288+
289+
if (!this.validates(values)) return null;
268290

269291
for (i = 0; i < nPath; i++) {
270292
param = params[i];
271293
value = values[param];
272-
type = this.params[param];
294+
cfg = this.params[param];
273295

274-
if (!type.is(value)) return null;
275-
if (value != null) result += encodeURIComponent(type.encode(value));
296+
if (!isDefined(value) && (segments[i] === '/' || segments[i + 1] === '/')) continue;
297+
if (value != null) result += encodeURIComponent(cfg.type.encode(value));
276298
result += segments[i + 1];
277299
}
278300

@@ -324,14 +346,15 @@ Type.prototype.pattern = /.*/;
324346
*/
325347
function $UrlMatcherFactory() {
326348

327-
var useCaseInsensitiveMatch = false;
349+
var isCaseInsensitive = false;
328350

329351
var enqueue = true, typeQueue = [], injector, defaultTypes = {
330352
int: {
331353
decode: function(val) {
332354
return parseInt(val, 10);
333355
},
334356
is: function(val) {
357+
if (!isDefined(val)) return false;
335358
return this.decode(val.toString()) === val;
336359
},
337360
pattern: /\d+/
@@ -371,16 +394,16 @@ function $UrlMatcherFactory() {
371394

372395
/**
373396
* @ngdoc function
374-
* @name ui.router.util.$urlMatcherFactory#caseInsensitiveMatch
397+
* @name ui.router.util.$urlMatcherFactory#caseInsensitive
375398
* @methodOf ui.router.util.$urlMatcherFactory
376399
*
377400
* @description
378401
* Define if url matching should be case sensistive, the default behavior, or not.
379402
*
380403
* @param {bool} value false to match URL in a case sensitive manner; otherwise true;
381404
*/
382-
this.caseInsensitiveMatch = function(value) {
383-
useCaseInsensitiveMatch = value;
405+
this.caseInsensitive = function(value) {
406+
isCaseInsensitive = value;
384407
};
385408

386409
/**
@@ -392,10 +415,11 @@ function $UrlMatcherFactory() {
392415
* Creates a {@link ui.router.util.type:UrlMatcher} for the specified pattern.
393416
*
394417
* @param {string} pattern The URL pattern.
418+
* @param {object} config The config object hash.
395419
* @returns {ui.router.util.type:UrlMatcher} The UrlMatcher.
396420
*/
397-
this.compile = function (pattern) {
398-
return new UrlMatcher(pattern, useCaseInsensitiveMatch);
421+
this.compile = function (pattern, config) {
422+
return new UrlMatcher(pattern, extend({ caseInsensitive: isCaseInsensitive }, config));
399423
};
400424

401425
/**

0 commit comments

Comments
 (0)