Skip to content

Commit 6374a3e

Browse files
fix($urlMatcherFactory): Pre-replace certain param values for better mapping
- Some parameter values can generate a non-bidirectional URL. For example, $state.go('foo', { param: null }) can map to the url "/foo/", but that url would match as ('foo', {param: ""}). Allow certain special values to be pre-replaced in Param.value() to support better support bi-directional URL mapping. - Switch squash policy to true/false instead of "squash"/"nosquash" - Allow squash policy to be an arbitrary string, used as a placeholder in the url (this uses the pre-replace feature to map that string to undefined)
1 parent 9136fec commit 6374a3e

File tree

2 files changed

+111
-39
lines changed

2 files changed

+111
-39
lines changed

src/urlMatcherFactory.js

+49-39
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,15 @@ function UrlMatcher(pattern, config, parentMatcher) {
9494
return params[id];
9595
}
9696

97-
function quoteRegExp(string, pattern, squashPolicy) {
98-
var flags = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
97+
function quoteRegExp(string, pattern, squash) {
98+
var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
9999
if (!pattern) return result;
100-
switch(squashPolicy) {
101-
case "nosquash": flags = ['', '']; break;
102-
case "value": flags = ['', '?']; break;
103-
case "slash": flags = ['?', '?']; break;
100+
switch(squash) {
101+
case false: surroundPattern = ['(', ')']; break;
102+
case true: surroundPattern = ['?(', ')?']; break;
103+
default: surroundPattern = ['(' + squash + "|", ')?']; break;
104104
}
105-
return result + flags[0] + '(' + pattern + ')' + flags[1];
105+
return result + surroundPattern[0] + pattern + surroundPattern[1];
106106
}
107107

108108
this.source = pattern;
@@ -231,7 +231,7 @@ UrlMatcher.prototype.exec = function (path, searchParams) {
231231

232232
var paramNames = this.parameters(), nTotal = paramNames.length,
233233
nPath = this.segments.length - 1,
234-
values = {}, i, cfg, paramName;
234+
values = {}, i, j, cfg, paramName;
235235

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

@@ -244,8 +244,11 @@ UrlMatcher.prototype.exec = function (path, searchParams) {
244244
for (i = 0; i < nPath; i++) {
245245
paramName = paramNames[i];
246246
var param = this.params[paramName];
247-
// if the param is optional, convert an empty string to `undefined`
248-
var paramVal = m[i+1] === "" ? param.emptyString : m[i+1];
247+
var paramVal = m[i+1];
248+
// if the param value matches a pre-replace pair, replace the value before decoding.
249+
for (j = 0; j < param.replace; j++) {
250+
if (param.replace[j].from === paramVal) paramVal = param.replace[j].to;
251+
}
249252
if (paramVal && param.array === true) paramVal = decodePathArray(paramVal);
250253
values[paramName] = param.value(paramVal);
251254
}
@@ -323,12 +326,12 @@ UrlMatcher.prototype.format = function (values) {
323326
var isPathParam = i < nPath;
324327
var name = params[i], param = paramset[name], value = param.value(values[name]);
325328
var isDefaultValue = param.isOptional && param.type.equals(param.value(), value);
326-
var squash = isDefaultValue ? param.squash : "nosquash";
329+
var squash = isDefaultValue ? param.squash : false;
327330
var encoded = param.type.encode(value);
328331

329332
if (isPathParam) {
330333
var nextSegment = segments[i + 1];
331-
if (squash === "nosquash") {
334+
if (squash === false) {
332335
if (encoded != null) {
333336
if (isArray(encoded)) {
334337
result += encoded.map(encodeDashes).join("-");
@@ -337,14 +340,14 @@ UrlMatcher.prototype.format = function (values) {
337340
}
338341
}
339342
result += nextSegment;
340-
} else if (squash === "value") {
341-
result += nextSegment;
342-
} else if (squash === "slash") {
343+
} else if (squash === true) {
343344
var capture = result.match(/\/$/) ? /\/?(.*)/ : /(.*)/;
344345
result += nextSegment.match(capture)[1];
346+
} else if (isString(squash)) {
347+
result += squash + nextSegment;
345348
}
346349
} else {
347-
if (encoded == null || (isDefaultValue && squash !== "nosquash")) continue;
350+
if (encoded == null || (isDefaultValue && squash !== false)) continue;
348351
if (!isArray(encoded)) encoded = [ encoded ];
349352
encoded = encoded.map(encodeURIComponent).join('&' + name + '=');
350353
result += (search ? '&' : '?') + (name + '=' + encoded);
@@ -525,7 +528,7 @@ Type.prototype.$asArray = function(mode, isSearch) {
525528
function $UrlMatcherFactory() {
526529
$$UMFP = this;
527530

528-
var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = "nosquash";
531+
var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false;
529532

530533
function valToString(val) { return val != null ? val.toString().replace("/", "%2F") : val; }
531534
function valFromString(val) { return val != null ? val.toString().replace("%2F", "/") : val; }
@@ -631,14 +634,15 @@ function $UrlMatcherFactory() {
631634
*
632635
* @param {string} value A string that defines the default parameter URL squashing behavior.
633636
* `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL
634-
* `value`: When generating an href with a default parameter value, squash (remove) the parameter value from the URL
635637
* `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the
636638
* parameter is surrounded by slashes, squash (remove) one slash from the URL
639+
* any other string, e.g. "~": When generating an href with a default parameter value, squash (remove)
640+
* the parameter value from the URL and replace it with this string.
637641
*/
638642
this.defaultSquashPolicy = function(value) {
639-
if (!value) return defaultSquashPolicy;
640-
if (value !== "nosquash" && value !== "value" && value !== "slash")
641-
throw new Error("Invalid squash policy: " + value + ". Valid policies: 'nosquash', 'value', 'slash'");
643+
if (!isDefined(value)) return defaultSquashPolicy;
644+
if (value !== true && value !== false && !isString(value))
645+
throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string");
642646
defaultSquashPolicy = value;
643647
return value;
644648
};
@@ -836,7 +840,7 @@ function $UrlMatcherFactory() {
836840
type = arrayMode ? type.$asArray(arrayMode, isSearch) : type;
837841
var isOptional = defaultValueConfig.value !== undefined;
838842
var squash = getSquashPolicy(config, isOptional);
839-
var emptyString = getEmptyStringValue(config, arrayMode, isOptional);
843+
var replace = getReplace(config, arrayMode, isOptional, squash);
840844

841845
function getDefaultValueConfig(config) {
842846
var keys = isObject(config) ? objectKeys(config) : [];
@@ -864,25 +868,26 @@ function $UrlMatcherFactory() {
864868
}
865869

866870
/**
867-
* returns "nosquash", "value", "slash" to indicate the "default parameter url squash policy".
868-
* undefined aliases to urlMatcherFactory default. `false` aliases to "nosquash". `true` aliases to "slash".
871+
* returns false, true, or the squash value to indicate the "default parameter url squash policy".
869872
*/
870873
function getSquashPolicy(config, isOptional) {
871874
var squash = config.squash;
872-
if (!isOptional || squash === false) return "nosquash";
873-
if (!isDefined(squash)) return defaultSquashPolicy;
874-
if (squash === true) return "slash";
875-
if (squash === "nosquash" || squash === "value" || squash === "slash") return squash;
876-
throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: 'nosquash' (false), 'value', 'slash' (true)");
875+
if (!isOptional || squash === false) return false;
876+
if (!isDefined(squash) || squash == null) return defaultSquashPolicy;
877+
if (squash === true || isString(squash)) return squash;
878+
throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string");
877879
}
878880

879-
/**
880-
* Returns "" or undefined, or whatever is defined in the param's config.emptyString.
881-
* If the parameter was matched in a URL, but was matched as an empty string, this value will be used instead.
882-
*/
883-
function getEmptyStringValue(config, arrayMode, isOptional) {
884-
var defaultPolicy = { emptyString: (isOptional || arrayMode ? undefined : "") };
885-
return extend(defaultPolicy, config).emptyString;
881+
function getReplace(config, arrayMode, isOptional, squash) {
882+
var replace, configuredKeys, defaultPolicy = [
883+
{ from: "", to: (isOptional || arrayMode ? undefined : "") },
884+
{ from: null, to: (isOptional || arrayMode ? undefined : "") }
885+
];
886+
replace = isArray(config.replace) ? config.replace : [];
887+
if (isString(squash))
888+
replace.push({ from: squash, to: undefined });
889+
configuredKeys = replace.map(function(item) { return item.from; } );
890+
return defaultPolicy.filter(function(item) { return configuredKeys.indexOf(item.from) === -1; }).concat(replace);
886891
}
887892

888893
/**
@@ -898,19 +903,24 @@ function $UrlMatcherFactory() {
898903
* default value, which may be the result of an injectable function.
899904
*/
900905
function $value(value) {
901-
if (value === "") value = self.emptyString;
906+
function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; }
907+
function $replace(value) {
908+
var replacement = self.replace.filter(hasReplaceVal(value)).map(function(obj) { return obj.to; });
909+
return replacement.length ? replacement[0] : value;
910+
}
911+
value = $replace(value);
902912
return isDefined(value) ? self.type.decode(value) : $$getDefaultValue();
903913
}
904914

905-
function toString() { return "{Param:" + id + " " + type + " squash: " + squash + " optional: " + isOptional + "}"; }
915+
function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; }
906916

907917
extend(this, {
908918
id: id,
909919
type: type,
910920
array: arrayMode,
911921
config: config,
912922
squash: squash,
913-
emptyString: emptyString,
923+
replace: replace,
914924
isOptional: isOptional,
915925
dynamic: undefined,
916926
value: $value,

test/urlMatcherFactorySpec.js

+62
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,68 @@ describe("urlMatcherFactory", function () {
554554
$stateParams.user = user;
555555
expect(m.exec('/users/').user).toBe(user);
556556
}));
557+
558+
describe("squash policy", function() {
559+
var Session = { username: "loggedinuser" };
560+
function getMatcher(squash) {
561+
return new UrlMatcher('/user/:userid/gallery/:galleryid/photo/:photoid', {
562+
params: {
563+
userid: { squash: squash, value: function () { return Session.username; } },
564+
galleryid: { squash: squash, value: "favorites" }
565+
}
566+
});
567+
}
568+
569+
it(": true should squash the default value and one slash", inject(function($stateParams) {
570+
var m = getMatcher(true);
571+
572+
var defaultParams = { userid: 'loggedinuser', galleryid: 'favorites', photoid: '123'};
573+
expect(m.exec('/user/gallery/photo/123')).toEqual(defaultParams);
574+
expect(m.exec('/user//gallery//photo/123')).toEqual(defaultParams);
575+
expect(m.format(defaultParams)).toBe('/user/gallery/photo/123');
576+
577+
var nonDefaultParams = { userid: 'otheruser', galleryid: 'travel', photoid: '987'};
578+
expect(m.exec('/user/otheruser/gallery/travel/photo/987')).toEqual(nonDefaultParams);
579+
expect(m.format(nonDefaultParams)).toBe('/user/otheruser/gallery/travel/photo/987');
580+
}));
581+
582+
it(": false should not squash default values", inject(function($stateParams) {
583+
var m = getMatcher(false);
584+
585+
var defaultParams = { userid: 'loggedinuser', galleryid: 'favorites', photoid: '123'};
586+
expect(m.exec('/user/loggedinuser/gallery/favorites/photo/123')).toEqual(defaultParams);
587+
expect(m.format(defaultParams)).toBe('/user/loggedinuser/gallery/favorites/photo/123');
588+
589+
var nonDefaultParams = { userid: 'otheruser', galleryid: 'travel', photoid: '987'};
590+
expect(m.exec('/user/otheruser/gallery/travel/photo/987')).toEqual(nonDefaultParams);
591+
expect(m.format(nonDefaultParams)).toBe('/user/otheruser/gallery/travel/photo/987');
592+
}));
593+
594+
it(": '' should squash the default value to an empty string", inject(function($stateParams) {
595+
var m = getMatcher("");
596+
597+
var defaultParams = { userid: 'loggedinuser', galleryid: 'favorites', photoid: '123'};
598+
expect(m.exec('/user//gallery//photo/123')).toEqual(defaultParams);
599+
expect(m.format(defaultParams)).toBe('/user//gallery//photo/123');
600+
601+
var nonDefaultParams = { userid: 'otheruser', galleryid: 'travel', photoid: '987'};
602+
expect(m.exec('/user/otheruser/gallery/travel/photo/987')).toEqual(nonDefaultParams);
603+
expect(m.format(nonDefaultParams)).toBe('/user/otheruser/gallery/travel/photo/987');
604+
}));
605+
606+
it(": '~' should squash the default value and replace it with '~'", inject(function($stateParams) {
607+
var m = getMatcher("~");
608+
609+
var defaultParams = { userid: 'loggedinuser', galleryid: 'favorites', photoid: '123'};
610+
expect(m.exec('/user//gallery//photo/123')).toEqual(defaultParams);
611+
expect(m.exec('/user/~/gallery/~/photo/123')).toEqual(defaultParams);
612+
expect(m.format(defaultParams)).toBe('/user/~/gallery/~/photo/123');
613+
614+
var nonDefaultParams = { userid: 'otheruser', galleryid: 'travel', photoid: '987'};
615+
expect(m.exec('/user/otheruser/gallery/travel/photo/987')).toEqual(nonDefaultParams);
616+
expect(m.format(nonDefaultParams)).toBe('/user/otheruser/gallery/travel/photo/987');
617+
}));
618+
});
557619
});
558620
});
559621

0 commit comments

Comments
 (0)