diff --git a/src/state.js b/src/state.js index 4b74670e8..c972eabc8 100644 --- a/src/state.js +++ b/src/state.js @@ -4,7 +4,6 @@ * * @requires ui.router.router.$urlRouterProvider * @requires ui.router.util.$urlMatcherFactoryProvider - * @requires $locationProvider * * @description * The new `$stateProvider` works similar to Angular's v1 router, but it focuses purely @@ -20,8 +19,8 @@ * * The `$stateProvider` provides interfaces to declare these states for your app. */ -$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider', '$locationProvider']; -function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $locationProvider) { +$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider']; +function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { var root, states = {}, $state, queue = {}, abstractKey = 'abstract'; @@ -521,6 +520,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $ * @requires $injector * @requires ui.router.util.$resolve * @requires ui.router.state.$stateParams + * @requires ui.router.router.$urlRouter * * @property {object} params A param object, e.g. {sectionId: section.id)}, that * you'd like to test against the current active state. @@ -534,24 +534,14 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $ * between them. It also provides interfaces to ask for current state or even states * you're coming from. */ - // $urlRouter is injected just to ensure it gets instantiated this.$get = $get; - $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$location', '$urlRouter', '$browser']; - function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $location, $urlRouter, $browser) { + $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter']; + function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter) { var TransitionSuperseded = $q.reject(new Error('transition superseded')); var TransitionPrevented = $q.reject(new Error('transition prevented')); var TransitionAborted = $q.reject(new Error('transition aborted')); var TransitionFailed = $q.reject(new Error('transition failed')); - var currentLocation = $location.url(); - var baseHref = $browser.baseHref(); - - function syncUrl() { - if ($location.url() !== currentLocation) { - $location.url(currentLocation); - $location.replace(); - } - } // Handles the case where a state which is the target of a transition is not found, and the user // can optionally retry or defer the transition @@ -591,7 +581,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $ var evt = $rootScope.$broadcast('$stateNotFound', redirect, state, params); if (evt.defaultPrevented) { - syncUrl(); + $urlRouter.update(); return TransitionAborted; } @@ -601,7 +591,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $ // Allow the handler to return a promise to defer state lookup retry if (options.$retry) { - syncUrl(); + $urlRouter.update(); return TransitionFailed; } var retryTransition = $state.transition = $q.when(evt.retry); @@ -613,7 +603,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $ }, function() { return TransitionAborted; }); - syncUrl(); + $urlRouter.update(); return retryTransition; } @@ -813,11 +803,11 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $ // If we're going to the same state and all locals are kept, we've got nothing to do. // But clear 'transition', as we still want to cancel any other pending transitions. - // TODO: We may not want to bump 'transition' if we're called from a location change that we've initiated ourselves, - // because we might accidentally abort a legitimate transition initiated from code? + // TODO: We may not want to bump 'transition' if we're called from a location change + // that we've initiated ourselves, because we might accidentally abort a legitimate + // transition initiated from code? if (shouldTriggerReload(to, from, locals, options) ) { - if (to.self.reloadOnSearch !== false) - syncUrl(); + if (to.self.reloadOnSearch !== false) $urlRouter.update(); $state.transition = null; return $q.when($state.current); } @@ -855,7 +845,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $ * */ if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams).defaultPrevented) { - syncUrl(); + $urlRouter.update(); return TransitionPrevented; } } @@ -910,14 +900,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $ copy($state.params, $stateParams); $state.transition = null; - // Update $location - var toNav = to.navigable; - if (options.location && toNav) { - $location.url(toNav.url.format(toNav.locals.globals.$stateParams)); - - if (options.location === 'replace') { - $location.replace(); - } + if (options.location && to.navigable) { + $urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, { + replace: options.location === 'replace' + }); } if (options.notify) { @@ -937,7 +923,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $ */ $rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams); } - currentLocation = $location.url(); + $urlRouter.update(true); return $state.current; }, function (error) { @@ -965,7 +951,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $ evt = $rootScope.$broadcast('$stateChangeError', to.self, toParams, from.self, fromParams, error); if (!evt.defaultPrevented) { - syncUrl(); + $urlRouter.update(); } return $q.reject(error); @@ -1112,33 +1098,18 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $ */ $state.href = function href(stateOrName, params, options) { options = extend({ lossy: true, inherit: false, absolute: false, relative: $state.$current }, options || {}); + var state = findState(stateOrName, options.relative); - if (!isDefined(state)) return null; + if (!isDefined(state)) return null; if (options.inherit) params = inheritParams($stateParams, params || {}, $state.$current, state); var nav = (state && options.lossy) ? state.navigable : state; - var url = (nav && nav.url) ? nav.url.format(normalize(state.params, params || {})) : null; - if (!$locationProvider.html5Mode() && url) { - url = "#" + $locationProvider.hashPrefix() + url; - } - - if (baseHref !== '/') { - if ($locationProvider.html5Mode()) { - url = baseHref.slice(0, -1) + url; - } else if (options.absolute){ - url = baseHref.slice(1) + url; - } - } - if (options.absolute && url) { - url = $location.protocol() + '://' + - $location.host() + - ($location.port() == 80 || $location.port() == 443 ? '' : ':' + $location.port()) + - (!$locationProvider.html5Mode() && url ? '/' : '') + - url; + if (!nav || !nav.url) { + return null; } - return url; + return $urlRouter.href(nav.url, normalize(state.params, params || {}), { absolute: options.absolute }); }; /** diff --git a/src/urlRouter.js b/src/urlRouter.js index 9bbe198e6..4652b89bd 100644 --- a/src/urlRouter.js +++ b/src/urlRouter.js @@ -3,6 +3,7 @@ * @name ui.router.router.$urlRouterProvider * * @requires ui.router.util.$urlMatcherFactoryProvider + * @requires $locationProvider * * @description * `$urlRouterProvider` has the responsibility of watching `$location`. @@ -13,8 +14,8 @@ * There are several methods on `$urlRouterProvider` that make it useful to use directly * in your module config. */ -$UrlRouterProvider.$inject = ['$urlMatcherFactoryProvider']; -function $UrlRouterProvider( $urlMatcherFactory) { +$UrlRouterProvider.$inject = ['$locationProvider', '$urlMatcherFactoryProvider']; +function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { var rules = [], otherwise = null; @@ -208,66 +209,120 @@ function $UrlRouterProvider( $urlMatcherFactory) { * @requires $location * @requires $rootScope * @requires $injector + * @requires $browser * * @description * */ - this.$get = - [ '$location', '$rootScope', '$injector', - function ($location, $rootScope, $injector) { - // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree - function update(evt) { - if (evt && evt.defaultPrevented) return; - function check(rule) { - var handled = rule($injector, $location); - if (handled) { - if (isString(handled)) $location.replace().url(handled); - return true; - } + this.$get = $get; + $get.$inject = ['$location', '$rootScope', '$injector', '$browser']; + function $get( $location, $rootScope, $injector, $browser) { + + var baseHref = $browser.baseHref(), location = $location.url(); + + function appendBasePath(url, isHtml5, absolute) { + if (baseHref === '/') return url; + if (isHtml5) return baseHref.slice(0, -1) + url; + if (absolute) return baseHref.slice(1) + url; + return url; + } + + // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree + function update(evt) { + if (evt && evt.defaultPrevented) return; + + function check(rule) { + var handled = rule($injector, $location); + + if (!handled) { return false; } - var n=rules.length, i; - for (i=0; i + * angular.module('app', ['ui.router']); + * .run(function($rootScope, $urlRouter) { + * $rootScope.$on('$locationChangeSuccess', function(evt) { + * // Halt state change from even starting + * evt.preventDefault(); + * // Perform custom logic + * var meetsRequirement = ... + * // Continue with the update and state transition if logic allows + * if (meetsRequirement) $urlRouter.sync(); + * }); + * }); + * + */ + sync: function() { + update(); + }, - return { - /** - * @ngdoc function - * @name ui.router.router.$urlRouter#sync - * @methodOf ui.router.router.$urlRouter - * - * @description - * Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`. - * This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event, - * perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed - * with the transition by calling `$urlRouter.sync()`. - * - * @example - *
-         * angular.module('app', ['ui.router']);
-         *   .run(function($rootScope, $urlRouter) {
-         *     $rootScope.$on('$locationChangeSuccess', function(evt) {
-         *       // Halt state change from even starting
-         *       evt.preventDefault();
-         *       // Perform custom logic
-         *       var meetsRequirement = ...
-         *       // Continue with the update and state transition if logic allows
-         *       if (meetsRequirement) $urlRouter.sync();
-         *     });
-         * });
-         * 
- */ - sync: function () { - update(); + update: function(read) { + if (read) { + location = $location.url(); + return; } - }; - }]; + if ($location.url() === location) { + return; + } + $location.url(location); + $location.replace(); + }, + + push: function(urlMatcher, params, options) { + $location.url(urlMatcher.format(params)); + options = options || {}; + + if (options.replace) { + $location.replace(); + } + }, + + href: function(urlMatcher, params, options) { + var isHtml5 = $locationProvider.html5Mode(); + var url = urlMatcher.format(params); + + if (!isHtml5 && url) { + url = "#" + $locationProvider.hashPrefix() + url; + } + url = appendBasePath(url, isHtml5, options.absolute); + + if (!options.absolute || !url) { + return url; + } + + var slash = (!isHtml5 && url ? '/' : ''), + port = $location.port() == 80 || $location.port() == 443 ? '' : ':' + $location.port(); + + return [$location.protocol(), '://', $location.host(), port, slash, url].join(''); + } + }; + } } angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider);