Skip to content

Parameter Typing -- URL Only #454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
322c85b
add flag in .transitionTo() to allow ability to disable broadcast of …
mattbroekhuis Sep 10, 2013
8518db2
can register a type
Sep 19, 2013
5220d5c
UrlMatcher looks for types in the Url and generates a typeMap between…
Sep 19, 2013
8c30b39
format decodes typed values. removed normalization that was forcing b…
Sep 20, 2013
438a62a
Correctly detect left-click on IE8, fixes #452.
nateabele Sep 21, 2013
4cdadcf
fix($resolve): resolve factories from string names
nateabele Sep 21, 2013
b7b2ceb
Merge pull request #451 from littlebitselectronics/component-support
nateabele Sep 22, 2013
d3c6a5d
added is and normalization to type
Sep 23, 2013
0cd78ae
matcher.exec checks typed parameters are of correct type
Sep 23, 2013
45e2639
split pattern and is
Sep 23, 2013
78f80a7
format checks the type before encoding
Sep 23, 2013
308cc1d
evaluates type equality when transitioning
Sep 23, 2013
ea9b3a7
added sample for custom type. documented the type function
Sep 24, 2013
7ac3739
Merge branch 'master' of github.com:toddhgardner/ui-router into typed…
Sep 24, 2013
e22517d
fixed equality in integer type
Sep 24, 2013
8c93f6b
Merge branch 'master' of github.com:toddhgardner/ui-router into typed…
Sep 24, 2013
c3859b8
merge
Sep 24, 2013
bcd71cb
fixing jsdoc for windows
Sep 24, 2013
c9da9f2
fixing jsdoc for windows
Sep 24, 2013
841239c
fixing release for windows
Sep 24, 2013
cf3e5b6
for 0.2.1. release
Sep 24, 2013
7e96189
release 0.2.1
Sep 24, 2013
92249d2
added simple typing to UrlMatch Contructor search params
Sep 24, 2013
04cc8a0
added anonymous type registration for search param regex
Sep 25, 2013
8271d0f
added pattern checking before decoding types in the exec()
Sep 25, 2013
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion sample/states.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ angular.module('uiRouterSample')
// Use $stateProvider to configure your states.
$stateProvider

/////////////////////
// Parameter Types //
/////////////////////
.type("date", {
equals: function (typeObj, otherObj) {
return typeObj.toISOString() === otherObj.toISOString();
},
decode: function (typeObj) {
return typeObj.toISOString();
},
encode: function (value) {
return new Date(value);
}
})

//////////
// Home //
//////////
Expand Down Expand Up @@ -139,7 +154,7 @@ angular.module('uiRouterSample')
// So its url will end up being '/contacts/{contactId:[0-9]{1,8}}'. When the
// url becomes something like '/contacts/42' then this state becomes active
// and the $stateParams object becomes { contactId: 42 }.
url: '/{contactId:[0-9]{1,4}}',
url: '/{contactId:integer}',

// If there is more than a single ui-view in the parent template, or you would
// like to target a ui-view from even higher up the state tree, you can use the
Expand Down
10 changes: 9 additions & 1 deletion src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
$delegates: {}
};

// Create a proxy through to the Type registration on the UrlMatcherFactory
this.isTypeRegistered = $urlMatcherFactory.isTypeRegistered;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be better to make type() a getter if you only pass a name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but instead, I've just dropped it. I only needed it for test purposes, and the exposed type function has to return the state object so that the config can be chained.

this.type = function (name, handler) {
$urlMatcherFactory.type(name, handler);
return this;
};

function isRelative(stateName) {
return stateName.indexOf(".") === 0 || stateName.indexOf("^") === 0;
}
Expand Down Expand Up @@ -456,7 +463,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $

forEach(keys, function (name) {
var value = values[name];
normalized[name] = (value != null) ? String(value) : null;
//normalized[name] = (value != null) ? String(value) : null;
normalized[name] = (value != null) ? value : null;
});
return normalized;
}
Expand Down
84 changes: 78 additions & 6 deletions src/urlMatcherFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ function UrlMatcher(pattern) {
var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
names = {}, compiled = '^', last = 0, m,
segments = this.segments = [],
params = this.params = [];
params = this.params = [],
typeMap = this.typeMap = {};

function addParameter(id) {
if (!/^\w+(-+\w+)*$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'");
Expand All @@ -80,6 +81,10 @@ function UrlMatcher(pattern) {
while ((m = placeholder.exec(pattern))) {
id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null
regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*');
if (isDefined(this.types[regexp])) {
this.typeMap[id] = regexp;
regexp = '[^/]*';
}
segment = pattern.substring(last, m.index);
if (segment.indexOf('?') >= 0) break; // we're into the search part
compiled += quoteRegExp(segment) + '(' + regexp + ')';
Expand Down Expand Up @@ -154,7 +159,10 @@ UrlMatcher.prototype.toString = function () {
* @return {Object} The captured parameter values.
*/
UrlMatcher.prototype.exec = function (path, searchParams) {
var m = this.regexp.exec(path);
var m = this.regexp.exec(path),
types = this.types,
typeMap = this.typeMap;

if (!m) return null;

var params = this.params, nTotal = params.length,
Expand All @@ -166,7 +174,17 @@ UrlMatcher.prototype.exec = function (path, searchParams) {
for (i=0; i<nPath; i++) values[params[i]] = m[i+1];
for (/**/; i<nTotal; i++) values[params[i]] = searchParams[params[i]];

return values;
var decodedValues = {};
forEach(values, function (value, key) {
if (isDefined(typeMap[key])) {
decodedValues[key] = types[typeMap[key]].decode(value);
}
else {
decodedValues[key] = value;
}
});

return decodedValues;
};

/**
Expand All @@ -193,20 +211,33 @@ UrlMatcher.prototype.parameters = function () {
* @return {string} the formatted URL (path and optionally search part).
*/
UrlMatcher.prototype.format = function (values) {
var segments = this.segments, params = this.params;
var segments = this.segments,
params = this.params,
types = this.types,
typeMap = this.typeMap;
if (!values) return segments.join('');

var nPath = segments.length-1, nTotal = params.length,
result = segments[0], i, search, value;

var encodedValues = {};
forEach(values, function (value, key) {
if (isDefined(typeMap[key])) {
encodedValues[key] = types[typeMap[key]].encode(value);
}
else {
encodedValues[key] = value;
}
});

for (i=0; i<nPath; i++) {
value = values[params[i]];
value = encodedValues[params[i]];
// TODO: Maybe we should throw on null here? It's not really good style to use '' and null interchangeabley
if (value != null) result += encodeURIComponent(value);
result += segments[i+1];
}
for (/**/; i<nTotal; i++) {
value = values[params[i]];
value = encodedValues[params[i]];
if (value != null) {
result += (search ? '&' : '?') + params[i] + '=' + encodeURIComponent(value);
search = true;
Expand All @@ -216,6 +247,43 @@ UrlMatcher.prototype.format = function (values) {
return result;
};

UrlMatcher.prototype.types = {
'boolean': {
equals: function (typeObj, otherObj) {
return typeObj === otherObj;
},
encode: function (typeObj) {
return typeObj.toString();
},
decode: function (value) {
if (value && value.toLowerCase() === 'true') return true;
if (value && value.toLowerCase() === 'false') return false;
return undefined;
}
},
'integer': {
equals: function (typeObj, otherObj) {
return typeObj === otherObj;
},
encode: function (typeObj) {
return typeObj.toString();
},
decode: function (value) {
return parseInt(value, 10);
}
}
};

/**
TODO
*/
UrlMatcher.prototype.type = function (name, handler) {
if (!isString(name) || !isObject(handler) || !isFunction(handler.equals) || !isFunction(handler.decode) || !isFunction(handler.encode)) {
throw new Error("Invalid type '" + name + "'");
}
UrlMatcher.prototype.types[name] = handler;
};

/**
* Service. Factory for {@link UrlMatcher} instances. The factory is also available to providers
* under the name `$urlMatcherFactoryProvider`.
Expand Down Expand Up @@ -247,6 +315,10 @@ function $UrlMatcherFactory() {
return isObject(o) && isFunction(o.exec) && isFunction(o.format) && isFunction(o.concat);
};

this.type = function (name, handler) {
return UrlMatcher.prototype.type(name, handler);
};

this.$get = function () {
return this;
};
Expand Down
12 changes: 6 additions & 6 deletions test/stateDirectivesSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('uiStateRef', function() {
$q.flush();

expect($state.current.name).toEqual('contacts.item.detail');
expect($stateParams).toEqual({ id: "5" });
expect($stateParams).toEqual({ id: 5 });
}));

it('should not transition states when ctrl-clicked', inject(function($state, $stateParams, $document, $q) {
Expand All @@ -106,7 +106,7 @@ describe('uiStateRef', function() {

$q.flush();
expect($state.current.name).toEqual('');
expect($stateParams).toEqual({ id: "5" });
expect($stateParams).toEqual({ id: 5 });
}));

it('should not transition states when meta-clicked', inject(function($state, $stateParams, $document, $q) {
Expand All @@ -116,7 +116,7 @@ describe('uiStateRef', function() {
$q.flush();

expect($state.current.name).toEqual('');
expect($stateParams).toEqual({ id: "5" });
expect($stateParams).toEqual({ id: 5 });
}));

it('should not transition states when shift-clicked', inject(function($state, $stateParams, $document, $q) {
Expand All @@ -126,7 +126,7 @@ describe('uiStateRef', function() {
$q.flush();

expect($state.current.name).toEqual('');
expect($stateParams).toEqual({ id: "5" });
expect($stateParams).toEqual({ id: 5 });
}));

it('should not transition states when middle-clicked', inject(function($state, $stateParams, $document, $q) {
Expand All @@ -136,7 +136,7 @@ describe('uiStateRef', function() {
$q.flush();

expect($state.current.name).toEqual('');
expect($stateParams).toEqual({ id: "5" });
expect($stateParams).toEqual({ id: 5 });
}));
});

Expand Down Expand Up @@ -176,7 +176,7 @@ describe('uiStateRef', function() {
$q.flush();

expect($state.$current.name).toBe("contacts.item.detail");
expect($state.params).toEqual({ id: '5' });
expect($state.params).toEqual({ id: 5 });
}));

it('should resolve states from parent uiView', inject(function ($state, $stateParams, $q) {
Expand Down
10 changes: 5 additions & 5 deletions test/stateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,13 @@ describe('state', function () {
expect(from).toBe(E);
expect(fromParams).toEqual({ i: 'iii' });
expect(to).toBe(D);
expect(toParams).toEqual({ x: '1', y: '2' });
expect(toParams).toEqual({ x: 1, y: 2 });

expect($state.current).toBe(from); // $state not updated yet
expect($state.params).toEqual(fromParams);
called = true;
});
$state.transitionTo(D, { x: '1', y: '2' });
$state.transitionTo(D, { x: 1, y: 2 });
$q.flush();
expect(called).toBeTruthy();
expect($state.current).toBe(D);
Expand Down Expand Up @@ -205,7 +205,7 @@ describe('state', function () {
$q.flush();
expect(called).toBeTruthy();
expect($state.current.name).toEqual('DDD');
expect($state.params).toEqual({ x: '1', y: '2', z: '3', w: '4' });
expect($state.params).toEqual({ x: 1, y: 2, z: 3, w: 4 });
}));

it('can defer a state transition in $stateNotFound', inject(function ($state, $q, $rootScope) {
Expand All @@ -222,7 +222,7 @@ describe('state', function () {
$q.flush();
expect(called).toBeTruthy();
expect($state.current.name).toEqual('AA');
expect($state.params).toEqual({ a: '1' });
expect($state.params).toEqual({ a: 1 });
}));

it('can defer and supersede a state transition in $stateNotFound', inject(function ($state, $q, $rootScope) {
Expand Down Expand Up @@ -374,7 +374,7 @@ describe('state', function () {
$q.flush();

expect($state.$current.name).toBe('about.person.item');
expect($stateParams).toEqual({ person: 'bob', id: '5' });
expect($stateParams).toEqual({ person: 'bob', id: 5 });

$state.go('^.^.sidebar');
$q.flush();
Expand Down
40 changes: 40 additions & 0 deletions test/urlMatcherFactorySpec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
describe("UrlMatcher", function () {
beforeEach(function () {
UrlMatcher.prototype.type("test", {
equals: function (typeObj, otherObj) {
return typeObj === otherObj;
},
encode: function (typeObj) {
return "encoded";
},
decode: function (value) {
return "decoded";
}
})
});

it("matches static URLs", function () {
expect(new UrlMatcher('/hello/world').exec('/hello/world')).toEqual({});
});
Expand All @@ -20,6 +34,12 @@ describe("UrlMatcher", function () {
expect(params).toContain('to');
});

it("parses parameters with types", function () {
var matcher = new UrlMatcher('/users/{id:type}');
var params = matcher.parameters();
expect(params).toContain('id');
});

it("handles proper snake case parameter names", function(){
var matcher = new UrlMatcher('/users/?from&to&snake-case&snake-case-triple');
var params = matcher.parameters();
Expand Down Expand Up @@ -47,6 +67,13 @@ describe("UrlMatcher", function () {
.toEqual({ id:'123', type:'', repeat:'0' });
});

it(".exec() captures typed parameter values", function () {
expect(
new UrlMatcher('/users/{id:test}')
.exec('/users/22', {}))
.toEqual({ id: 'decoded' });
});

it(".exec() captures catch-all parameters", function () {
var m = new UrlMatcher('/document/*path');
expect(m.exec('/document/a/b/c', {})).toEqual({ path: 'a/b/c' });
Expand Down Expand Up @@ -97,6 +124,10 @@ describe("UrlMatcher", function () {
expect(new UrlMatcher('/users/:id').format({ id:'100%'})).toEqual('/users/100%25');
});

it(".format() encode typed URL parameters", function () {
expect(new UrlMatcher('/users/{id:test}').format({ id:55})).toEqual('/users/encoded');
});

it(".concat() concatenates matchers", function () {
var matcher = new UrlMatcher('/users/:id/details/{type}?from').concat('/{repeat:[0-9]+}?to');
var params = matcher.parameters();
Expand Down Expand Up @@ -132,4 +163,13 @@ describe("urlMatcherFactory", function () {
it("recognizes matchers", function () {
expect($umf.isMatcher(new UrlMatcher('/'))).toBe(true);
});

it("registers types", function () {
$umf.type("test", {
equals: function (typeObj, otherObj) {},
decode: function (typeObj) {},
encode: function (value) {}
});
expect($umf.compile('/').types["test"]).toBeDefined();
});
});