/**
* State-based routing for AngularJS
- * @version v0.2.18
+ * @version v0.2.15
* @link http://angular-ui.github.com/
* @license MIT License, http://www.opensource.org/licenses/MIT
*/
isArray = angular.isArray,
forEach = angular.forEach,
extend = angular.extend,
- copy = angular.copy,
- toJson = angular.toJson;
+ copy = angular.copy;
function inherit(parent, extra) {
return extend(new (extend(function() {}, { prototype: parent }))(), extra);
var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = [];
for (var i in parents) {
- if (!parents[i] || !parents[i].params) continue;
+ if (!parents[i].params) continue;
parentParams = objectKeys(parents[i].params);
if (!parentParams.length) continue;
* propagated immediately. Once the `$resolve` promise has been rejected, no
* further invocables will be called.
*
- * Cyclic dependencies between invocables are not permitted and will cause `$resolve`
+ * Cyclic dependencies between invocables are not permitted and will caues `$resolve`
* to throw an error. As a special case, an injectable can depend on a parameter
* with the same name as the injectable, which will be fulfilled from the `parent`
* injectable of the same name. This allows inherited values to be decorated.
// The regular expression is somewhat complicated due to the need to allow curly braces
// inside the regular expression. The placeholder regexp breaks down as follows:
// ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case)
- // \{([\w\[\]]+)(?:\:\s*( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case
+ // \{([\w\[\]]+)(?:\:( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case
// (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either
// [^{}\\]+ - anything other than curly braces or backslash
// \\. - a backslash escape
// \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms
- var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
- searchPlaceholder = /([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
+ var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
+ searchPlaceholder = /([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
compiled = '^', last = 0, m,
segments = this.segments = [],
parentParams = parentMatcher ? parentMatcher.params : {},
function addParameter(id, type, config, location) {
paramNames.push(id);
if (parentParams[id]) return parentParams[id];
- if (!/^\w+([-.]+\w+)*(?:\[\])?$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'");
+ 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] = new $$UMFP.Param(id, type, config, location);
return params[id];
if (!pattern) return result;
switch(squash) {
case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break;
- case true:
- result = result.replace(/\/$/, '');
- surroundPattern = ['(?:\/(', ')|\/)?'];
- break;
+ case true: surroundPattern = ['?(', ')?']; break;
default: surroundPattern = ['(' + squash + "|", ')?']; break;
}
return result + surroundPattern[0] + pattern + surroundPattern[1];
cfg = config.params[id];
segment = pattern.substring(last, m.index);
regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null);
-
- if (regexp) {
- type = $$UMFP.type(regexp) || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) });
- }
-
+ type = $$UMFP.type(regexp || "string") || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) });
return {
id: id, regexp: regexp, segment: segment, type: type, cfg: cfg
};
return map(allReversed, unquoteDashes).reverse();
}
- var param, paramVal;
for (i = 0; i < nPath; i++) {
paramName = paramNames[i];
- param = this.params[paramName];
- paramVal = m[i+1];
+ var param = this.params[paramName];
+ var paramVal = m[i+1];
// if the param value matches a pre-replace pair, replace the value before decoding.
- for (j = 0; j < param.replace.length; j++) {
+ for (j = 0; j < param.replace; j++) {
if (param.replace[j].from === paramVal) paramVal = param.replace[j].to;
}
if (paramVal && param.array === true) paramVal = decodePathArray(paramVal);
- if (isDefined(paramVal)) paramVal = param.type.decode(paramVal);
values[paramName] = param.value(paramVal);
}
for (/**/; i < nTotal; i++) {
paramName = paramNames[i];
values[paramName] = this.params[paramName].value(searchParams[paramName]);
- param = this.params[paramName];
- paramVal = searchParams[paramName];
- for (j = 0; j < param.replace.length; j++) {
- if (param.replace[j].from === paramVal) paramVal = param.replace[j].to;
- }
- if (isDefined(paramVal)) paramVal = param.type.decode(paramVal);
- values[paramName] = param.value(paramVal);
}
return values;
/**
* @ngdoc function
- * @name ui.router.util.type:UrlMatcher#validates
+ * @name ui.router.util.type:UrlMatcher#validate
* @methodOf ui.router.util.type:UrlMatcher
*
* @description
if (isPathParam) {
var nextSegment = segments[i + 1];
- var isFinalPathParam = i + 1 === nPath;
-
if (squash === false) {
if (encoded != null) {
if (isArray(encoded)) {
} else if (isString(squash)) {
result += squash + nextSegment;
}
-
- if (isFinalPathParam && param.squash === true && result.slice(-1) === '/') result = result.slice(0, -1);
} else {
if (encoded == null || (isDefaultValue && squash !== false)) continue;
if (!isArray(encoded)) encoded = [ encoded ];
- if (encoded.length === 0) continue;
encoded = map(encoded, encodeURIComponent).join('&' + name + '=');
result += (search ? '&' : '?') + (name + '=' + encoded);
search = true;
// Wraps type (.is/.encode/.decode) functions to operate on each value of an array
function arrayHandler(callback, allTruthyMode) {
return function handleArray(val) {
- if (isArray(val) && val.length === 0) return val;
val = arrayWrap(val);
var result = map(val, callback);
if (allTruthyMode === true)
var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false;
- // Use tildes to pre-encode slashes.
- // If the slashes are simply URLEncoded, the browser can choose to pre-decode them,
- // and bidirectional encoding/decoding fails.
- // Tilde was chosen because it's not a RFC 3986 section 2.2 Reserved Character
- function valToString(val) { return val != null ? val.toString().replace(/~/g, "~~").replace(/\//g, "~2F") : val; }
- function valFromString(val) { return val != null ? val.toString().replace(/~2F/g, "/").replace(/~~/g, "~") : val; }
+ function valToString(val) { return val != null ? val.toString().replace(/\//g, "%2F") : val; }
+ function valFromString(val) { return val != null ? val.toString().replace(/%2F/g, "/") : val; }
var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = {
- "string": {
+ string: {
encode: valToString,
decode: valFromString,
// TODO: in 1.0, make string .is() return false if value is undefined/null by default.
is: function(val) { return val == null || !isDefined(val) || typeof val === "string"; },
pattern: /[^/]*/
},
- "int": {
+ int: {
encode: valToString,
decode: function(val) { return parseInt(val, 10); },
is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; },
pattern: /\d+/
},
- "bool": {
+ bool: {
encode: function(val) { return val ? 1 : 0; },
decode: function(val) { return parseInt(val, 10) !== 0; },
is: function(val) { return val === true || val === false; },
pattern: /0|1/
},
- "date": {
+ date: {
encode: function (val) {
if (!this.is(val))
return undefined;
pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/,
capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/
},
- "json": {
+ json: {
encode: angular.toJson,
decode: angular.fromJson,
is: angular.isObject,
equals: angular.equals,
pattern: /[^/]*/
},
- "any": { // does not encode/decode
+ any: { // does not encode/decode
encode: angular.identity,
decode: angular.identity,
equals: angular.equals,
if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations.");
if (urlType) return urlType;
if (!config.type) return (location === "config" ? $types.any : $types.string);
-
- if (angular.isString(config.type))
- return $types[config.type];
- if (config.type instanceof Type)
- return config.type;
- return new Type(config.type);
+ return config.type instanceof Type ? config.type : new Type(config.type);
}
// array config: param name (param[]) overrides default settings. explicit config overrides param name.
* });
* </pre>
*
- * @param {function} rule Handler function that takes `$injector` and `$location`
+ * @param {object} rule Handler function that takes `$injector` and `$location`
* services as arguments. You can use them to return a valid path as a string.
*
* @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance
* });
* </pre>
*
- * @param {string|function} rule The url path you want to redirect to or a function
+ * @param {string|object} rule The url path you want to redirect to or a function
* rule that returns the url path. The function version is passed two params:
* `$injector` and `$location` services, and must return a url string.
*
* @methodOf ui.router.router.$urlRouterProvider
*
* @description
- * Registers a handler for a given url matching.
- *
- * If the handler is a string, it is
+ * Registers a handler for a given url matching. if handle is a string, it is
* treated as a redirect, and is interpolated according to the syntax of match
* (i.e. like `String.replace()` for `RegExp`, or like a `UrlMatcher` pattern otherwise).
*
* </pre>
*
* @param {string|object} what The incoming path that you want to redirect.
- * @param {string|function} handler The path you want to redirect your user to.
+ * @param {string|object} handler The path you want to redirect your user to.
*/
this.when = function (what, handler) {
var redirect, handlerIsString = isString(handler);
*
*/
this.$get = $get;
- $get.$inject = ['$location', '$rootScope', '$injector', '$browser', '$sniffer'];
- function $get( $location, $rootScope, $injector, $browser, $sniffer) {
+ $get.$inject = ['$location', '$rootScope', '$injector', '$browser'];
+ function $get( $location, $rootScope, $injector, $browser) {
var baseHref = $browser.baseHref(), location = $location.url(), lastPushedUrl;
if (angular.isObject(isHtml5)) {
isHtml5 = isHtml5.enabled;
}
-
- isHtml5 = isHtml5 && $sniffer.history;
var url = urlMatcher.format(params);
options = options || {};
// inherit 'data' from parent and override by own values (if any)
data: function(state) {
if (state.parent && state.parent.data) {
- state.data = state.self.data = inherit(state.parent.data, state.data);
+ state.data = state.self.data = extend({}, state.parent.data, state.data);
}
return state.data;
},
// Derive parameters for this state and ensure they're a super-set of parent's parameters
params: function(state) {
- var ownParams = pick(state.ownParams, state.ownParams.$$keys());
- return state.parent && state.parent.params ? extend(state.parent.params.$$new(), ownParams) : new $$UMFP.ParamSet();
+ return state.parent && state.parent.params ? extend(state.parent.params.$$new(), state.ownParams) : new $$UMFP.ParamSet();
},
// If there is no explicit multi-view configuration, make one up so we don't have
var name = state.name;
if (!isString(name) || name.indexOf('@') >= 0) throw new Error("State must have a valid name");
- if (states.hasOwnProperty(name)) throw new Error("State '" + name + "' is already defined");
+ if (states.hasOwnProperty(name)) throw new Error("State '" + name + "'' is already defined");
// Get parent name
var parentName = (name.indexOf('.') !== -1) ? name.substring(0, name.lastIndexOf('.'))
*
* Callback function for when a state is entered. Good way
* to trigger an action or dispatch an event, such as opening a dialog.
- * If minifying your scripts, make sure to explicitly annotate this function,
+ * If minifying your scripts, make sure to explictly annotate this function,
* because it won't be automatically annotated by your build tools.
*
* <pre>onEnter: function(MyService, $stateParams) {
*
* Callback function for when a state is exited. Good way to
* trigger an action or dispatch an event, such as opening a dialog.
- * If minifying your scripts, make sure to explicitly annotate this function,
+ * If minifying your scripts, make sure to explictly annotate this function,
* because it won't be automatically annotated by your build tools.
*
* <pre>onExit: function(MyService, $stateParams) {
*
* @param {object=} params A map of the parameters that will be sent to the state,
* will populate $stateParams. Any parameters that are not specified will be inherited from currently
- * defined parameters. Only parameters specified in the state definition can be overridden, new
- * parameters will be ignored. This allows, for example, going to a sibling state that shares parameters
+ * defined parameters. This allows, for example, going to a sibling state that shares parameters
* specified in a parent state. Parameter inheritance only works between common ancestor states, I.e.
* transitioning to a sibling will get you the parameters for all parents, transitioning to a child
* will get you all current parameters, etc.
* - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'),
* defines which state to be relative from.
* - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events.
- * - **`reload`** (v0.2.5) - {boolean=false|string|object}, If `true` will force transition even if no state or params
- * have changed. It will reload the resolves and views of the current state and parent states.
- * If `reload` is a string (or state object), the state object is fetched (by name, or object reference); and \
- * the transition reloads the resolves and views for that matched state, and all its children states.
+ * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params
+ * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd
+ * use this when you want to force a reload when *everything* is the same, including search params.
*
* @returns {promise} A promise representing the state of the new transition.
*
if (hash) toParams['#'] = hash;
$state.params = toParams;
copy($state.params, $stateParams);
- copy(filterByKeys(to.params.$$keys(), $stateParams), to.locals.globals.$stateParams);
if (options.location && to.navigable && to.navigable.url) {
$urlRouter.push(to.navigable.url, toParams, {
$$avoidResync: true, replace: options.location === 'replace'
// Filter parameters before we pass them to event handlers etc.
toParams = filterByKeys(to.params.$$keys(), toParams || {});
-
- // Re-add the saved hash before we start returning things or broadcasting $stateChangeStart
- if (hash) toParams['#'] = hash;
-
+
// Broadcast start event and cancel the transition if requested
if (options.notify) {
/**
* })
* </pre>
*/
- if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams, options).defaultPrevented) {
+ if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams).defaultPrevented) {
$rootScope.$broadcast('$stateChangeCancel', to.self, toParams, from.self, fromParams);
- //Don't update and resync url if there's been a new transition started. see issue #2238, #600
- if ($state.transition == null) $urlRouter.update();
+ $urlRouter.update();
return TransitionPrevented;
}
}
}
}
+ // Re-add the saved hash before we start returning things
+ if (hash) toParams['#'] = hash;
+
// Run it again, to catch any transitions in callbacks
if ($state.transition !== transition) return TransitionSuperseded;
}
angular.module('ui.router.state')
- .factory('$stateParams', function () { return {}; })
+ .value('$stateParams', {})
.provider('$state', $StateProvider);
if (options.view) {
result = $templateFactory.fromConfig(options.view, options.params, options.locals);
}
+ if (result && options.notify) {
+ /**
+ * @ngdoc event
+ * @name ui.router.state.$state#$viewContentLoading
+ * @eventOf ui.router.state.$view
+ * @eventType broadcast on root scope
+ * @description
+ *
+ * Fired once the view **begins loading**, *before* the DOM is rendered.
+ *
+ * @param {Object} event Event object.
+ * @param {Object} viewConfig The view config properties (template, controller, etc).
+ *
+ * @example
+ *
+ * <pre>
+ * $scope.$on('$viewContentLoading',
+ * function(event, viewConfig){
+ * // Access to all the view config properties.
+ * // and one special property 'targetView'
+ * // viewConfig.targetView
+ * });
+ * </pre>
+ */
+ $rootScope.$broadcast('$viewContentLoading', options);
+ }
return result;
}
};
angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider);
-var ngMajorVer = angular.version.major;
-var ngMinorVer = angular.version.minor;
/**
* @ngdoc directive
* @name ui.router.state.directive:ui-view
* service, {@link ui.router.state.$uiViewScroll}. This custom service let's you
* scroll ui-view elements into view when they are populated during a state activation.
*
- * @param {string=} noanimation If truthy, the non-animated renderer will be selected (no animations
- * will be applied to the ui-view)
- *
* *Note: To revert back to old [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll)
* functionality, call `$uiViewScrollProvider.useAnchorScroll()`.*
*
// Returns a set of DOM manipulation functions based on which Angular version
// it should use
function getRenderer(attrs, scope) {
- var statics = {
- enter: function (element, target, cb) { target.after(element); cb(); },
- leave: function (element, cb) { element.remove(); cb(); }
+ var statics = function() {
+ return {
+ enter: function (element, target, cb) { target.after(element); cb(); },
+ leave: function (element, cb) { element.remove(); cb(); }
+ };
};
- if (!!attrs.noanimation) return statics;
-
- function animEnabled(element) {
- if (ngMajorVer === 1 && ngMinorVer >= 4) return !!$animate.enabled(element);
- if (ngMajorVer === 1 && ngMinorVer >= 2) return !!$animate.enabled();
- return (!!$animator);
- }
-
- // ng 1.2+
if ($animate) {
return {
enter: function(element, target, cb) {
- if (!animEnabled(element)) {
- statics.enter(element, target, cb);
- } else if (angular.version.minor > 2) {
- $animate.enter(element, null, target).then(cb);
- } else {
- $animate.enter(element, null, target, cb);
- }
+ var promise = $animate.enter(element, null, target, cb);
+ if (promise && promise.then) promise.then(cb);
},
leave: function(element, cb) {
- if (!animEnabled(element)) {
- statics.leave(element, cb);
- } else if (angular.version.minor > 2) {
- $animate.leave(element).then(cb);
- } else {
- $animate.leave(element, cb);
- }
+ var promise = $animate.leave(element, cb);
+ if (promise && promise.then) promise.then(cb);
}
};
}
- // ng 1.1.5
if ($animator) {
var animate = $animator && $animator(scope, attrs);
};
}
- return statics;
+ return statics();
}
var directive = {
scope.$on('$stateChangeSuccess', function() {
updateView(false);
});
+ scope.$on('$viewContentLoading', function() {
+ updateView(false);
+ });
updateView(true);
function cleanupLastView() {
- var _previousEl = previousEl;
- var _currentScope = currentScope;
-
- if (_currentScope) {
- _currentScope._willBeDestroyed = true;
+ if (previousEl) {
+ previousEl.remove();
+ previousEl = null;
}
- function cleanOld() {
- if (_previousEl) {
- _previousEl.remove();
- }
-
- if (_currentScope) {
- _currentScope.$destroy();
- }
+ if (currentScope) {
+ currentScope.$destroy();
+ currentScope = null;
}
if (currentEl) {
renderer.leave(currentEl, function() {
- cleanOld();
previousEl = null;
});
previousEl = currentEl;
- } else {
- cleanOld();
- previousEl = null;
+ currentEl = null;
}
-
- currentEl = null;
- currentScope = null;
}
function updateView(firstTime) {
name = getUiViewName(scope, attrs, $element, $interpolate),
previousLocals = name && $state.$current && $state.$current.locals[name];
- if (!firstTime && previousLocals === latestLocals || scope._willBeDestroyed) return; // nothing to do
+ if (!firstTime && previousLocals === latestLocals) return; // nothing to do
newScope = scope.$new();
latestLocals = $state.$current.locals[name];
- /**
- * @ngdoc event
- * @name ui.router.state.directive:ui-view#$viewContentLoading
- * @eventOf ui.router.state.directive:ui-view
- * @eventType emits on ui-view directive scope
- * @description
- *
- * Fired once the view **begins loading**, *before* the DOM is rendered.
- *
- * @param {Object} event Event object.
- * @param {string} viewName Name of the view.
- */
- newScope.$emit('$viewContentLoading', name);
-
var clone = $transclude(newScope, function(clone) {
renderer.enter(clone, $element, function onUiViewEnter() {
if(currentScope) {
* @name ui.router.state.directive:ui-view#$viewContentLoaded
* @eventOf ui.router.state.directive:ui-view
* @eventType emits on ui-view directive scope
- * @description
+ * @description *
* Fired once the view is **loaded**, *after* the DOM is rendered.
*
* @param {Object} event Event object.
- * @param {string} viewName Name of the view.
*/
- currentScope.$emit('$viewContentLoaded', name);
+ currentScope.$emit('$viewContentLoaded');
currentScope.$eval(onloadExp);
}
};
}
}
-function getTypeInfo(el) {
- // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute.
- var isSvg = Object.prototype.toString.call(el.prop('href')) === '[object SVGAnimatedString]';
- var isForm = el[0].nodeName === "FORM";
-
- return {
- attr: isForm ? "action" : (isSvg ? 'xlink:href' : 'href'),
- isAnchor: el.prop("tagName").toUpperCase() === "A",
- clickable: !isForm
- };
-}
-
-function clickHook(el, $state, $timeout, type, current) {
- return function(e) {
- var button = e.which || e.button, target = current();
-
- if (!(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || el.attr('target'))) {
- // HACK: This is to allow ng-clicks to be processed before the transition is initiated:
- var transition = $timeout(function() {
- $state.go(target.state, target.params, target.options);
- });
- e.preventDefault();
-
- // if the state has no URL, ignore one preventDefault from the <a> directive.
- var ignorePreventDefaultCount = type.isAnchor && !target.href ? 1: 0;
-
- e.preventDefault = function() {
- if (ignorePreventDefaultCount-- <= 0) $timeout.cancel(transition);
- };
- }
- };
-}
-
-function defaultOpts(el, $state) {
- return { relative: stateContext(el) || $state.$current, inherit: true };
-}
-
/**
* @ngdoc directive
* @name ui.router.state.directive:ui-sref
* @restrict A
*
* @description
- * A directive that binds a link (`<a>` tag) to a state. If the state has an associated
- * URL, the directive will automatically generate & update the `href` attribute via
- * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking
- * the link will trigger a state transition with optional parameters.
+ * A directive that binds a link (`<a>` tag) to a state. If the state has an associated
+ * URL, the directive will automatically generate & update the `href` attribute via
+ * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking
+ * the link will trigger a state transition with optional parameters.
*
- * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be
+ * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be
* handled natively by the browser.
*
- * You can also use relative state paths within ui-sref, just like the relative
+ * You can also use relative state paths within ui-sref, just like the relative
* paths passed to `$state.go()`. You just need to be aware that the path is relative
- * to the state that the link lives in, in other words the state that loaded the
+ * to the state that the link lives in, in other words the state that loaded the
* template containing the link.
*
* You can specify options to pass to {@link ui.router.state.$state#go $state.go()}
* and `reload`.
*
* @example
- * Here's an example of how you'd use ui-sref and how it would compile. If you have the
+ * Here's an example of how you'd use ui-sref and how it would compile. If you have the
* following template:
* <pre>
* <a ui-sref="home">Home</a> | <a ui-sref="about">About</a> | <a ui-sref="{page: 2}">Next page</a>
- *
+ *
* <ul>
* <li ng-repeat="contact in contacts">
* <a ui-sref="contacts.detail({ id: contact.id })">{{ contact.name }}</a>
* </li>
* </ul>
* </pre>
- *
+ *
* Then the compiled html would be (assuming Html5Mode is off and current state is contacts):
* <pre>
* <a href="#/home" ui-sref="home">Home</a> | <a href="#/about" ui-sref="about">About</a> | <a href="#/contacts?page=2" ui-sref="{page: 2}">Next page</a>
- *
+ *
* <ul>
* <li ng-repeat="contact in contacts">
* <a href="#/contacts/1" ui-sref="contacts.detail({ id: contact.id })">Joe</a>
*/
$StateRefDirective.$inject = ['$state', '$timeout'];
function $StateRefDirective($state, $timeout) {
+ var allowedOptions = ['location', 'inherit', 'reload', 'absolute'];
+
return {
restrict: 'A',
require: ['?^uiSrefActive', '?^uiSrefActiveEq'],
link: function(scope, element, attrs, uiSrefActive) {
- var ref = parseStateRef(attrs.uiSref, $state.current.name);
- var def = { state: ref.state, href: null, params: null };
- var type = getTypeInfo(element);
- var active = uiSrefActive[1] || uiSrefActive[0];
+ var ref = parseStateRef(attrs.uiSref, $state.current.name);
+ var params = null, url = null, base = stateContext(element) || $state.$current;
+ // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute.
+ var hrefKind = Object.prototype.toString.call(element.prop('href')) === '[object SVGAnimatedString]' ?
+ 'xlink:href' : 'href';
+ var newHref = null, isAnchor = element.prop("tagName").toUpperCase() === "A";
+ var isForm = element[0].nodeName === "FORM";
+ var attr = isForm ? "action" : hrefKind, nav = true;
+
+ var options = { relative: base, inherit: true };
+ var optionsOverride = scope.$eval(attrs.uiSrefOpts) || {};
+
+ angular.forEach(allowedOptions, function(option) {
+ if (option in optionsOverride) {
+ options[option] = optionsOverride[option];
+ }
+ });
- def.options = extend(defaultOpts(element, $state), attrs.uiSrefOpts ? scope.$eval(attrs.uiSrefOpts) : {});
+ var update = function(newVal) {
+ if (newVal) params = angular.copy(newVal);
+ if (!nav) return;
- var update = function(val) {
- if (val) def.params = angular.copy(val);
- def.href = $state.href(ref.state, def.params, def.options);
+ newHref = $state.href(ref.state, params, options);
- if (active) active.$$addStateInfo(ref.state, def.params);
- if (def.href !== null) attrs.$set(type.attr, def.href);
+ var activeDirective = uiSrefActive[1] || uiSrefActive[0];
+ if (activeDirective) {
+ activeDirective.$$addStateInfo(ref.state, params);
+ }
+ if (newHref === null) {
+ nav = false;
+ return false;
+ }
+ attrs.$set(attr, newHref);
};
if (ref.paramExpr) {
- scope.$watch(ref.paramExpr, function(val) { if (val !== def.params) update(val); }, true);
- def.params = angular.copy(scope.$eval(ref.paramExpr));
+ scope.$watch(ref.paramExpr, function(newVal, oldVal) {
+ if (newVal !== params) update(newVal);
+ }, true);
+ params = angular.copy(scope.$eval(ref.paramExpr));
}
update();
- if (!type.clickable) return;
- element.bind("click", clickHook(element, $state, $timeout, type, function() { return def; }));
- }
- };
-}
-
-/**
- * @ngdoc directive
- * @name ui.router.state.directive:ui-state
- *
- * @requires ui.router.state.uiSref
- *
- * @restrict A
- *
- * @description
- * Much like ui-sref, but will accept named $scope properties to evaluate for a state definition,
- * params and override options.
- *
- * @param {string} ui-state 'stateName' can be any valid absolute or relative state
- * @param {Object} ui-state-params params to pass to {@link ui.router.state.$state#href $state.href()}
- * @param {Object} ui-state-opts options to pass to {@link ui.router.state.$state#go $state.go()}
- */
-$StateRefDynamicDirective.$inject = ['$state', '$timeout'];
-function $StateRefDynamicDirective($state, $timeout) {
- return {
- restrict: 'A',
- require: ['?^uiSrefActive', '?^uiSrefActiveEq'],
- link: function(scope, element, attrs, uiSrefActive) {
- var type = getTypeInfo(element);
- var active = uiSrefActive[1] || uiSrefActive[0];
- var group = [attrs.uiState, attrs.uiStateParams || null, attrs.uiStateOpts || null];
- var watch = '[' + group.map(function(val) { return val || 'null'; }).join(', ') + ']';
- var def = { state: null, params: null, options: null, href: null };
-
- function runStateRefLink (group) {
- def.state = group[0]; def.params = group[1]; def.options = group[2];
- def.href = $state.href(def.state, def.params, def.options);
-
- if (active) active.$$addStateInfo(def.state, def.params);
- if (def.href) attrs.$set(type.attr, def.href);
- }
-
- scope.$watch(watch, runStateRefLink, true);
- runStateRefLink(scope.$eval(watch));
+ if (isForm) return;
- if (!type.clickable) return;
- element.bind("click", clickHook(element, $state, $timeout, type, function() { return def; }));
+ element.bind("click", function(e) {
+ var button = e.which || e.button;
+ if ( !(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr('target')) ) {
+ // HACK: This is to allow ng-clicks to be processed before the transition is initiated:
+ var transition = $timeout(function() {
+ $state.go(ref.state, params, options);
+ });
+ e.preventDefault();
+
+ // if the state has no URL, ignore one preventDefault from the <a> directive.
+ var ignorePreventDefaultCount = isAnchor && !newHref ? 1: 0;
+ e.preventDefault = function() {
+ if (ignorePreventDefaultCount-- <= 0)
+ $timeout.cancel(transition);
+ };
+ }
+ });
}
};
}
-
/**
* @ngdoc directive
* @name ui.router.state.directive:ui-sref-active
* </li>
* </ul>
* </pre>
- *
- * It is also possible to pass ui-sref-active an expression that evaluates
- * to an object hash, whose keys represent active class names and whose
- * values represent the respective state names/globs.
- * ui-sref-active will match if the current active state **includes** any of
- * the specified state names/globs, even the abstract ones.
- *
- * @Example
- * Given the following template, with "admin" being an abstract state:
- * <pre>
- * <div ui-sref-active="{'active': 'admin.*'}">
- * <a ui-sref-active="active" ui-sref="admin.roles">Roles</a>
- * </div>
- * </pre>
- *
- * When the current state is "admin.roles" the "active" class will be applied
- * to both the <div> and <a> elements. It is important to note that the state
- * names/globs passed to ui-sref-active shadow the state provided by ui-sref.
*/
/**
function $StateRefActiveDirective($state, $stateParams, $interpolate) {
return {
restrict: "A",
- controller: ['$scope', '$element', '$attrs', '$timeout', function ($scope, $element, $attrs, $timeout) {
- var states = [], activeClasses = {}, activeEqClass, uiSrefActive;
+ controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
+ var states = [], activeClass;
// There probably isn't much point in $observing this
// uiSrefActive and uiSrefActiveEq share the same directive object with some
// slight difference in logic routing
- activeEqClass = $interpolate($attrs.uiSrefActiveEq || '', false)($scope);
-
- try {
- uiSrefActive = $scope.$eval($attrs.uiSrefActive);
- } catch (e) {
- // Do nothing. uiSrefActive is not a valid expression.
- // Fall back to using $interpolate below
- }
- uiSrefActive = uiSrefActive || $interpolate($attrs.uiSrefActive || '', false)($scope);
- if (isObject(uiSrefActive)) {
- forEach(uiSrefActive, function(stateOrName, activeClass) {
- if (isString(stateOrName)) {
- var ref = parseStateRef(stateOrName, $state.current.name);
- addState(ref.state, $scope.$eval(ref.paramExpr), activeClass);
- }
- });
- }
+ activeClass = $interpolate($attrs.uiSrefActiveEq || $attrs.uiSrefActive || '', false)($scope);
// Allow uiSref to communicate with uiSrefActive[Equals]
this.$$addStateInfo = function (newState, newParams) {
- // we already got an explicit state provided by ui-sref-active, so we
- // shadow the one that comes from ui-sref
- if (isObject(uiSrefActive) && states.length > 0) {
- return;
- }
- addState(newState, newParams, uiSrefActive);
- update();
- };
-
- $scope.$on('$stateChangeSuccess', update);
-
- function addState(stateName, stateParams, activeClass) {
- var state = $state.get(stateName, stateContext($element));
- var stateHash = createStateHash(stateName, stateParams);
+ var state = $state.get(newState, stateContext($element));
states.push({
- state: state || { name: stateName },
- params: stateParams,
- hash: stateHash
+ state: state || { name: newState },
+ params: newParams
});
- activeClasses[stateHash] = activeClass;
- }
+ update();
+ };
- /**
- * @param {string} state
- * @param {Object|string} [params]
- * @return {string}
- */
- function createStateHash(state, params) {
- if (!isString(state)) {
- throw new Error('state should be a string');
- }
- if (isObject(params)) {
- return state + toJson(params);
- }
- params = $scope.$eval(params);
- if (isObject(params)) {
- return state + toJson(params);
- }
- return state;
- }
+ $scope.$on('$stateChangeSuccess', update);
// Update route state
function update() {
- for (var i = 0; i < states.length; i++) {
- if (anyMatch(states[i].state, states[i].params)) {
- addClass($element, activeClasses[states[i].hash]);
- } else {
- removeClass($element, activeClasses[states[i].hash]);
- }
+ if (anyMatch()) {
+ $element.addClass(activeClass);
+ } else {
+ $element.removeClass(activeClass);
+ }
+ }
- if (exactMatch(states[i].state, states[i].params)) {
- addClass($element, activeEqClass);
- } else {
- removeClass($element, activeEqClass);
+ function anyMatch() {
+ for (var i = 0; i < states.length; i++) {
+ if (isMatch(states[i].state, states[i].params)) {
+ return true;
}
}
+ return false;
}
- function addClass(el, className) { $timeout(function () { el.addClass(className); }); }
- function removeClass(el, className) { el.removeClass(className); }
- function anyMatch(state, params) { return $state.includes(state.name, params); }
- function exactMatch(state, params) { return $state.is(state.name, params); }
-
- update();
+ function isMatch(state, params) {
+ if (typeof $attrs.uiSrefActiveEq !== 'undefined') {
+ return $state.is(state.name, params);
+ } else {
+ return $state.includes(state.name, params);
+ }
+ }
}]
};
}
angular.module('ui.router.state')
.directive('uiSref', $StateRefDirective)
.directive('uiSrefActive', $StateRefActiveDirective)
- .directive('uiSrefActiveEq', $StateRefActiveDirective)
- .directive('uiState', $StateRefDynamicDirective);
+ .directive('uiSrefActiveEq', $StateRefActiveDirective);
/**
* @ngdoc filter
*/
$IsStateFilter.$inject = ['$state'];
function $IsStateFilter($state) {
- var isFilter = function (state, params) {
- return $state.is(state, params);
+ var isFilter = function (state) {
+ return $state.is(state);
};
isFilter.$stateful = true;
return isFilter;
*/
$IncludedByStateFilter.$inject = ['$state'];
function $IncludedByStateFilter($state) {
- var includesFilter = function (state, params, options) {
- return $state.includes(state, params, options);
+ var includesFilter = function (state) {
+ return $state.includes(state);
};
includesFilter.$stateful = true;
return includesFilter;