diff --git a/src/state.js b/src/state.js index 730e719f8..816d254e1 100644 --- a/src/state.js +++ b/src/state.js @@ -820,11 +820,33 @@ 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' 
+     * //and current state is 'contacts.detail.item'
+     * var app angular.module('app', ['ui.router']);
+     *
+     * app.controller('ctrl', function ($scope, $state) {
+     *   $scope.reload = function(){
+     *     //will reload 'contact.detail' and 'contact.detail.item' states
+     *     $state.reload('contact.detail');
+     *   }
+     * });
+     * 
+ * + * `reload()` is just an alias for: + *
+     * $state.transitionTo($state.current, $stateParams, { 
+     *   reload: true, inherit: false, notify: true
+     * });
+     * 
+ * @returns {promise} A promise representing the state of the new transition. See * {@link ui.router.state.$state#methods_go $state.go}. */ - $state.reload = function reload() { - return $state.transitionTo($state.current, $stateParams, { reload: true, inherit: false, notify: true }); + $state.reload = function reload(state) { + return $state.transitionTo($state.current, $stateParams, { reload: state || true, inherit: false, notify: true}); }; /** @@ -928,9 +950,11 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * - **`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}, 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. + * if Object, then a stateObj is expected, will reload the state found in stateObj, and any chhildren. * * @returns {promise} A promise representing the state of the new transition. See * {@link ui.router.state.$state#methods_go $state.go}. @@ -975,6 +999,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { // Starting from the root of the path, keep all levels that haven't changed var keep = 0, state = toPath[keep], locals = root.locals, toLocals = []; + var skipTriggerReloadCheck = false; if (!options.reload) { while (state && state === fromPath[keep] && state.ownParams.$$equals(toParams, fromParams)) { @@ -982,6 +1007,23 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { keep++; state = toPath[keep]; } + } else if (isString(options.reload) || isObject(options.reload)) { + 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) + "'"); + } + + skipTriggerReloadCheck = true; + + while (state && state === fromPath[keep] && state !== reloadState) { + locals = toLocals[keep] = state.locals; + keep++; + state = toPath[keep]; + } } // If we're going to the same state and all locals are kept, we've got nothing to do. @@ -989,7 +1031,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { // 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 (!skipTriggerReloadCheck && shouldTriggerReload(to, from, locals, options)) { if (to.self.reloadOnSearch !== false) $urlRouter.update(); $state.transition = null; return $q.when($state.current); diff --git a/test/stateSpec.js b/test/stateSpec.js index 77c9373c6..d0d5f420b 100644 --- a/test/stateSpec.js +++ b/test/stateSpec.js @@ -113,7 +113,30 @@ describe('state', function () { // State param inheritance tests. param1 is inherited by sub1 & sub2; // param2 should not be transferred (unless explicitly set). .state('root', { url: '^/root?param1' }) - .state('root.sub1', {url: '/1?param2' }); + .state('root.sub1', {url: '/1?param2' }) + .state('logA', { + url: "/logA", + template: "
", + controller: function() {log += "logA;"} + }) + .state('logA.logB', { + url: "/logB", + views:{ + '':{ + template: "
", + controller: function() {log += "logB;"} + } + } + }) + .state('logA.logB.logC', { + url: "/logC", + views:{ + '':{ + template: "
", + controller: function() {log += "logC;"} + } + } + }) $stateProvider.state('root.sub2', {url: '/2?param2' }); $provide.value('AppInjectable', AppInjectable); @@ -531,6 +554,92 @@ describe('state', function () { $q.flush(); expect(log).toBe('Success!controller;Success!controller;'); })); + + it('should invoke the controllers by state when given state name', inject(function ($state, $q, $timeout, $rootScope, $compile) { + $compile('
')($rootScope); + $state.transitionTo('logA.logB.logC'); + $q.flush(); + expect(log).toBe('logA;logB;logC;'); + + log = ''; + $state.reload('logA'); + $q.flush(); + expect(log).toBe('logA;logB;logC;'); + + log = ''; + $state.reload('logA.logB'); + $q.flush(); + expect(log).toBe('logB;logC;'); + + log = ''; + $state.reload('logA.logB.logC'); + $q.flush(); + expect(log).toBe('logC;'); + })); + + it('should reload all states when passing false', inject(function ($state, $q, $timeout, $rootScope, $compile) { + $compile('
')($rootScope); + $state.transitionTo('logA.logB.logC'); + $q.flush(); + expect(log).toBe('logA;logB;logC;'); + + log = ''; + $state.reload(false); + $q.flush(); + expect(log).toBe('logA;logB;logC;'); + })); + + it('should reload all states when passing true', inject(function ($state, $q, $timeout, $rootScope, $compile) { + $compile('
')($rootScope); + $state.transitionTo('logA.logB.logC'); + $q.flush(); + expect(log).toBe('logA;logB;logC;'); + + log = ''; + $state.reload(true); + $q.flush(); + expect(log).toBe('logA;logB;logC;'); + })); + + + it('should invoke the controllers by state when given stateObj', inject(function ($state, $q, $timeout, $rootScope, $compile) { + $compile('
')($rootScope); + $state.transitionTo('logA.logB.logC'); + + $q.flush(); + expect(log).toBe('logA;logB;logC;'); + + log = ''; + $state.reload($state.current); + $q.flush(); + expect(log).toBe('logC;'); + })); + + it('should throw an exception for invalid reload state name', inject(function ($state, $q, $timeout, $rootScope, $compile) { + $compile('
')($rootScope); + $state.transitionTo('logA.logB.logC'); + $q.flush(); + expect(log).toBe('logA;logB;logC;'); + + expect(function(){ + $state.reload('logInvalid')} + ).toThrow("No such reload state 'logInvalid'"); + })); + + it('should throw an exception for invalid reload state object', inject(function ($state, $q, $timeout, $rootScope, $compile) { + $compile('
')($rootScope); + $state.transitionTo('logA.logB.logC'); + $q.flush(); + expect(log).toBe('logA;logB;logC;'); + + expect(function(){ + $state.reload({foo:'bar'})} + ).toThrow("Invalid reload state object"); + + expect(function(){ + $state.reload({name:'invalidState'})} + ).toThrow("No such reload state 'invalidState'"); + })); }); describe('.is()', function () { @@ -784,6 +893,9 @@ describe('state', function () { 'home.item', 'home.redirect', 'json', + 'logA', + 'logA.logB', + 'logA.logB.logC', 'resolveFail', 'resolveTimeout', 'root',