Skip to content

Commit 0cc1e6c

Browse files
feat($urlMatcherFactory): Made a Params and ParamSet class
- make state.ownParams and params use Param type
1 parent 753efee commit 0cc1e6c

File tree

3 files changed

+132
-70
lines changed

3 files changed

+132
-70
lines changed

src/common.js

+14
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ function objectKeys(object) {
6161
return result;
6262
}
6363

64+
/**
65+
* like objectKeys, but includes keys from prototype chain.
66+
* @param object the object whose prototypal keys will be returned
67+
* @param ignoreKeys an array of keys to ignore
68+
*/
69+
function protoKeys(object, ignoreKeys) {
70+
var result = [];
71+
for (var key in object) {
72+
if (!ignoreKeys || ignoreKeys.indexOf(key) === -1)
73+
result.push(key);
74+
}
75+
return result;
76+
}
77+
6478
/**
6579
* IE8-safe wrapper for `Array.prototype.indexOf()`.
6680
*

src/state.js

+15-30
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,19 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
6464
return state.url ? state : (state.parent ? state.parent.navigable : null);
6565
},
6666

67+
// Own parameters for this state. state.url.params is already built at this point. Create and add non-url params
68+
ownParams: function(state) {
69+
var params = state.url && state.url.params || new $$UrlMatcherFactoryProvider.ParamSet();
70+
forEach(state.params || {}, function(config, id) {
71+
if (!params[id]) params[id] = new $$UrlMatcherFactoryProvider.Param(id, null, config);
72+
});
73+
return params;
74+
},
75+
6776
// Derive parameters for this state and ensure they're a super-set of parent's parameters
6877
params: function(state) {
69-
if (!state.params) {
70-
return state.url ? state.url.params : state.parent.params;
71-
}
72-
return state.params;
78+
var parentParams = state.parent && state.parent.params || new $$UrlMatcherFactoryProvider.ParamSet();
79+
return inherit(parentParams, state.ownParams);
7380
},
7481

7582
// If there is no explicit multi-view configuration, make one up so we don't have
@@ -87,28 +94,6 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
8794
return views;
8895
},
8996

90-
ownParams: function(state) {
91-
state.params = state.params || {};
92-
93-
if (!state.parent) {
94-
return objectKeys(state.params);
95-
}
96-
var paramNames = {}; forEach(state.params, function (v, k) { paramNames[k] = true; });
97-
98-
forEach(state.parent.params, function (v, k) {
99-
if (!paramNames[k]) {
100-
throw new Error("Missing required parameter '" + k + "' in state '" + state.name + "'");
101-
}
102-
paramNames[k] = false;
103-
});
104-
var ownParams = [];
105-
106-
forEach(paramNames, function (own, p) {
107-
if (own) ownParams.push(p);
108-
});
109-
return ownParams;
110-
},
111-
11297
// Keep a full path from the root down to this state as this is needed for state activation.
11398
path: function(state) {
11499
return state.parent ? state.parent.path.concat(state) : []; // exclude root from path
@@ -801,7 +786,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
801786
var keep = 0, state = toPath[keep], locals = root.locals, toLocals = [];
802787

803788
if (!options.reload) {
804-
while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams)) {
789+
while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams.$$keys())) {
805790
locals = toLocals[keep] = state.locals;
806791
keep++;
807792
state = toPath[keep];
@@ -820,7 +805,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
820805
}
821806

822807
// Filter parameters before we pass them to event handlers etc.
823-
toParams = filterByKeys(objectKeys(to.params), toParams || {});
808+
toParams = filterByKeys(to.params.$$keys(), toParams || {});
824809

825810
// Broadcast start event and cancel the transition if requested
826811
if (options.notify) {
@@ -1133,7 +1118,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
11331118
if (!nav || nav.url === undefined || nav.url === null) {
11341119
return null;
11351120
}
1136-
return $urlRouter.href(nav.url, filterByKeys(objectKeys(state.params), params || {}), {
1121+
return $urlRouter.href(nav.url, filterByKeys(state.params.$$keys(), params || {}), {
11371122
absolute: options.absolute
11381123
});
11391124
};
@@ -1162,7 +1147,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
11621147
// necessary. In addition to being available to the controller and onEnter/onExit callbacks,
11631148
// we also need $stateParams to be available for any $injector calls we make during the
11641149
// dependency resolution process.
1165-
var $stateParams = (paramsAreFiltered) ? params : filterByKeys(objectKeys(state.params), params);
1150+
var $stateParams = (paramsAreFiltered) ? params : filterByKeys(state.params.$$keys(), params);
11661151
var locals = { $stateParams: $stateParams };
11671152

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

src/urlMatcherFactory.js

+103-40
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
* @returns {Object} New `UrlMatcher` object
6161
*/
6262
function UrlMatcher(pattern, config) {
63-
config = angular.isObject(config) ? config : {};
63+
config = extend({ params: {} }, isObject(config) ? config : {});
6464

6565
// Find all placeholders and create a compiled pattern, using either classic or curly syntax:
6666
// '*' name
@@ -78,21 +78,13 @@ function UrlMatcher(pattern, config) {
7878
var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
7979
compiled = '^', last = 0, m,
8080
segments = this.segments = [],
81-
params = this.params = {};
82-
83-
/**
84-
* [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the
85-
* default value, which may be the result of an injectable function.
86-
*/
87-
function $value(value) {
88-
/*jshint validthis: true */
89-
return isDefined(value) ? this.type.decode(value) : $UrlMatcherFactory.$$getDefaultValue(this);
90-
}
81+
params = this.params = new $$UrlMatcherFactoryProvider.ParamSet();
9182

9283
function addParameter(id, type, config) {
9384
if (!/^\w+(-+\w+)*$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'");
9485
if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'");
95-
params[id] = extend({ type: type || new Type(), $value: $value }, config);
86+
params[id] = new $$UrlMatcherFactoryProvider.Param(id, type, config);
87+
return params[id];
9688
}
9789

9890
function quoteRegExp(string, pattern, isOptional) {
@@ -102,12 +94,6 @@ function UrlMatcher(pattern, config) {
10294
return result + flag + '(' + pattern + ')' + flag;
10395
}
10496

105-
function paramConfig(param) {
106-
if (!config.params || !config.params[param]) return {};
107-
var cfg = config.params[param];
108-
return isObject(cfg) ? cfg : { value: cfg };
109-
}
110-
11197
this.source = pattern;
11298

11399
// Split into static segments separated by path parameter placeholders.
@@ -119,12 +105,12 @@ function UrlMatcher(pattern, config) {
119105
regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*');
120106
segment = pattern.substring(last, m.index);
121107
type = this.$types[regexp] || new Type({ pattern: new RegExp(regexp) });
122-
cfg = paramConfig(id);
108+
cfg = config.params[id];
123109

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

126-
compiled += quoteRegExp(segment, type.$subPattern(), isDefined(cfg.value));
127-
addParameter(id, type, cfg);
112+
var param = addParameter(id, type, cfg);
113+
compiled += quoteRegExp(segment, type.$subPattern(), param.isOptional);
128114
segments.push(segment);
129115
last = placeholder.lastIndex;
130116
}
@@ -140,7 +126,7 @@ function UrlMatcher(pattern, config) {
140126

141127
// Allow parameters to be separated by '?' as well as '&' to make concat() easier
142128
forEach(search.substring(1).split(/[&?]/), function(key) {
143-
addParameter(key, null, paramConfig(key));
129+
addParameter(key, null, config.params[key]);
144130
});
145131
} else {
146132
this.sourcePath = pattern;
@@ -180,7 +166,7 @@ UrlMatcher.prototype.concat = function (pattern, config) {
180166
// Because order of search parameters is irrelevant, we can add our own search
181167
// parameters to the end of the new pattern. Parse the new pattern by itself
182168
// and then join the bits together, but it's much easier to do this on a string level.
183-
return new $$UrlMatcherFactoryProvider.compile(this.sourcePath + pattern + this.sourceSearch, config);
169+
return $$UrlMatcherFactoryProvider.compile(this.sourcePath + pattern + this.sourceSearch, config);
184170
};
185171

186172
UrlMatcher.prototype.toString = function () {
@@ -216,21 +202,19 @@ UrlMatcher.prototype.exec = function (path, searchParams) {
216202
if (!m) return null;
217203
searchParams = searchParams || {};
218204

219-
var params = this.parameters(), nTotal = params.length,
205+
var paramNames = this.parameters(), nTotal = paramNames.length,
220206
nPath = this.segments.length - 1,
221-
values = {}, i, cfg, param;
207+
values = {}, i, cfg, paramName;
222208

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

225211
for (i = 0; i < nPath; i++) {
226-
param = params[i];
227-
cfg = this.params[param];
228-
values[param] = cfg.$value(m[i + 1]);
212+
paramName = paramNames[i];
213+
values[paramName] = this.params[paramName].value(m[i + 1]);
229214
}
230215
for (/**/; i < nTotal; i++) {
231-
param = params[i];
232-
cfg = this.params[param];
233-
values[param] = cfg.$value(searchParams[param]);
216+
paramName = paramNames[i];
217+
values[paramName] = this.params[paramName].value(searchParams[paramName]);
234218
}
235219

236220
return values;
@@ -265,15 +249,7 @@ UrlMatcher.prototype.parameters = function (param) {
265249
* @returns {boolean} Returns `true` if `params` validates, otherwise `false`.
266250
*/
267251
UrlMatcher.prototype.validates = function (params) {
268-
var result = true, isOptional, cfg, self = this;
269-
270-
forEach(params, function(val, key) {
271-
if (!self.params[key]) return;
272-
cfg = self.params[key];
273-
isOptional = !val && isDefined(cfg.value);
274-
result = result && (isOptional || cfg.type.is(val));
275-
});
276-
return result;
252+
return this.params.$$validates(params);
277253
};
278254

279255
/**
@@ -717,7 +693,94 @@ function $UrlMatcherFactory() {
717693
UrlMatcher.prototype.$types[type.name] = def;
718694
});
719695
}
696+
697+
this.Param = function Param(id, type, config) {
698+
var self = this;
699+
var defaultValueConfig = getDefaultValueConfig(config);
700+
config = config || {};
701+
type = getType(config, type);
702+
703+
function getDefaultValueConfig(config) {
704+
var keys = isObject(config) ? objectKeys(config) : [];
705+
var isShorthand = keys.indexOf("value") === -1 && keys.indexOf("type") === -1;
706+
var configValue = isShorthand ? config : config.value;
707+
return {
708+
fn: isInjectable(configValue) ? configValue : function () { return configValue; },
709+
value: configValue
710+
};
711+
}
712+
713+
function getType(config, urlType) {
714+
if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations.");
715+
if (urlType && !config.type) return urlType;
716+
return config.type instanceof Type ? config.type : new Type(config.type || {});
717+
}
718+
719+
/**
720+
* [Internal] Get the default value of a parameter, which may be an injectable function.
721+
*/
722+
function $$getDefaultValue() {
723+
if (!injector) throw new Error("Injectable functions cannot be called at configuration time");
724+
return injector.invoke(defaultValueConfig.fn);
725+
}
726+
727+
/**
728+
* [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the
729+
* default value, which may be the result of an injectable function.
730+
*/
731+
function $value(value) {
732+
return isDefined(value) ? self.type.decode(value) : $$getDefaultValue();
733+
}
734+
735+
extend(this, {
736+
id: id,
737+
type: type,
738+
config: config,
739+
dynamic: undefined,
740+
isOptional: defaultValueConfig.value !== undefined,
741+
value: $value
742+
});
743+
};
744+
745+
function ParamSet(params) {
746+
extend(this, params || {});
747+
}
748+
749+
ParamSet.prototype = {
750+
$$keys: function () {
751+
return protoKeys(this, ["$$keys", "$$values", "$$equals", "$$validates"]);
752+
},
753+
$$values: function(paramValues) {
754+
var values = {}, self = this;
755+
forEach(self.$$keys(), function(key) {
756+
values[key] = self[key].value(paramValues && paramValues[key]);
757+
});
758+
return values;
759+
},
760+
$$equals: function(paramValues1, paramValues2) {
761+
var equal = true; self = this;
762+
forEach(self.$$keys(), function(key) {
763+
var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key];
764+
if (!self[key].type.equals(left, right)) equal = false;
765+
});
766+
return equal;
767+
},
768+
$$validates: function $$validate(paramValues) {
769+
var result = true, isOptional, val, param, self = this;
770+
771+
forEach(this.$$keys(), function(key) {
772+
param = self[key];
773+
val = paramValues[key];
774+
isOptional = !val && param.isOptional;
775+
result = result && (isOptional || param.type.is(val));
776+
});
777+
return result;
778+
}
779+
};
780+
781+
this.ParamSet = ParamSet;
720782
}
721783

722784
// Register as a provider so it's available to other providers
723785
angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory);
786+
angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]);

0 commit comments

Comments
 (0)