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)