diff --git a/src/stateDirectives.js b/src/stateDirectives.js index e97da32b4..282eb7533 100644 --- a/src/stateDirectives.js +++ b/src/stateDirectives.js @@ -26,14 +26,23 @@ function getTypeInfo(el) { }; } -function clickHook(el, $state, $timeout, type, current) { +function clickHook(scope, 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); + var transitionPromise = $state.go(target.state, target.params, target.options); + var noop = function() {}; + + // if there's an error since the state change is cancelled + // emit $stateChangeCancel + transitionPromise.then(noop, function(e) { + if (scope) { + scope.$emit('$stateChangeCancel', e); + } + }); }); e.preventDefault(); @@ -144,7 +153,7 @@ function $StateRefDirective($state, $timeout) { update(); if (!type.clickable) return; - hookFn = clickHook(element, $state, $timeout, type, function() { return def; }); + hookFn = clickHook(scope, element, $state, $timeout, type, function() { return def; }); element.bind("click", hookFn); scope.$on('$destroy', function() { element.unbind("click", hookFn); @@ -196,7 +205,7 @@ function $StateRefDynamicDirective($state, $timeout) { runStateRefLink(scope.$eval(watch)); if (!type.clickable) return; - hookFn = clickHook(element, $state, $timeout, type, function() { return def; }); + hookFn = clickHook(scope, element, $state, $timeout, type, function() { return def; }); element.bind("click", hookFn); scope.$on('$destroy', function() { element.unbind("click", hookFn); diff --git a/test/stateDirectivesSpec.js b/test/stateDirectivesSpec.js index 0b538ce1e..9773fe895 100644 --- a/test/stateDirectivesSpec.js +++ b/test/stateDirectivesSpec.js @@ -141,7 +141,7 @@ describe('uiStateRef', function() { ctrlKey: undefined, shiftKey: undefined, altKey: undefined, - button: undefined + button: undefined }); timeoutFlush(); $q.flush(); @@ -156,7 +156,7 @@ describe('uiStateRef', function() { timeoutFlush(); $q.flush(); - + expect($state.current.name).toEqual('top'); expect($stateParams).toEqualData({ }); })); @@ -222,7 +222,7 @@ describe('uiStateRef', function() { it('should allow passing params to current state', inject(function($compile, $rootScope, $state) { $state.current.name = 'contacts.item.detail'; - + el = angular.element("Details"); $rootScope.$index = 3; $rootScope.$apply(); @@ -231,10 +231,10 @@ describe('uiStateRef', function() { $rootScope.$digest(); expect(el.attr('href')).toBe('#/contacts/3'); })); - + it('should allow multi-line attribute values when passing params to current state', inject(function($compile, $rootScope, $state) { $state.current.name = 'contacts.item.detail'; - + el = angular.element("Details"); $rootScope.$index = 3; $rootScope.$apply(); @@ -257,6 +257,25 @@ describe('uiStateRef', function() { $rootScope.$digest(); expect(angular.element(template[0].querySelector('a')).attr('href')).toBe('#/contacts/2'); })); + + it('emits $stateChangeCancel when transition rejects', inject(function ($rootScope, $timeout, $state, $q) { + var TransitionSupersededError = new Error('Transition superseded'); + + $rootScope.$on('$stateChangeCancel', function(_, e) { + expect(e).toEqual(TransitionSupersededError); + }); + + spyOn($state, 'go').andCallFake(function(state, params, options) { + var deferred = $q.defer(); + + deferred.reject(TransitionSupersededError); + return deferred.promise; + }); + + triggerClick(el); + $timeout.flush(); + $rootScope.$digest(); + })); }); describe('links in html5 mode', function() { @@ -368,7 +387,7 @@ describe('uiStateRef', function() { expect(angular.element(template[0]).attr('href')).toBe('#/contacts/10'); })); - it('accepts option overrides', inject(function ($compile, $timeout, $state) { + it('accepts option overrides', inject(function ($compile, $timeout, $state, $q) { var transitionOptions; el = angular.element('state'); @@ -378,7 +397,11 @@ describe('uiStateRef', function() { scope.$digest(); spyOn($state, 'go').andCallFake(function(state, params, options) { + var deferred = $q.defer(); + + deferred.resolve(42); transitionOptions = options; + return deferred.promise; }); triggerClick(template)