Skip to content

Commit 2371ba9

Browse files
fix($urlMatcherFactory): default to parameter string coersion.
feat($urlMatcherFactory): unify params handling code for path/search feat($urlMatcherFactory): add a defaultType that does string coersion and supports arrays (for params) feat($urlMatcherFactory): separate default Type(s) for path/query params Closes #1414
1 parent 8dd978d commit 2371ba9

File tree

4 files changed

+69
-27
lines changed

4 files changed

+69
-27
lines changed

src/state.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -784,8 +784,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
784784
if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState);
785785
if (!toState.params.$$validates(toParams)) return TransitionFailed;
786786

787-
var defaultParams = toState.params.$$values();
788-
toParams = extend(defaultParams, toParams);
787+
toParams = toState.params.$$values(toParams);
789788
to = toState;
790789

791790
var toPath = to.path;
@@ -794,7 +793,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
794793
var keep = 0, state = toPath[keep], locals = root.locals, toLocals = [];
795794

796795
if (!options.reload) {
797-
while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams.$$keys())) {
796+
while (state && state === fromPath[keep] && state.ownParams.$$equals(toParams, fromParams)) {
798797
locals = toLocals[keep] = state.locals;
799798
keep++;
800799
state = toPath[keep];

src/urlMatcherFactory.js

+61-18
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,14 @@ function UrlMatcher(pattern, config) {
7070
// The regular expression is somewhat complicated due to the need to allow curly braces
7171
// inside the regular expression. The placeholder regexp breaks down as follows:
7272
// ([:*])(\w+) classic placeholder ($1 / $2)
73-
// \{(\w+)(?:\:( ... ))?\} curly brace placeholder ($3) with optional regexp ... ($4)
73+
// ([:]?)([\w-]+) classic search placeholder (supports snake-case-params) ($1 / $2)
74+
// \{(\w+)(?:\:( ... ))?\} curly brace placeholder ($3) with optional regexp/type ... ($4)
7475
// (?: ... | ... | ... )+ the regexp consists of any number of atoms, an atom being either
7576
// [^{}\\]+ - anything other than curly braces or backslash
7677
// \\. - a backslash escape
7778
// \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms
7879
var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
80+
searchPlaceholder = /([:]?)([\w-]+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
7981
compiled = '^', last = 0, m,
8082
segments = this.segments = [],
8183
params = this.params = new $$UrlMatcherFactoryProvider.ParamSet();
@@ -106,20 +108,26 @@ function UrlMatcher(pattern, config) {
106108

107109
// Split into static segments separated by path parameter placeholders.
108110
// The number of segments is always 1 more than the number of parameters.
109-
var id, regexp, segment, type, cfg;
110-
111-
while ((m = placeholder.exec(pattern))) {
111+
function matchDetails(m, isSearch) {
112+
var id, regexp, segment, type, cfg;
112113
id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null
113-
regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*');
114+
regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : '[^/]*');
114115
segment = pattern.substring(last, m.index);
115-
type = this.$types[regexp] || regexpType(regexp);
116+
type = regexp ? UrlMatcher.prototype.$types[regexp] || regexpType(regexp) : undefined;
116117
cfg = config.params[id];
118+
return {
119+
id: id, regexp: regexp, segment: segment, type: type, cfg: cfg
120+
};
121+
}
117122

118-
if (segment.indexOf('?') >= 0) break; // we're into the search part
123+
var p, param, segment;
124+
while ((m = placeholder.exec(pattern))) {
125+
p = matchDetails(m, false);
126+
if (p.segment.indexOf('?') >= 0) break; // we're into the search part
119127

120-
var param = addParameter(id, type, cfg);
121-
compiled += quoteRegExp(segment, type.$subPattern(), param.isOptional);
122-
segments.push(segment);
128+
param = addParameter(p.id, p.type, p.cfg);
129+
compiled += quoteRegExp(p.segment, p.type.$subPattern(), param.isOptional);
130+
segments.push(p.segment);
123131
last = placeholder.lastIndex;
124132
}
125133
segment = pattern.substring(last);
@@ -132,10 +140,15 @@ function UrlMatcher(pattern, config) {
132140
segment = segment.substring(0, i);
133141
this.sourcePath = pattern.substring(0, last + i);
134142

135-
// Allow parameters to be separated by '?' as well as '&' to make concat() easier
136-
forEach(search.substring(1).split(/[&?]/), function(key) {
137-
addParameter(key, null, config.params[key]);
138-
});
143+
if (search.length > 0) {
144+
last = 0;
145+
while ((m = searchPlaceholder.exec(search))) {
146+
p = matchDetails(m, true);
147+
param = addParameter(p.id, p.type, p.cfg);
148+
last = placeholder.lastIndex;
149+
// check if ?&
150+
}
151+
}
139152
} else {
140153
this.sourcePath = pattern;
141154
this.sourceSearch = '';
@@ -385,7 +398,7 @@ Type.prototype.encode = function(val, key) {
385398
* @methodOf ui.router.util.type:Type
386399
*
387400
* @description
388-
* Converts a string URL parameter value to a custom/native value.
401+
* Converts a parameter value (from URL string or transition param) to a custom/native value.
389402
*
390403
* @param {string} val The URL parameter value to decode.
391404
* @param {string} key The name of the parameter in which `val` is stored. Can be used for
@@ -433,7 +446,36 @@ function $UrlMatcherFactory() {
433446

434447
var isCaseInsensitive = false, isStrictMode = true;
435448

449+
function safeString(val) { return isDefined(val) ? val.toString() : val; }
450+
function coerceEquals(left, right) { return left == right; }
451+
function angularEquals(left, right) { return angular.equals(left, right); }
452+
// TODO: function regexpMatches(val) { return isDefined(val) && this.pattern.test(val); }
453+
function regexpMatches(val) { /*jshint validthis:true */ return this.pattern.test(val); }
454+
function normalizeStringOrArray(val) {
455+
if (isArray(val)) {
456+
var encoded = [];
457+
forEach(val, function(item) { encoded.push(safeString(item)); });
458+
return encoded;
459+
} else {
460+
return safeString(val);
461+
}
462+
}
463+
436464
var enqueue = true, typeQueue = [], injector, defaultTypes = {
465+
"searchParam": {
466+
encode: normalizeStringOrArray,
467+
decode: normalizeStringOrArray,
468+
equals: angularEquals,
469+
is: regexpMatches,
470+
pattern: /[^&?]*/
471+
},
472+
"pathParam": {
473+
encde: safeString,
474+
decode: safeString,
475+
equals: coerceEquals,
476+
is: regexpMatches,
477+
pattern: /[^/]*/
478+
},
437479
int: {
438480
decode: function(val) {
439481
return parseInt(val, 10);
@@ -449,7 +491,7 @@ function $UrlMatcherFactory() {
449491
return val ? 1 : 0;
450492
},
451493
decode: function(val) {
452-
return parseInt(val, 10) === 0 ? false : true;
494+
return parseInt(val, 10) !== 0;
453495
},
454496
is: function(val) {
455497
return val === true || val === false;
@@ -720,8 +762,9 @@ function $UrlMatcherFactory() {
720762

721763
function getType(config, urlType) {
722764
if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations.");
723-
if (urlType && !config.type) return urlType;
724-
return config.type instanceof Type ? config.type : new Type(config.type || {});
765+
if (urlType) return urlType;
766+
if (!config.type) return UrlMatcher.prototype.$types.pathParam;
767+
return config.type instanceof Type ? config.type : new Type(config.type);
725768
}
726769

727770
/**

test/stateDirectivesSpec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ describe('uiStateRef', function() {
130130
$q.flush();
131131

132132
expect($state.current.name).toEqual('contacts.item.detail');
133-
expect($stateParams).toEqual({ id: 5 });
133+
expect($stateParams).toEqual({ id: "5" });
134134
}));
135135

136136
it('should transition when given a click that contains no data (fake-click)', inject(function($state, $stateParams, $q) {
@@ -147,7 +147,7 @@ describe('uiStateRef', function() {
147147
$q.flush();
148148

149149
expect($state.current.name).toEqual('contacts.item.detail');
150-
expect($stateParams).toEqual({ id: 5 });
150+
expect($stateParams).toEqual({ id: "5" });
151151
}));
152152

153153
it('should not transition states when ctrl-clicked', inject(function($state, $stateParams, $q) {

test/stateSpec.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ describe('state', function () {
273273
$q.flush();
274274
expect(called).toBeTruthy();
275275
expect($state.current.name).toEqual('DDD');
276-
expect($state.params).toEqual({ x: 1, y: 2, z: 3, w: 4 });
276+
expect($state.params).toEqual({ x: "1", y: "2", z: "3", w: "4" });
277277
}));
278278

279279
it('can defer a state transition in $stateNotFound', inject(function ($state, $q, $rootScope) {
@@ -290,7 +290,7 @@ describe('state', function () {
290290
$q.flush();
291291
expect(called).toBeTruthy();
292292
expect($state.current.name).toEqual('AA');
293-
expect($state.params).toEqual({ a: 1 });
293+
expect($state.params).toEqual({ a: "1" });
294294
}));
295295

296296
it('can defer and supersede a state transition in $stateNotFound', inject(function ($state, $q, $rootScope) {
@@ -996,7 +996,7 @@ describe('state', function () {
996996
$state.go('root.sub1', { param2: 2 });
997997
$q.flush();
998998
expect($state.current.name).toEqual('root.sub1');
999-
expect($stateParams).toEqual({ param1: 1, param2: 2 });
999+
expect($stateParams).toEqual({ param1: "1", param2: "2" });
10001000
}));
10011001

10021002
it('should not inherit siblings\' states', inject(function ($state, $stateParams, $q) {
@@ -1009,7 +1009,7 @@ describe('state', function () {
10091009
$q.flush();
10101010
expect($state.current.name).toEqual('root.sub2');
10111011

1012-
expect($stateParams).toEqual({ param1: 1, param2: undefined });
1012+
expect($stateParams).toEqual({ param1: "1", param2: undefined });
10131013
}));
10141014
});
10151015

0 commit comments

Comments
 (0)