Skip to content

Commit cb9fd9d

Browse files
fix($urlMatcherFactory): no longer generate unroutable urls
Previously, urlMatcherFactory would squash default param values and any double slashes from generated urls (by default). This behaviour caused urls generated with default values in them to not route back to the state that generated them. Change the default behaviour to squash only the param-value and not the URLs. Closes #1487 feat($urlMatcherFactory): configurable url param squashing Allow configuration of the squashing behavior of default-url-parameters on a per-param basis with reasonable default setting.
1 parent 725cda6 commit cb9fd9d

File tree

2 files changed

+97
-47
lines changed

2 files changed

+97
-47
lines changed

src/urlMatcherFactory.js

+76-24
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,15 @@ function UrlMatcher(pattern, config) {
9191
return params[id];
9292
}
9393

94-
function quoteRegExp(string, pattern, isOptional) {
95-
var result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
94+
function quoteRegExp(string, pattern, squashPolicy) {
95+
var flags = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
9696
if (!pattern) return result;
97-
var flag = isOptional ? '?' : '';
98-
return result + flag + '(' + pattern + ')' + flag;
97+
switch(squashPolicy) {
98+
case "nosquash": flags = ['', '']; break;
99+
case "value": flags = ['', '?']; break;
100+
case "slash": flags = ['?', '?']; break;
101+
}
102+
return result + flags[0] + '(' + pattern + ')' + flags[1];
99103
}
100104

101105
this.source = pattern;
@@ -122,7 +126,7 @@ function UrlMatcher(pattern, config) {
122126
if (p.segment.indexOf('?') >= 0) break; // we're into the search part
123127

124128
param = addParameter(p.id, p.type, p.cfg);
125-
compiled += quoteRegExp(p.segment, param.type.pattern.source, param.isOptional);
129+
compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash);
126130
segments.push(p.segment);
127131
last = placeholder.lastIndex;
128132
}
@@ -290,37 +294,47 @@ UrlMatcher.prototype.validates = function (params) {
290294
*/
291295
UrlMatcher.prototype.format = function (values) {
292296
var segments = this.segments, params = this.parameters();
293-
294-
if (!values) return segments.join('').replace('//', '/');
297+
var paramset = this.params;
298+
values = values || {};
295299

296300
var nPath = segments.length - 1, nTotal = params.length,
297-
result = segments[0], i, search, value, param, cfg, array;
301+
result = segments[0], i, search, value, name, param, array, isDefaultValue;
298302

299303
if (!this.validates(values)) return null;
300304

301305
for (i = 0; i < nPath; i++) {
302-
param = params[i];
303-
value = values[param];
304-
cfg = this.params[param];
305-
306-
if (!isDefined(value) && (segments[i] === '/' && segments[i + 1] === '/')) continue;
307-
if (value != null) result += encodeURIComponent(cfg.type.encode(value));
308-
result += segments[i + 1];
306+
name = params[i];
307+
param = paramset[name];
308+
value = param.value(values[name]);
309+
isDefaultValue = param.isOptional && param.type.equals(param.value(), value);
310+
var squash = isDefaultValue ? param.squash : "nosquash";
311+
var encoded = param.type.encode(value);
312+
313+
var nextSegment = segments[i + 1];
314+
if (squash === "nosquash") {
315+
if (encoded != null) result += encodeURIComponent(encoded);
316+
result += nextSegment;
317+
} else if (squash === "value") {
318+
result += nextSegment;
319+
} else if (squash === "slash") {
320+
var capture = result.match(/\/$/) ? /\/?(.*)/ : /(.*)/;
321+
result += nextSegment.match(capture)[1];
322+
}
309323
}
310324

311325
for (/**/; i < nTotal; i++) {
312-
param = params[i];
313-
value = values[param];
326+
name = params[i];
327+
value = values[name];
314328
if (value == null) continue;
315329
array = isArray(value);
316330

317331
if (array) {
318-
value = value.map(encodeURIComponent).join('&' + param + '=');
332+
value = value.map(encodeURIComponent).join('&' + name + '=');
319333
}
320-
result += (search ? '&' : '?') + param + '=' + (array ? value : encodeURIComponent(value));
334+
result += (search ? '&' : '?') + name + '=' + (array ? value : encodeURIComponent(value));
321335
search = true;
322336
}
323-
return result.replace('//', '/');
337+
return result;
324338
};
325339

326340
/**
@@ -446,9 +460,9 @@ Type.prototype.pattern = /.*/;
446460
function $UrlMatcherFactory() {
447461
$$UMFP = this;
448462

449-
var isCaseInsensitive = false, isStrictMode = true;
463+
var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = "nosquash";
450464

451-
function safeString(val) { return isDefined(val) ? val.toString() : val; }
465+
function safeString(val) { return val != null ? val.toString() : val; }
452466
function coerceEquals(left, right) { return left == right; }
453467
function angularEquals(left, right) { return angular.equals(left, right); }
454468
// TODO: function regexpMatches(val) { return isDefined(val) && this.pattern.test(val); }
@@ -569,6 +583,28 @@ function $UrlMatcherFactory() {
569583
isStrictMode = value;
570584
};
571585

586+
/**
587+
* @ngdoc function
588+
* @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy
589+
* @methodOf ui.router.util.$urlMatcherFactory
590+
*
591+
* @description
592+
* Sets the default behavior when generating or matching URLs with default parameter values.
593+
*
594+
* @param {string} value A string that defines the default parameter URL squashing behavior.
595+
* `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL
596+
* `value`: When generating an href with a default parameter value, squash (remove) the parameter value from the URL
597+
* `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the
598+
* parameter is surrounded by slashes, squash (remove) one slash from the URL
599+
*/
600+
this.defaultSquashPolicy = function(value) {
601+
if (!value) return defaultSquashPolicy;
602+
if (value !== "nosquash" && value !== "value" && value !== "slash")
603+
throw new Error("Invalid squash policy: " + value + ". Valid policies: 'nosquash', 'value', 'slash'");
604+
defaultSquashPolicy = value;
605+
return value;
606+
};
607+
572608
/**
573609
* @ngdoc function
574610
* @name ui.router.util.$urlMatcherFactory#compile
@@ -758,10 +794,12 @@ function $UrlMatcherFactory() {
758794
var defaultValueConfig = getDefaultValueConfig(config);
759795
config = config || {};
760796
type = getType(config, type);
797+
var isOptional = defaultValueConfig.value !== undefined;
798+
var squash = getSquashPolicy(config, isOptional);
761799

762800
function getDefaultValueConfig(config) {
763801
var keys = isObject(config) ? objectKeys(config) : [];
764-
var isShorthand = keys.indexOf("value") === -1 && keys.indexOf("type") === -1;
802+
var isShorthand = keys.indexOf("value") === -1 && keys.indexOf("type") === -1 && keys.indexOf("squash") === -1;
765803
var configValue = isShorthand ? config : config.value;
766804
return {
767805
fn: isInjectable(configValue) ? configValue : function () { return configValue; },
@@ -776,6 +814,19 @@ function $UrlMatcherFactory() {
776814
return config.type instanceof Type ? config.type : new Type(config.type);
777815
}
778816

817+
/**
818+
* returns "nosquash", "value", "slash" to indicate the "default parameter url squash policy".
819+
* undefined aliases to urlMatcherFactory default. `false` aliases to "nosquash". `true` aliases to "slash".
820+
*/
821+
function getSquashPolicy(config, isOptional) {
822+
var squash = config.squash;
823+
if (!isOptional || squash === false) return "nosquash";
824+
if (!isDefined(squash)) return defaultSquashPolicy;
825+
if (squash === true) return "slash";
826+
if (squash === "nosquash" || squash === "value" || squash === "slash") return squash;
827+
throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: 'nosquash' (false), 'value', 'slash' (true)");
828+
}
829+
779830
/**
780831
* [Internal] Get the default value of a parameter, which may be an injectable function.
781832
*/
@@ -796,8 +847,9 @@ function $UrlMatcherFactory() {
796847
id: id,
797848
type: type,
798849
config: config,
850+
squash: squash,
799851
dynamic: undefined,
800-
isOptional: defaultValueConfig.value !== undefined,
852+
isOptional: isOptional,
801853
value: $value
802854
});
803855
};

test/urlMatcherFactorySpec.js

+21-23
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ describe("urlMatcherFactory", function () {
280280
describe("optional parameters", function() {
281281
it("should match with or without values", function () {
282282
var m = new UrlMatcher('/users/{id:int}', {
283-
params: { id: { value: null } }
283+
params: { id: { value: null, squash: true } }
284284
});
285285
expect(m.exec('/users/1138')).toEqual({ id: 1138 });
286286
expect(m.exec('/users/').id).toBeNull();
@@ -289,7 +289,7 @@ describe("urlMatcherFactory", function () {
289289

290290
it("should correctly match multiple", function() {
291291
var m = new UrlMatcher('/users/{id:int}/{state:[A-Z]+}', {
292-
params: { id: { value: null }, state: { value: null } }
292+
params: { id: { value: null, squash: true }, state: { value: null, squash: true } }
293293
});
294294
expect(m.exec('/users/1138')).toEqual({ id: 1138, state: null });
295295
expect(m.exec('/users/1138/NY')).toEqual({ id: 1138, state: "NY" });
@@ -314,7 +314,7 @@ describe("urlMatcherFactory", function () {
314314

315315
it("should correctly format multiple", function() {
316316
var m = new UrlMatcher('/users/{id:int}/{state:[A-Z]+}', {
317-
params: { id: { value: null }, state: { value: null } }
317+
params: { id: { value: null, squash: true }, state: { value: null, squash: true } }
318318
});
319319

320320
expect(m.format()).toBe("/users/");
@@ -325,7 +325,7 @@ describe("urlMatcherFactory", function () {
325325

326326
it("should match in between static segments", function() {
327327
var m = new UrlMatcher('/users/{user:int}/photos', {
328-
params: { user: 5 }
328+
params: { user: { value: 5, squash: true } }
329329
});
330330
expect(m.exec('/users/photos').user).toBe(5);
331331
expect(m.exec('/users/6/photos').user).toBe(6);
@@ -334,20 +334,20 @@ describe("urlMatcherFactory", function () {
334334
});
335335

336336
it("should correctly format with an optional followed by a required parameter", function() {
337-
var m = new UrlMatcher('/:user/gallery/photos/:photo', {
337+
var m = new UrlMatcher('/home/:user/gallery/photos/:photo', {
338338
params: {
339-
user: {value: null},
340-
photo: {}
339+
user: {value: null, squash: true},
340+
photo: undefined
341341
}
342342
});
343-
expect(m.format({ photo: 12 })).toBe("/gallery/photos/12");
344-
expect(m.format({ user: 1138, photo: 13 })).toBe("/1138/gallery/photos/13");
343+
expect(m.format({ photo: 12 })).toBe("/home/gallery/photos/12");
344+
expect(m.format({ user: 1138, photo: 13 })).toBe("/home/1138/gallery/photos/13");
345345
});
346346

347347
describe("default values", function() {
348348
it("should populate if not supplied in URL", function() {
349349
var m = new UrlMatcher('/users/{id:int}/{test}', {
350-
params: { id: { value: 0 }, test: { value: "foo" } }
350+
params: { id: { value: 0, squash: true }, test: { value: "foo", squash: true } }
351351
});
352352
expect(m.exec('/users')).toEqual({ id: 0, test: "foo" });
353353
expect(m.exec('/users/2')).toEqual({ id: 2, test: "foo" });
@@ -360,7 +360,7 @@ describe("urlMatcherFactory", function () {
360360
var m = new UrlMatcher('/foo/:foo', {
361361
params: { foo: "bar" }
362362
});
363-
expect(m.exec("/foo")).toEqual({ foo: "bar" });
363+
expect(m.exec("/foo/")).toEqual({ foo: "bar" });
364364
});
365365

366366
it("should populate query params", function() {
@@ -372,21 +372,19 @@ describe("urlMatcherFactory", function () {
372372
});
373373

374374
it("should allow function-calculated values", function() {
375+
function barFn() { return "Value from bar()"; }
375376
var m = new UrlMatcher('/foo/:bar', {
376-
params: {
377-
bar: function() {
378-
return "Value from bar()";
379-
}
380-
}
377+
params: { bar: barFn }
378+
});
379+
expect(m.exec('/foo/').bar).toBe("Value from bar()");
380+
381+
m = new UrlMatcher('/foo/:bar', {
382+
params: { bar: { value: barFn, squash: true } }
381383
});
382384
expect(m.exec('/foo').bar).toBe("Value from bar()");
383385

384-
var m = new UrlMatcher('/foo?bar', {
385-
params: {
386-
bar: function() {
387-
return "Value from bar()";
388-
}
389-
}
386+
m = new UrlMatcher('/foo?bar', {
387+
params: { bar: barFn }
390388
});
391389
expect(m.exec('/foo').bar).toBe("Value from bar()");
392390
});
@@ -402,7 +400,7 @@ describe("urlMatcherFactory", function () {
402400
var user = { name: "Bob" };
403401

404402
$stateParams.user = user;
405-
expect(m.exec('/users').user).toBe(user);
403+
expect(m.exec('/users/').user).toBe(user);
406404
}));
407405
});
408406
});

0 commit comments

Comments
 (0)