Skip to content

Commit 3141a8f

Browse files
feat(transition): Improve supersede logic: Do not supersede if the new trans is aborted before onStart
Previously, any calls to state.go would supersede the running transition. Now, if the new transition is aborted before it reaches "run" phase (onStart hook), it will not cause the running transition to be aborted. Closes #41
1 parent da0bdfe commit 3141a8f

File tree

6 files changed

+59
-29
lines changed

6 files changed

+59
-29
lines changed

src/globals.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,14 @@ export class UIRouterGlobals implements Disposable {
4141
$current: StateObject;
4242

4343
/**
44-
* The current transition (in progress)
44+
* The current started/running transition.
45+
* This transition has reached at least the onStart phase, but is not yet complete
4546
*/
4647
transition: Transition;
4748

49+
/** @internalapi */
50+
lastStartedTransitionId: number = -1;
51+
4852
/** @internalapi */
4953
transitionHistory = new Queue<Transition>([], 1);
5054

src/hooks/updateGlobals.ts

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
/** @module hooks */ /** for typedoc */
2-
import { Transition } from "../transition/transition";
3-
import { copy } from "../common/common";
4-
import { TransitionService } from "../transition/transitionService";
1+
/** @module hooks */
2+
/** for typedoc */
3+
import { Transition } from '../transition/transition';
4+
import { copy } from '../common/common';
5+
import { TransitionService } from '../transition/transitionService';
56

67
/**
78
* A [[TransitionHookFn]] which updates global UI-Router state
@@ -17,25 +18,23 @@ import { TransitionService } from "../transition/transitionService";
1718
*/
1819
const updateGlobalState = (trans: Transition) => {
1920
let globals = trans.router.globals;
20-
globals.transition = trans;
21-
globals.transitionHistory.enqueue(trans);
2221

23-
const updateGlobalState = () => {
22+
const transitionSuccessful = () => {
2423
globals.successfulTransitions.enqueue(trans);
2524
globals.$current = trans.$to();
2625
globals.current = globals.$current.self;
26+
2727
copy(trans.params(), globals.params);
2828
};
2929

30-
trans.onSuccess({}, updateGlobalState, {priority: 10000});
31-
3230
const clearCurrentTransition = () => {
3331
// Do not clear globals.transition if a different transition has started in the meantime
3432
if (globals.transition === trans) globals.transition = null;
3533
};
3634

35+
trans.onSuccess({}, transitionSuccessful, { priority: 10000 });
3736
trans.promise.then(clearCurrentTransition, clearCurrentTransition);
3837
};
3938

4039
export const registerUpdateGlobalState = (transitionService: TransitionService) =>
41-
transitionService.onBefore({}, updateGlobalState);
40+
transitionService.onCreate({}, updateGlobalState);

src/state/stateService.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,10 @@ export class StateService {
309309
transitionTo(to: StateOrName, toParams: RawParams = {}, options: TransitionOptions = {}): TransitionPromise {
310310
let router = this.router;
311311
let globals = router.globals;
312-
let transHistory = globals.transitionHistory;
313312
options = defaults(options, defaultTransOpts);
314-
options = extend(options, { current: transHistory.peekTail.bind(transHistory)});
313+
const getCurrent = () =>
314+
globals.transition;
315+
options = extend(options, { current: getCurrent });
315316

316317
let ref: TargetState = this.target(to, toParams, options);
317318
let currentPath = this.getCurrentPath();

src/transition/transition.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -618,8 +618,6 @@ export class Transition implements IHookRegistry {
618618
*/
619619
run(): Promise<any> {
620620
let runAllHooks = TransitionHook.runAllHooks;
621-
let globals = this.router.globals;
622-
globals.transitionHistory.enqueue(this);
623621

624622
// Gets transition hooks array for the given phase
625623
const hooksFor = (phase: TransitionHookPhase) =>
@@ -630,6 +628,16 @@ export class Transition implements IHookRegistry {
630628
const chainFor = (phase: TransitionHookPhase) =>
631629
TransitionHook.chain(hooksFor(phase));
632630

631+
const startTransition = () => {
632+
let globals = this.router.globals;
633+
634+
globals.lastStartedTransitionId = this.$id;
635+
globals.transition = this;
636+
globals.transitionHistory.enqueue(this);
637+
638+
trace.traceTransitionStart(this);
639+
};
640+
633641
// When the chain is complete, then resolve or reject the deferred
634642
const transitionSuccess = () => {
635643
trace.traceSuccess(this.$to(), this);
@@ -648,7 +656,7 @@ export class Transition implements IHookRegistry {
648656

649657
services.$q.when()
650658
.then(() => chainFor(TransitionHookPhase.BEFORE))
651-
.then(() => trace.traceTransitionStart(this))
659+
.then(startTransition)
652660
// This waits to build the RUN hook chain until after the "BEFORE" hooks complete
653661
// This allows a BEFORE hook to dynamically add RUN hooks via the Transition object.
654662
.then(() => chainFor(TransitionHookPhase.RUN))
@@ -657,10 +665,9 @@ export class Transition implements IHookRegistry {
657665
return this.promise;
658666
}
659667

660-
/**
661-
* Checks if this transition is currently active/running.
662-
*/
663-
isActive = () => this === this._options.current();
668+
/** Checks if this transition is currently active/running. */
669+
isActive = () =>
670+
this.router.globals.transition === this;
664671

665672
/**
666673
* Checks if the Transition is valid

src/transition/transitionHook.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @module transition
44
*/
55
/** for typedoc */
6-
import { TransitionHookOptions, HookResult } from './interface';
6+
import { TransitionHookOptions, HookResult, TransitionHookPhase } from './interface';
77
import { defaults, noop, silentRejection } from '../common/common';
88
import { fnToString, maxLength } from '../common/strings';
99
import { isPromise } from '../common/predicates';
@@ -73,7 +73,7 @@ export class TransitionHook {
7373
};
7474

7575
private isSuperseded = () =>
76-
!this.type.synchronous && this.options.current() !== this.options.transition;
76+
this.type.hookPhase === TransitionHookPhase.RUN && !this.options.transition.isActive();
7777

7878
logError(err): any {
7979
this.transition.router.stateService.defaultErrorHandler()(err);

test/stateServiceSpec.ts

+26-7
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,8 @@ describe('stateService', function () {
241241
});
242242

243243
afterEach((done) => {
244-
if (router.globals.transition) {
245-
router.globals.transition.promise.then(done, done)
246-
} else {
247-
done();
248-
}
244+
router.dispose();
245+
done();
249246
});
250247

251248
describe('[ transition.dynamic() ]:', function() {
@@ -545,12 +542,15 @@ describe('stateService', function () {
545542
done();
546543
});
547544

548-
it('aborts pending transitions when superseded from callbacks', async(done) => {
545+
fit('aborts pending transitions when superseded from callbacks', async(done) => {
546+
// router.trace.enable();
549547
$state.defaultErrorHandler(() => null);
550548
$registry.register({
551549
name: 'redir',
552550
url: "redir",
553-
onEnter: (trans) => { trans.router.stateService.go('A') }
551+
onEnter: (trans) => {
552+
trans.router.stateService.go('A')
553+
}
554554
});
555555
let result = await $state.transitionTo('redir').catch(err => err);
556556
await router.globals.transition.promise;
@@ -561,6 +561,25 @@ describe('stateService', function () {
561561
done();
562562
});
563563

564+
it('does not abort pending transition when a new transition is cancelled by onBefore hook', (done) => {
565+
router.transitionService.onBefore({}, (t) => {
566+
if (t.$id === 1) return false;
567+
return (new Promise(resolve => setTimeout(resolve, 100))) as any;
568+
});
569+
570+
let promise1 = $state.transitionTo('A'); // takes 100 ms
571+
let promise2 = $state.transitionTo('A'); // is cancelled
572+
let promise2Error;
573+
574+
promise1.then(() => {
575+
expect($state.current.name).toBe('A');
576+
expect(promise2Error).toBeDefined();
577+
done();
578+
});
579+
580+
promise2.catch((err) => promise2Error = err);
581+
});
582+
564583
it('triggers onEnter and onExit callbacks', async(done) => {
565584
let log = "";
566585
await initStateTo(A);

0 commit comments

Comments
 (0)