diff --git a/release/angular-ui-router.js b/release/angular-ui-router.js index 8444a7342..ea0c8946a 100644 --- a/release/angular-ui-router.js +++ b/release/angular-ui-router.js @@ -1,6 +1,6 @@ /** * State-based routing for AngularJS - * @version v0.2.15-dev-2015-11-19 + * @version v0.2.16 * @link http://angular-ui.github.com/ * @license MIT License, http://www.opensource.org/licenses/MIT */ @@ -23,7 +23,8 @@ var isDefined = angular.isDefined, isDate = angular.isDate, forEach = angular.forEach, extend = angular.extend, - copy = angular.copy; + copy = angular.copy, + toJson = angular.toJson; function inherit(parent, extra) { return extend(new (extend(function() {}, { prototype: parent }))(), extra); @@ -110,7 +111,7 @@ function inheritParams(currentParams, newParams, $current, $to) { var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; for (var i in parents) { - if (!parents[i].params) continue; + if (!parents[i] || !parents[i].params) continue; parentParams = objectKeys(parents[i].params); if (!parentParams.length) continue; @@ -242,7 +243,7 @@ angular.module('ui.router.util', ['ng']); /** * @ngdoc overview * @name ui.router.router - * + * * @requires ui.router.util * * @description @@ -256,7 +257,7 @@ angular.module('ui.router.router', ['ui.router.util']); /** * @ngdoc overview * @name ui.router.state - * + * * @requires ui.router.router * @requires ui.router.util * @@ -265,7 +266,7 @@ angular.module('ui.router.router', ['ui.router.util']); * * This module is a dependency of the main ui.router module. Do not include this module as a dependency * in your angular app (use {@link ui.router} module instead). - * + * */ angular.module('ui.router.state', ['ui.router.router', 'ui.router.util']); @@ -277,17 +278,17 @@ angular.module('ui.router.state', ['ui.router.router', 'ui.router.util']); * * @description * # ui.router - * - * ## The main module for ui.router + * + * ## The main module for ui.router * There are several sub-modules included with the ui.router module, however only this module is needed - * as a dependency within your angular app. The other modules are for organization purposes. + * as a dependency within your angular app. The other modules are for organization purposes. * * The modules are: * * ui.router - the main "umbrella" module - * * ui.router.router - - * + * * ui.router.router - + * * *You'll need to include **only** this module as the dependency within your angular app.* - * + * *
* * @@ -321,14 +322,14 @@ angular.module('ui.router.compat', ['ui.router']); */ $Resolve.$inject = ['$q', '$injector']; function $Resolve( $q, $injector) { - + var VISIT_IN_PROGRESS = 1, VISIT_DONE = 2, NOTHING = {}, NO_DEPENDENCIES = [], NO_LOCALS = NOTHING, NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING }); - + /** * @ngdoc function @@ -344,7 +345,7 @@ function $Resolve( $q, $injector) { ** - * @param {object} rule Handler function that takes `$injector` and `$location` + * @param {function} 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 @@ -1819,8 +1851,8 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { * }); * * - * @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: + * @param {string|function} 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. * * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance @@ -1848,7 +1880,9 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { * @methodOf ui.router.router.$urlRouterProvider * * @description - * Registers a handler for a given url matching. if handle is a string, it is + * Registers a handler for a given url matching. + * + * If the handler 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). * @@ -1877,7 +1911,7 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { * * * @param {string|object} what The incoming path that you want to redirect. - * @param {string|object} handler The path you want to redirect your user to. + * @param {string|function} handler The path you want to redirect your user to. */ this.when = function (what, handler) { var redirect, handlerIsString = isString(handler); @@ -1988,8 +2022,8 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { * */ this.$get = $get; - $get.$inject = ['$location', '$rootScope', '$injector', '$browser']; - function $get( $location, $rootScope, $injector, $browser) { + $get.$inject = ['$location', '$rootScope', '$injector', '$browser', '$sniffer']; + function $get( $location, $rootScope, $injector, $browser, $sniffer) { var baseHref = $browser.baseHref(), location = $location.url(), lastPushedUrl; @@ -2029,6 +2063,12 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { return listener; } + rules.sort(function(ruleA, ruleB) { + var aLength = ruleA.prefix ? ruleA.prefix.length : 0; + var bLength = ruleB.prefix ? ruleB.prefix.length : 0; + return bLength - aLength; + }); + if (!interceptDeferred) listen(); return { @@ -2122,7 +2162,9 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { if (angular.isObject(isHtml5)) { isHtml5 = isHtml5.enabled; } - + + isHtml5 = isHtml5 && $sniffer.history; + var url = urlMatcher.format(params); options = options || {}; @@ -2195,7 +2237,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { // 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 = extend({}, state.parent.data, state.data); + state.data = state.self.data = inherit(state.parent.data, state.data); } return state.data; }, @@ -2276,7 +2318,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { if (path) { if (!base) throw new Error("No reference point given for path '" + name + "'"); base = findState(base); - + var rel = name.split("."), i = 0, pathLength = rel.length, current = base; for (; i < pathLength; i++) { @@ -2326,7 +2368,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { 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('.')) @@ -2411,9 +2453,9 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * @methodOf ui.router.state.$stateProvider * * @description - * Allows you to extend (carefully) or override (at your own peril) the - * `stateBuilder` object used internally by `$stateProvider`. This can be used - * to add custom functionality to ui-router, for example inferring templateUrl + * Allows you to extend (carefully) or override (at your own peril) the + * `stateBuilder` object used internally by `$stateProvider`. This can be used + * to add custom functionality to ui-router, for example inferring templateUrl * based on the state name. * * When passing only a name, it returns the current (original or decorated) builder @@ -2422,14 +2464,14 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * The builder functions that can be decorated are listed below. Though not all * necessarily have a good use case for decoration, that is up to you to decide. * - * In addition, users can attach custom decorators, which will generate new - * properties within the state's internal definition. There is currently no clear - * use-case for this beyond accessing internal states (i.e. $state.$current), - * however, expect this to become increasingly relevant as we introduce additional + * In addition, users can attach custom decorators, which will generate new + * properties within the state's internal definition. There is currently no clear + * use-case for this beyond accessing internal states (i.e. $state.$current), + * however, expect this to become increasingly relevant as we introduce additional * meta-programming features. * - * **Warning**: Decorators should not be interdependent because the order of - * execution of the builder functions in non-deterministic. Builder functions + * **Warning**: Decorators should not be interdependent because the order of + * execution of the builder functions in non-deterministic. Builder functions * should only be dependent on the state definition object and super function. * * @@ -2440,21 +2482,21 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * overridden by own values (if any). * - **url** `{object}` - returns a {@link ui.router.util.type:UrlMatcher UrlMatcher} * or `null`. - * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is + * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is * navigable). - * - **params** `{object}` - returns an array of state params that are ensured to + * - **params** `{object}` - returns an array of state params that are ensured to * be a super-set of parent's params. - * - **views** `{object}` - returns a views object where each key is an absolute view - * name (i.e. "viewName@stateName") and each value is the config object - * (template, controller) for the view. Even when you don't use the views object + * - **views** `{object}` - returns a views object where each key is an absolute view + * name (i.e. "viewName@stateName") and each value is the config object + * (template, controller) for the view. Even when you don't use the views object * explicitly on a state config, one is still created for you internally. - * So by decorating this builder function you have access to decorating template + * So by decorating this builder function you have access to decorating template * and controller properties. - * - **ownParams** `{object}` - returns an array of params that belong to the state, + * - **ownParams** `{object}` - returns an array of params that belong to the state, * not including any params defined by ancestor states. - * - **path** `{string}` - returns the full path from the root down to this state. + * - **path** `{string}` - returns the full path from the root down to this state. * Needed for state activation. - * - **includes** `{object}` - returns an object that includes every state that + * - **includes** `{object}` - returns an object that includes every state that * would pass a `$state.includes()` test. * * @example @@ -2487,8 +2529,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * // and /partials/home/contact/item.html, respectively. * * - * @param {string} name The name of the builder function to decorate. - * @param {object} func A function that is responsible for decorating the original + * @param {string} name The name of the builder function to decorate. + * @param {object} func A function that is responsible for decorating the original * builder function. The function receives two parameters: * * - `{object}` - state - The state config object. @@ -2527,9 +2569,9 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * @param {string|function=} stateConfig.template * * html template as a string or a function that returns - * an html template as a string which should be used by the uiView directives. This property + * an html template as a string which should be used by the uiView directives. This property * takes precedence over templateUrl. - * + * * If `template` is a function, it will be called with the following parameters: * * - {array.<object>} - state parameters extracted from the current $location.path() by @@ -2547,10 +2589,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * * path or function that returns a path to an html * template that should be used by uiView. - * + * * If `templateUrl` is a function, it will be called with the following parameters: * - * - {array.<object>} - state parameters extracted from the current $location.path() by + * - {array.<object>} - state parameters extracted from the current $location.path() by * applying the current state * ** $resolve.resolve(invocables, locals, parent, self) *- * but the former is more efficient (in fact `resolve` just calls `study` + * but the former is more efficient (in fact `resolve` just calls `study` * internally). * * @param {object} invocables Invocable objects @@ -353,19 +354,19 @@ function $Resolve( $q, $injector) { this.study = function (invocables) { if (!isObject(invocables)) throw new Error("'invocables' must be an object"); var invocableKeys = objectKeys(invocables || {}); - + // Perform a topological sort of invocables to build an ordered plan var plan = [], cycle = [], visited = {}; function visit(value, key) { if (visited[key] === VISIT_DONE) return; - + cycle.push(key); if (visited[key] === VISIT_IN_PROGRESS) { cycle.splice(0, indexOf(cycle, key)); throw new Error("Cyclic dependency: " + cycle.join(" -> ")); } visited[key] = VISIT_IN_PROGRESS; - + if (isString(value)) { plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES); } else { @@ -375,17 +376,17 @@ function $Resolve( $q, $injector) { }); plan.push(key, value, params); } - + cycle.pop(); visited[key] = VISIT_DONE; } forEach(invocables, visit); invocables = cycle = visited = null; // plan is all that's required - + function isResolve(value) { return isObject(value) && value.then && value.$$promises; } - + return function (locals, parent, self) { if (isResolve(locals) && self === undefined) { self = parent; parent = locals; locals = null; @@ -393,12 +394,12 @@ function $Resolve( $q, $injector) { if (!locals) locals = NO_LOCALS; else if (!isObject(locals)) { throw new Error("'locals' must be an object"); - } + } if (!parent) parent = NO_PARENT; else if (!isResolve(parent)) { throw new Error("'parent' must be a promise returned by $resolve.resolve()"); } - + // To complete the overall resolution, we have to wait for the parent // promise and for the promise for each invokable in our plan. var resolution = $q.defer(), @@ -407,18 +408,18 @@ function $Resolve( $q, $injector) { values = extend({}, locals), wait = 1 + plan.length/3, merged = false; - + function done() { // Merge parent values we haven't got yet and publish our own $$values if (!--wait) { - if (!merged) merge(values, parent.$$values); + if (!merged) merge(values, parent.$$values); result.$$values = values; result.$$promises = result.$$promises || true; // keep for isResolve() delete result.$$inheritedValues; resolution.resolve(values); } } - + function fail(reason) { result.$$failure = reason; resolution.reject(reason); @@ -429,7 +430,7 @@ function $Resolve( $q, $injector) { fail(parent.$$failure); return result; } - + if (parent.$$inheritedValues) { merge(values, omit(parent.$$inheritedValues, invocableKeys)); } @@ -444,16 +445,16 @@ function $Resolve( $q, $injector) { } else { if (parent.$$inheritedValues) { result.$$inheritedValues = omit(parent.$$inheritedValues, invocableKeys); - } + } parent.then(done, fail); } - + // Process each invocable in the plan, but ignore any where a local of the same name exists. for (var i=0, ii=plan.length; i} The template html as a string, or a promise + * @return {string|Promise. } The template html as a string, or a promise * for that string. */ this.fromUrl = function (url, params) { @@ -661,9 +662,9 @@ function $TemplateFactory( $http, $templateCache, $injector) { * * @param {Function} provider Function to invoke via `$injector.invoke` * @param {Object} params Parameters for the template. - * @param {Object} locals Locals to pass to `invoke`. Defaults to + * @param {Object} locals Locals to pass to `invoke`. Defaults to * `{ params: params }`. - * @return {string|Promise. } The template html as a string, or a promise + * @return {string|Promise. } The template html as a string, or a promise * for that string. */ this.fromProvider = function (provider, params, locals) { @@ -751,13 +752,13 @@ function UrlMatcher(pattern, config, parentMatcher) { // 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\[\]]+)(?:\:( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case + // \{([\w\[\]]+)(?:\:\s*( ... ))?\} - 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\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, - searchPlaceholder = /([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + searchPlaceholder = /([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, compiled = '^', last = 0, m, segments = this.segments = [], parentParams = parentMatcher ? parentMatcher.params : {}, @@ -767,7 +768,7 @@ function UrlMatcher(pattern, config, parentMatcher) { 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]; @@ -778,7 +779,10 @@ function UrlMatcher(pattern, config, parentMatcher) { if (!pattern) return result; switch(squash) { case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break; - case true: surroundPattern = ['?(', ')?']; break; + case true: + result = result.replace(/\/$/, ''); + surroundPattern = ['(?:\/(', ')|\/)?']; + break; default: surroundPattern = ['(' + squash + "|", ')?']; break; } return result + surroundPattern[0] + pattern + surroundPattern[1]; @@ -794,7 +798,11 @@ function UrlMatcher(pattern, config, parentMatcher) { cfg = config.params[id]; segment = pattern.substring(last, m.index); regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); - type = $$UMFP.type(regexp || "string") || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) }); + + if (regexp) { + type = $$UMFP.type(regexp) || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) }); + } + return { id: id, regexp: regexp, segment: segment, type: type, cfg: cfg }; @@ -924,20 +932,29 @@ UrlMatcher.prototype.exec = function (path, searchParams) { return map(allReversed, unquoteDashes).reverse(); } + var param, paramVal; for (i = 0; i < nPath; i++) { paramName = paramNames[i]; - var param = this.params[paramName]; - var paramVal = m[i+1]; + param = this.params[paramName]; + paramVal = m[i+1]; // if the param value matches a pre-replace pair, replace the value before decoding. - for (j = 0; j < param.replace; j++) { + for (j = 0; j < param.replace.length; 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; @@ -961,7 +978,7 @@ UrlMatcher.prototype.parameters = function (param) { /** * @ngdoc function - * @name ui.router.util.type:UrlMatcher#validate + * @name ui.router.util.type:UrlMatcher#validates * @methodOf ui.router.util.type:UrlMatcher * * @description @@ -1014,6 +1031,8 @@ UrlMatcher.prototype.format = function (values) { if (isPathParam) { var nextSegment = segments[i + 1]; + var isFinalPathParam = i + 1 === nPath; + if (squash === false) { if (encoded != null) { if (isArray(encoded)) { @@ -1029,9 +1048,12 @@ UrlMatcher.prototype.format = function (values) { } 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; @@ -1196,6 +1218,7 @@ Type.prototype.$asArray = function(mode, isSearch) { // 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) @@ -1244,8 +1267,12 @@ function $UrlMatcherFactory() { var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false; - function valToString(val) { return val != null ? val.toString().replace(/\//g, "%2F") : val; } - function valFromString(val) { return val != null ? val.toString().replace(/%2F/g, "/") : val; } + // 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; } var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = { string: { @@ -1588,7 +1615,12 @@ function $UrlMatcherFactory() { 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); - return config.type instanceof Type ? config.type : new Type(config.type); + + if (angular.isString(config.type)) + return $types[config.type]; + if (config.type instanceof Type) + return config.type; + return new Type(config.type); } // array config: param name (param[]) overrides default settings. explicit config overrides param name. @@ -1732,9 +1764,9 @@ angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcher * @requires $locationProvider * * @description - * `$urlRouterProvider` has the responsibility of watching `$location`. - * When `$location` changes it runs through a list of rules one by one until a - * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify + * `$urlRouterProvider` has the responsibility of watching `$location`. + * When `$location` changes it runs through a list of rules one by one until a + * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify * a url in a state configuration. All urls are compiled into a UrlMatcher object. * * There are several methods on `$urlRouterProvider` that make it useful to use directly @@ -1783,7 +1815,7 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { * }); *
templateUrl: "home.html"@@ -2594,7 +2636,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * * @param {string=} stateConfig.controllerAs * - * + * * A controller alias name. If present the controller will be * published to scope under the controllerAs name. *
controllerAs: "myCtrl"@@ -2610,17 +2652,17 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * * * An optional map<string, function> of dependencies which - * should be injected into the controller. If any of these dependencies are promises, + * should be injected into the controller. If any of these dependencies are promises, * the router will wait for them all to be resolved before the controller is instantiated. * If all the promises are resolved successfully, the $stateChangeSuccess event is fired * and the values of the resolved promises are injected into any controllers that reference them. * If any of the promises are rejected the $stateChangeError event is fired. * * The map object is: - * + * * - key - {string}: name of dependency to be injected into controller - * - factory - {string|function}: If string then it is alias for service. Otherwise if function, - * it is injected and return value it treated as dependency. If result is a promise, it is + * - factory - {string|function}: If string then it is alias for service. Otherwise if function, + * it is injected and return value it treated as dependency. If result is a promise, it is * resolved before its value is injected into controller. * *
resolve: { @@ -2634,7 +2676,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * * * A url fragment with optional parameters. When a state is navigated or - * transitioned to, the `$stateParams` service will be populated with any + * transitioned to, the `$stateParams` service will be populated with any * parameters that were passed. * * (See {@link ui.router.util.type:UrlMatcher UrlMatcher} `UrlMatcher`} for @@ -2694,7 +2736,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * * 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 explictly annotate this function, + * If minifying your scripts, make sure to explicitly annotate this function, * because it won't be automatically annotated by your build tools. * *onEnter: function(MyService, $stateParams) { @@ -2706,7 +2748,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * * 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 explictly annotate this function, + * If minifying your scripts, make sure to explicitly annotate this function, * because it won't be automatically annotated by your build tools. * *- * + * * Then the compiled html would be (assuming Html5Mode is off and current state is contacts): *onExit: function(MyService, $stateParams) { @@ -2717,7 +2759,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * * * If `false`, will not retrigger the same state - * just because a search/query parameter has changed (via $location.search() or $location.hash()). + * just because a search/query parameter has changed (via $location.search() or $location.hash()). * Useful for when you'd like to modify $location.search() without triggering a reload. **reloadOnSearch: false* @@ -2852,11 +2894,11 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * @requires ui.router.state.$stateParams * @requires ui.router.router.$urlRouter * - * @property {object} params A param object, e.g. {sectionId: section.id)}, that + * @property {object} params A param object, e.g. {sectionId: section.id)}, that * you'd like to test against the current active state. - * @property {object} current A reference to the state's config object. However + * @property {object} current A reference to the state's config object. However * you passed it in. Useful for accessing custom data. - * @property {object} transition Currently pending transition. A promise that'll + * @property {object} transition Currently pending transition. A promise that'll * resolve or reject. * * @description @@ -2969,7 +3011,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * * `reload()` is just an alias for: *- * $state.transitionTo($state.current, $stateParams, { + * $state.transitionTo($state.current, $stateParams, { * reload: true, inherit: false, notify: true * }); *@@ -2977,7 +3019,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * @param {string=|object=} state - A state name or a state object, which is the root of the resolves to be re-resolved. * @example *- * //assuming app application consists of 3 states: 'contacts', 'contacts.detail', 'contacts.detail.item' + * //assuming app application consists of 3 states: 'contacts', 'contacts.detail', 'contacts.detail.item' * //and current state is 'contacts.detail.item' * var app angular.module('app', ['ui.router']); * @@ -2991,7 +3033,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * * `reload()` is just an alias for: **/ - if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams).defaultPrevented) { + if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams, options).defaultPrevented) { $rootScope.$broadcast('$stateChangeCancel', to.self, toParams, from.self, fromParams); - $urlRouter.update(); + //Don't update and resync url if there's been a new transition started. see issue #2238, #600 + if ($state.transition == null) $urlRouter.update(); return TransitionPrevented; } } @@ -3279,9 +3328,6 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { } } - // 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; @@ -3483,10 +3529,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * first parameter, then the constructed href url will be built from the first navigable ancestor (aka * ancestor with a valid url). * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. - * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), * defines which state to be relative from. * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". - * + * * @returns {string} compiled state url */ $state.href = function href(stateOrName, params, options) { @@ -3501,7 +3547,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { if (!isDefined(state)) return null; if (options.inherit) params = inheritParams($stateParams, params || {}, $state.$current, state); - + var nav = (state && options.lossy) ? state.navigable : state; if (!nav || nav.url === undefined || nav.url === null) { @@ -3615,7 +3661,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { } angular.module('ui.router.state') - .value('$stateParams', {}) + .factory('$stateParams', function () { return {}; }) .provider('$state', $StateProvider); @@ -3656,32 +3702,6 @@ function $ViewProvider() { 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 - * - *- * $state.transitionTo($state.current, $stateParams, { + * $state.transitionTo($state.current, $stateParams, { * reload: true, inherit: false, notify: true * }); *@@ -3009,11 +3051,11 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * @methodOf ui.router.state.$state * * @description - * Convenience method for transitioning to a new state. `$state.go` calls - * `$state.transitionTo` internally but automatically sets options to - * `{ location: true, inherit: true, relative: $state.$current, notify: true }`. - * This allows you to easily use an absolute or relative to path and specify - * only the parameters you'd like to update (while letting unspecified parameters + * Convenience method for transitioning to a new state. `$state.go` calls + * `$state.transitionTo` internally but automatically sets options to + * `{ location: true, inherit: true, relative: $state.$current, notify: true }`. + * This allows you to easily use an absolute or relative to path and specify + * only the parameters you'd like to update (while letting unspecified parameters * inherit from the currently active ancestor states). * * @example @@ -3035,9 +3077,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * - `$state.go('^.sibling')` - will go to a sibling state * - `$state.go('.child.grandchild')` - will go to grandchild state * - * @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. This allows, for example, going to a sibling state that shares parameters + * @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 * 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. @@ -3046,12 +3089,13 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` * will not. If string, must be `"replace"`, which will update url and also replace last history record. * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. - * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * - **`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}, 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. + * - **`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. * * @returns {promise} A promise representing the state of the new transition. * @@ -3101,10 +3145,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` * will not. If string, must be `"replace"`, which will update url and also replace last history record. * - **`inherit`** - {boolean=false}, If `true` will inherit url parameters from current url. - * - **`relative`** - {object=}, When transitioning with relative path (e.g '^'), + * - **`relative`** - {object=}, 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 the state or params + * - **`reload`** (v0.2.5) - {boolean=false|string=|object=}, 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. * if String, then will reload the state with the name given in reload, and any children. @@ -3167,7 +3211,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { if (isObject(options.reload) && !options.reload.name) { throw new Error('Invalid reload state object'); } - + var reloadState = options.reload === true ? fromPath[0] : findState(options.reload); if (options.reload && !reloadState) { throw new Error("No such reload state '" + (isString(options.reload) ? options.reload : options.reload.name) + "'"); @@ -3189,6 +3233,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { 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' @@ -3202,6 +3247,9 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { // 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) { /** @@ -3231,9 +3279,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * }) *- * $scope.$on('$viewContentLoading', - * function(event, viewConfig){ - * // Access to all the view config properties. - * // and one special property 'targetView' - * // viewConfig.targetView - * }); - *- */ - $rootScope.$broadcast('$viewContentLoading', options); - } return result; } }; @@ -3771,26 +3791,26 @@ angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider) * functionality, call `$uiViewScrollProvider.useAnchorScroll()`.* * * @param {string=} onload Expression to evaluate whenever the view updates. - * + * * @example - * A view can be unnamed or named. + * A view can be unnamed or named. ** - * - * + * + * * * ** - * You can only have one unnamed view within any template (or root html). If you are only using a + * You can only have one unnamed view within any template (or root html). If you are only using a * single view and it is unnamed then you can populate it like so: *- * + * * $stateProvider.state("home", { * template: "- * + * * The above is a convenient shortcut equivalent to specifying your view explicitly with the {@link ui.router.state.$stateProvider#views `views`} * config property, by name, in this case an empty name: *HELLO!
" * }) *@@ -3799,33 +3819,33 @@ angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider) * "": { * template: "- * - * But typically you'll only use the views property if you name your view or have more than one view - * in the same template. There's not really a compelling reason to name a view if its the only one, + * + * But typically you'll only use the views property if you name your view or have more than one view + * in the same template. There's not really a compelling reason to name a view if its the only one, * but you could if you wanted, like so: *HELLO!
" * } - * } + * } * }) ** - *+ ** $stateProvider.state("home", { * views: { * "main": { * template: "- * + * * Really though, you'll use views to set up multiple views: *HELLO!
" * } - * } + * } * }) ** - * - * + * + * *- * + * ** $stateProvider.state("home", { * views: { @@ -3838,7 +3858,7 @@ angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider) * "data": { * template: "* @@ -3888,12 +3908,18 @@ function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate) if ($animate) { return { enter: function(element, target, cb) { - var promise = $animate.enter(element, null, target, cb); - if (promise && promise.then) promise.then(cb); + if (angular.version.minor > 2) { + $animate.enter(element, null, target).then(cb); + } else { + $animate.enter(element, null, target, cb); + } }, leave: function(element, cb) { - var promise = $animate.leave(element, cb); - if (promise && promise.then) promise.then(cb); + if (angular.version.minor > 2) { + $animate.leave(element).then(cb); + } else { + $animate.leave(element, cb); + } } }; } @@ -3925,31 +3951,41 @@ function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate) scope.$on('$stateChangeSuccess', function() { updateView(false); }); - scope.$on('$viewContentLoading', function() { - updateView(false); - }); updateView(true); function cleanupLastView() { - if (previousEl) { - previousEl.remove(); - previousEl = null; + var _previousEl = previousEl; + var _currentScope = currentScope; + + if (_currentScope) { + _currentScope._willBeDestroyed = true; } - if (currentScope) { - currentScope.$destroy(); - currentScope = null; + function cleanOld() { + if (_previousEl) { + _previousEl.remove(); + } + + if (_currentScope) { + _currentScope.$destroy(); + } } if (currentEl) { renderer.leave(currentEl, function() { + cleanOld(); previousEl = null; }); previousEl = currentEl; - currentEl = null; + } else { + cleanOld(); + previousEl = null; } + + currentEl = null; + currentScope = null; } function updateView(firstTime) { @@ -3957,10 +3993,24 @@ function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate) name = getUiViewName(scope, attrs, $element, $interpolate), previousLocals = name && $state.$current && $state.$current.locals[name]; - if (!firstTime && previousLocals === latestLocals) return; // nothing to do + if (!firstTime && previousLocals === latestLocals || scope._willBeDestroyed) 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) { @@ -3981,12 +4031,13 @@ function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate) * @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'); + currentScope.$emit('$viewContentLoaded', name); currentScope.$eval(onloadExp); } }; @@ -4063,6 +4114,43 @@ function stateContext(el) { } } +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 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 @@ -4073,17 +4161,17 @@ function stateContext(el) { * @restrict A * * @description - * A directive that binds a link (`` 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 (`` 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()} @@ -4091,22 +4179,22 @@ function stateContext(el) { * 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: *" * } - * } + * } * }) * * Home | About | Next page - * + * **
*- * {{ contact.name }} *
** Home | About | Next page - * + * *
+ *+ * + * When the current state is "admin.roles" the "active" class will be applied + * to both the+ * Roles + *+ *