Skip to content

Commit 3b6c735

Browse files
committed
fix(ui-sref): handle state transition promise rejection
In the current world, ui-sref does nothing when the promise returned by $state.go is rejected. This PR gives ui-sref directive the ability to emit $stateChangeCancel when the transition promise rejects. The reason why I have chosen to implement this in the ui-sref directive and not in the definition for $state.go or $state.transitionTo are outlined in the issue below. angular-ui#3027.
1 parent 953235a commit 3b6c735

File tree

2 files changed

+42
-10
lines changed

2 files changed

+42
-10
lines changed

src/stateDirectives.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,23 @@ function getTypeInfo(el) {
2626
};
2727
}
2828

29-
function clickHook(el, $state, $timeout, type, current) {
29+
function clickHook(scope, el, $state, $timeout, type, current) {
3030
return function(e) {
3131
var button = e.which || e.button, target = current();
3232

3333
if (!(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || el.attr('target'))) {
3434
// HACK: This is to allow ng-clicks to be processed before the transition is initiated:
3535
var transition = $timeout(function() {
36-
$state.go(target.state, target.params, target.options);
36+
var transitionPromise = $state.go(target.state, target.params, target.options);
37+
var noop = function() {};
38+
39+
// if there's an error since the state change is cancelled
40+
// emit $stateChangeCancel
41+
transitionPromise.then(noop, function(e) {
42+
if (scope) {
43+
scope.$emit('$stateChangeCancel', e);
44+
}
45+
});
3746
});
3847
e.preventDefault();
3948

@@ -144,7 +153,7 @@ function $StateRefDirective($state, $timeout) {
144153
update();
145154

146155
if (!type.clickable) return;
147-
hookFn = clickHook(element, $state, $timeout, type, function() { return def; });
156+
hookFn = clickHook(scope, element, $state, $timeout, type, function() { return def; });
148157
element.bind("click", hookFn);
149158
scope.$on('$destroy', function() {
150159
element.unbind("click", hookFn);
@@ -196,7 +205,7 @@ function $StateRefDynamicDirective($state, $timeout) {
196205
runStateRefLink(scope.$eval(watch));
197206

198207
if (!type.clickable) return;
199-
hookFn = clickHook(element, $state, $timeout, type, function() { return def; });
208+
hookFn = clickHook(scope, element, $state, $timeout, type, function() { return def; });
200209
element.bind("click", hookFn);
201210
scope.$on('$destroy', function() {
202211
element.unbind("click", hookFn);

test/stateDirectivesSpec.js

+29-6
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ describe('uiStateRef', function() {
141141
ctrlKey: undefined,
142142
shiftKey: undefined,
143143
altKey: undefined,
144-
button: undefined
144+
button: undefined
145145
});
146146
timeoutFlush();
147147
$q.flush();
@@ -156,7 +156,7 @@ describe('uiStateRef', function() {
156156

157157
timeoutFlush();
158158
$q.flush();
159-
159+
160160
expect($state.current.name).toEqual('top');
161161
expect($stateParams).toEqualData({ });
162162
}));
@@ -222,7 +222,7 @@ describe('uiStateRef', function() {
222222

223223
it('should allow passing params to current state', inject(function($compile, $rootScope, $state) {
224224
$state.current.name = 'contacts.item.detail';
225-
225+
226226
el = angular.element("<a ui-sref=\"{id: $index}\">Details</a>");
227227
$rootScope.$index = 3;
228228
$rootScope.$apply();
@@ -231,10 +231,10 @@ describe('uiStateRef', function() {
231231
$rootScope.$digest();
232232
expect(el.attr('href')).toBe('#/contacts/3');
233233
}));
234-
234+
235235
it('should allow multi-line attribute values when passing params to current state', inject(function($compile, $rootScope, $state) {
236236
$state.current.name = 'contacts.item.detail';
237-
237+
238238
el = angular.element("<a ui-sref=\"{\n\tid: $index\n}\">Details</a>");
239239
$rootScope.$index = 3;
240240
$rootScope.$apply();
@@ -257,6 +257,25 @@ describe('uiStateRef', function() {
257257
$rootScope.$digest();
258258
expect(angular.element(template[0].querySelector('a')).attr('href')).toBe('#/contacts/2');
259259
}));
260+
261+
it('emits $stateChangeCancel when transition rejects', inject(function ($rootScope, $timeout, $state, $q) {
262+
var TransitionSupersededError = new Error('Transition superseded');
263+
264+
$rootScope.$on('$stateChangeCancel', function(_, e) {
265+
expect(e).toEqual(TransitionSupersededError);
266+
});
267+
268+
spyOn($state, 'go').andCallFake(function(state, params, options) {
269+
var deferred = $q.defer();
270+
271+
deferred.reject(TransitionSupersededError);
272+
return deferred.promise;
273+
});
274+
275+
triggerClick(el);
276+
$timeout.flush();
277+
$rootScope.$digest();
278+
}));
260279
});
261280

262281
describe('links in html5 mode', function() {
@@ -368,7 +387,7 @@ describe('uiStateRef', function() {
368387
expect(angular.element(template[0]).attr('href')).toBe('#/contacts/10');
369388
}));
370389

371-
it('accepts option overrides', inject(function ($compile, $timeout, $state) {
390+
it('accepts option overrides', inject(function ($compile, $timeout, $state, $q) {
372391
var transitionOptions;
373392

374393
el = angular.element('<a ui-state="state" ui-state-opts="opts">state</a>');
@@ -378,7 +397,11 @@ describe('uiStateRef', function() {
378397
scope.$digest();
379398

380399
spyOn($state, 'go').andCallFake(function(state, params, options) {
400+
var deferred = $q.defer();
401+
402+
deferred.resolve(42);
381403
transitionOptions = options;
404+
return deferred.promise;
382405
});
383406

384407
triggerClick(template)

0 commit comments

Comments
 (0)