Skip to content

Commit dbc4d63

Browse files
committed
fix(lazyLoad): abort URL-sourced transition when lazy loaded states do not match the URL
When a URL matches a future state's prefix but nothing in the lazy loaded module matches the full URL, retryTransition() delegates to urlService.sync() but returned undefined, letting the original transition resume and activate the deregistered .** placeholder, superseding the otherwise() transition started by sync(). Return false after sync() to abort the original transition so the sync()-initiated transition (e.g. the otherwise() rule) can complete. Closes ui-router#850
1 parent c647c9c commit dbc4d63

2 files changed

Lines changed: 57 additions & 2 deletions

File tree

src/hooks/lazyLoad.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ const lazyLoadHook: TransitionHookFn = (transition: Transition) => {
5454
return router.stateService.target(state, params, transition.options());
5555
}
5656

57-
// No matching state found, so let .sync() choose the best non-state match/otherwise
57+
// No matching state found, so let .sync() choose the best non-state match/otherwise.
58+
// Abort the current transition; otherwise it continues towards the (now deregistered)
59+
// future state placeholder and supersedes the transition started by sync().
5860
router.urlService.sync();
61+
return false;
5962
}
6063

6164
const promises = transition

test/lazyLoadSpec.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UIRouter, TransitionService, StateService } from '../src/index';
1+
import { UIRouter, TransitionService, StateService, RejectType, Rejection } from '../src/index';
22

33
import { StateRegistry } from '../src/state/stateRegistry';
44
import { UrlRouter } from '../src/url/urlRouter';
@@ -298,6 +298,58 @@ describe('future state', function() {
298298
});
299299
});
300300

301+
describe('triggered by a URL sync that the lazy loaded states do not match', () => {
302+
let lazyLoadCount: number;
303+
304+
beforeEach(() => {
305+
lazyLoadCount = 0;
306+
$state.defaultErrorHandler(() => {});
307+
$registry.register({ name: 'notFound' });
308+
$registry.register({
309+
name: 'A.**',
310+
url: '/a',
311+
lazyLoad: () => {
312+
lazyLoadCount++;
313+
return Promise.resolve({ states: [{ name: 'A', url: '/a/:id' }] });
314+
},
315+
});
316+
});
317+
318+
// Test for https://github.com/ui-router/core/issues/850
319+
it('should activate the otherwise() rule instead of the deregistered placeholder', (done) => {
320+
router.urlService.rules.otherwise({ state: 'notFound' });
321+
322+
router.urlService.url('/a/no/such/page');
323+
router.urlService.sync();
324+
325+
$transitions.onSuccess({}, () => {
326+
expect(lazyLoadCount).toBe(1);
327+
expect($state.current.name).toBe('notFound');
328+
expect($registry.get('A.**')).toBeFalsy();
329+
done();
330+
});
331+
});
332+
333+
it('should abort the original transition to the deregistered placeholder', (done) => {
334+
router.urlService.rules.otherwise({ state: 'notFound' });
335+
const errors: { targetName: string; error: Rejection }[] = [];
336+
$transitions.onError({}, (trans) => {
337+
errors.push({ targetName: trans.targetState().name(), error: trans.error() });
338+
});
339+
340+
// The url change is synced by the url listener
341+
router.urlService.url('/a/no/such/page');
342+
343+
$transitions.onSuccess({}, () => {
344+
expect(errors.length).toBe(1);
345+
expect(errors[0].targetName).toBe('A.**');
346+
expect(errors[0].error.type).toBe(RejectType.ABORTED);
347+
expect($state.current.name).toBe('notFound');
348+
done();
349+
});
350+
});
351+
});
352+
301353
describe('that resolves to multiple states', () => {
302354
let futureA, futureB;
303355
const lazyA = { name: 'A', url: '/a/:id', params: { id: 'default' } };

0 commit comments

Comments
 (0)