Skip to content

Commit

Permalink
feat($urlMatcherFactory): Made a Params and ParamSet class
Browse files Browse the repository at this point in the history
- make state.ownParams and params use Param type
  • Loading branch information
christopherthielen committed Oct 20, 2014
1 parent 753efee commit 0cc1e6c
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 70 deletions.
14 changes: 14 additions & 0 deletions src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ function objectKeys(object) {
return result;
}

/**
* like objectKeys, but includes keys from prototype chain.
* @param object the object whose prototypal keys will be returned
* @param ignoreKeys an array of keys to ignore
*/
function protoKeys(object, ignoreKeys) {
var result = [];
for (var key in object) {
if (!ignoreKeys || ignoreKeys.indexOf(key) === -1)
result.push(key);
}
return result;
}

/**
* IE8-safe wrapper for `Array.prototype.indexOf()`.
*
Expand Down
45 changes: 15 additions & 30 deletions src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,19 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
return state.url ? state : (state.parent ? state.parent.navigable : null);
},

// Own parameters for this state. state.url.params is already built at this point. Create and add non-url params
ownParams: function(state) {
var params = state.url && state.url.params || new $$UrlMatcherFactoryProvider.ParamSet();
forEach(state.params || {}, function(config, id) {
if (!params[id]) params[id] = new $$UrlMatcherFactoryProvider.Param(id, null, config);
});
return params;
},

// Derive parameters for this state and ensure they're a super-set of parent's parameters
params: function(state) {
if (!state.params) {
return state.url ? state.url.params : state.parent.params;
}
return state.params;
var parentParams = state.parent && state.parent.params || new $$UrlMatcherFactoryProvider.ParamSet();
return inherit(parentParams, state.ownParams);
},

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

ownParams: function(state) {
state.params = state.params || {};

if (!state.parent) {
return objectKeys(state.params);
}
var paramNames = {}; forEach(state.params, function (v, k) { paramNames[k] = true; });

forEach(state.parent.params, function (v, k) {
if (!paramNames[k]) {
throw new Error("Missing required parameter '" + k + "' in state '" + state.name + "'");
}
paramNames[k] = false;
});
var ownParams = [];

forEach(paramNames, function (own, p) {
if (own) ownParams.push(p);
});
return ownParams;
},

// Keep a full path from the root down to this state as this is needed for state activation.
path: function(state) {
return state.parent ? state.parent.path.concat(state) : []; // exclude root from path
Expand Down Expand Up @@ -801,7 +786,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
var keep = 0, state = toPath[keep], locals = root.locals, toLocals = [];

if (!options.reload) {
while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams)) {
while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams.$$keys())) {
locals = toLocals[keep] = state.locals;
keep++;
state = toPath[keep];
Expand All @@ -820,7 +805,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
}

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

// Broadcast start event and cancel the transition if requested
if (options.notify) {
Expand Down Expand Up @@ -1133,7 +1118,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
if (!nav || nav.url === undefined || nav.url === null) {
return null;
}
return $urlRouter.href(nav.url, filterByKeys(objectKeys(state.params), params || {}), {
return $urlRouter.href(nav.url, filterByKeys(state.params.$$keys(), params || {}), {
absolute: options.absolute
});
};
Expand Down Expand Up @@ -1162,7 +1147,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
// necessary. In addition to being available to the controller and onEnter/onExit callbacks,
// we also need $stateParams to be available for any $injector calls we make during the
// dependency resolution process.
var $stateParams = (paramsAreFiltered) ? params : filterByKeys(objectKeys(state.params), params);
var $stateParams = (paramsAreFiltered) ? params : filterByKeys(state.params.$$keys(), params);
var locals = { $stateParams: $stateParams };

// Resolve 'global' dependencies for the state, i.e. those not specific to a view.
Expand Down
143 changes: 103 additions & 40 deletions src/urlMatcherFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
* @returns {Object} New `UrlMatcher` object
*/
function UrlMatcher(pattern, config) {
config = angular.isObject(config) ? config : {};
config = extend({ params: {} }, isObject(config) ? config : {});

// Find all placeholders and create a compiled pattern, using either classic or curly syntax:
// '*' name
Expand All @@ -78,21 +78,13 @@ function UrlMatcher(pattern, config) {
var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
compiled = '^', last = 0, m,
segments = this.segments = [],
params = this.params = {};

/**
* [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the
* default value, which may be the result of an injectable function.
*/
function $value(value) {
/*jshint validthis: true */
return isDefined(value) ? this.type.decode(value) : $UrlMatcherFactory.$$getDefaultValue(this);
}
params = this.params = new $$UrlMatcherFactoryProvider.ParamSet();

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

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

function paramConfig(param) {
if (!config.params || !config.params[param]) return {};
var cfg = config.params[param];
return isObject(cfg) ? cfg : { value: cfg };
}

this.source = pattern;

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

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

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

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

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

var params = this.parameters(), nTotal = params.length,
var paramNames = this.parameters(), nTotal = paramNames.length,
nPath = this.segments.length - 1,
values = {}, i, cfg, param;
values = {}, i, cfg, paramName;

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

for (i = 0; i < nPath; i++) {
param = params[i];
cfg = this.params[param];
values[param] = cfg.$value(m[i + 1]);
paramName = paramNames[i];
values[paramName] = this.params[paramName].value(m[i + 1]);
}
for (/**/; i < nTotal; i++) {
param = params[i];
cfg = this.params[param];
values[param] = cfg.$value(searchParams[param]);
paramName = paramNames[i];
values[paramName] = this.params[paramName].value(searchParams[paramName]);
}

return values;
Expand Down Expand Up @@ -265,15 +249,7 @@ UrlMatcher.prototype.parameters = function (param) {
* @returns {boolean} Returns `true` if `params` validates, otherwise `false`.
*/
UrlMatcher.prototype.validates = function (params) {
var result = true, isOptional, cfg, self = this;

forEach(params, function(val, key) {
if (!self.params[key]) return;
cfg = self.params[key];
isOptional = !val && isDefined(cfg.value);
result = result && (isOptional || cfg.type.is(val));
});
return result;
return this.params.$$validates(params);
};

/**
Expand Down Expand Up @@ -717,7 +693,94 @@ function $UrlMatcherFactory() {
UrlMatcher.prototype.$types[type.name] = def;
});
}

this.Param = function Param(id, type, config) {
var self = this;
var defaultValueConfig = getDefaultValueConfig(config);
config = config || {};
type = getType(config, type);

function getDefaultValueConfig(config) {
var keys = isObject(config) ? objectKeys(config) : [];
var isShorthand = keys.indexOf("value") === -1 && keys.indexOf("type") === -1;
var configValue = isShorthand ? config : config.value;
return {
fn: isInjectable(configValue) ? configValue : function () { return configValue; },
value: configValue
};
}

function getType(config, urlType) {
if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations.");
if (urlType && !config.type) return urlType;
return config.type instanceof Type ? config.type : new Type(config.type || {});
}

/**
* [Internal] Get the default value of a parameter, which may be an injectable function.
*/
function $$getDefaultValue() {
if (!injector) throw new Error("Injectable functions cannot be called at configuration time");
return injector.invoke(defaultValueConfig.fn);
}

/**
* [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the
* default value, which may be the result of an injectable function.
*/
function $value(value) {
return isDefined(value) ? self.type.decode(value) : $$getDefaultValue();
}

extend(this, {
id: id,
type: type,
config: config,
dynamic: undefined,
isOptional: defaultValueConfig.value !== undefined,
value: $value
});
};

function ParamSet(params) {
extend(this, params || {});
}

ParamSet.prototype = {
$$keys: function () {
return protoKeys(this, ["$$keys", "$$values", "$$equals", "$$validates"]);
},
$$values: function(paramValues) {
var values = {}, self = this;
forEach(self.$$keys(), function(key) {
values[key] = self[key].value(paramValues && paramValues[key]);
});
return values;
},
$$equals: function(paramValues1, paramValues2) {
var equal = true; self = this;
forEach(self.$$keys(), function(key) {
var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key];
if (!self[key].type.equals(left, right)) equal = false;
});
return equal;
},
$$validates: function $$validate(paramValues) {
var result = true, isOptional, val, param, self = this;

forEach(this.$$keys(), function(key) {
param = self[key];
val = paramValues[key];
isOptional = !val && param.isOptional;
result = result && (isOptional || param.type.is(val));
});
return result;
}
};

this.ParamSet = ParamSet;
}

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

0 comments on commit 0cc1e6c

Please sign in to comment.