Skip to content

Commit e3c7cfa

Browse files
committed
feat($state): sync last known good url back on fail
Closes #273, #242
1 parent dd68190 commit e3c7cfa

File tree

2 files changed

+45
-7
lines changed

2 files changed

+45
-7
lines changed

src/state.js

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,14 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
248248
var TransitionPrevented = $q.reject(new Error('transition prevented'));
249249
var TransitionAborted = $q.reject(new Error('transition aborted'));
250250
var TransitionFailed = $q.reject(new Error('transition failed'));
251+
var currentLocation = $location.url();
252+
253+
function syncUrl() {
254+
if ($location.url() !== currentLocation) {
255+
$location.url(currentLocation);
256+
$location.replace();
257+
}
258+
}
251259

252260
root.locals = { resolve: null, globals: { $stateParams: {} } };
253261
$state = {
@@ -267,20 +275,23 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
267275
options = extend({ location: true, inherit: false, relative: null, notify: true, $retry: false }, options);
268276

269277
var from = $state.$current, fromParams = $state.params, fromPath = from.path;
270-
271-
var toState = findState(to, options.relative);
272-
273-
var evt;
278+
var evt, toState = findState(to, options.relative);
274279

275280
if (!isDefined(toState)) {
276281
// Broadcast not found event and abort the transition if prevented
277282
var redirect = { to: to, toParams: toParams, options: options };
278283
evt = $rootScope.$broadcast('$stateNotFound', redirect, from.self, fromParams);
279-
if (evt.defaultPrevented) return TransitionAborted;
284+
if (evt.defaultPrevented) {
285+
syncUrl();
286+
return TransitionAborted;
287+
}
280288

281289
// Allow the handler to return a promise to defer state lookup retry
282290
if (evt.retry) {
283-
if (options.$retry) return TransitionFailed;
291+
if (options.$retry) {
292+
syncUrl();
293+
return TransitionFailed;
294+
}
284295
var retryTransition = $state.transition = $q.when(evt.retry);
285296
retryTransition.then(function() {
286297
if (retryTransition !== $state.transition) return TransitionSuperseded;
@@ -290,6 +301,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
290301
function() {
291302
return TransitionAborted;
292303
});
304+
syncUrl();
293305
return retryTransition;
294306
}
295307

@@ -323,6 +335,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
323335
// TODO: We may not want to bump 'transition' if we're called from a location change that we've initiated ourselves,
324336
// because we might accidentally abort a legitimate transition initiated from code?
325337
if (to === from && locals === from.locals) {
338+
syncUrl();
326339
$state.transition = null;
327340
return $q.when($state.current);
328341
}
@@ -333,7 +346,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
333346
// Broadcast start event and cancel the transition if requested
334347
if (options.notify) {
335348
evt = $rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams);
336-
if (evt.defaultPrevented) return TransitionPrevented;
349+
if (evt.defaultPrevented) {
350+
syncUrl();
351+
return TransitionPrevented;
352+
}
337353
}
338354

339355
// Resolve locals for the remaining states, but don't update any global state just
@@ -399,13 +415,15 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
399415
if (options.notify) {
400416
$rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams);
401417
}
418+
currentLocation = $location.url();
402419

403420
return $state.current;
404421
}, function (error) {
405422
if ($state.transition !== transition) return TransitionSuperseded;
406423

407424
$state.transition = null;
408425
$rootScope.$broadcast('$stateChangeError', to.self, toParams, from.self, fromParams, error);
426+
syncUrl();
409427

410428
return $q.reject(error);
411429
});

test/stateSpec.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ describe('state', function () {
7373
$state.transitionTo("about");
7474
}
7575
})
76+
.state('resolveFail', {
77+
url: "/resolve-fail",
78+
resolve: {
79+
badness: function($q) {
80+
return $q.reject("!");
81+
}
82+
}
83+
})
7684

7785
.state('first', { url: '^/first/subpath' })
7886
.state('second', { url: '^/second' });
@@ -614,6 +622,7 @@ describe('state', function () {
614622
'home',
615623
'home.item',
616624
'home.redirect',
625+
'resolveFail',
617626
'second'
618627
];
619628
expect(list.map(function(state) { return state.name; })).toEqual(names);
@@ -654,6 +663,17 @@ describe('state', function () {
654663
expect($state.current.name).toBe('');
655664
}));
656665

666+
it('should revert to last known working url on state change failure', inject(function ($state, $rootScope, $location, $q) {
667+
$state.transitionTo("about");
668+
$q.flush();
669+
670+
$location.path("/resolve-fail");
671+
$rootScope.$broadcast("$locationChangeSuccess");
672+
$rootScope.$apply();
673+
674+
expect($state.current.name).toBe("about");
675+
}));
676+
657677
it('should replace browser history when "replace" enabled', inject(function ($state, $rootScope, $location, $q) {
658678
var originalReplaceFn = $location.replace, replaceWasCalled = false;
659679

0 commit comments

Comments
 (0)