From 4fcccd8e0aaec3eff99851feff6a0e74630f024d Mon Sep 17 00:00:00 2001 From: Chris Thielen Date: Tue, 16 Aug 2016 15:39:29 -0500 Subject: [PATCH] fix(defaultErrorHandler): Invoke handler when a transition is Canceled. Closes #2924 --- src/common/common.ts | 7 +++++-- src/state/stateService.ts | 9 ++++++++- src/transition/interface.ts | 28 +++++++++++++++++++++++----- test/ng1/stateEventsSpec.js | 1 + test/ng1/stateSpec.js | 1 + test/ng1/viewHookSpec.ts | 2 ++ 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index e9e9dbab4..239988d6d 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -381,14 +381,17 @@ export const unnestR = (memo: any[], elem: any[]) => memo.concat(elem); export const flattenR = (memo: any[], elem: any) => isArray(elem) ? memo.concat(elem.reduce(flattenR, [])) : pushR(memo, elem); -/** Reduce function that pushes an object to an array, then returns the array. Mostly just for [[flattenR]] */ +/** + * Reduce function that pushes an object to an array, then returns the array. + * Mostly just for [[flattenR]] and [[uniqR]] + */ export function pushR(arr: any[], obj: any) { arr.push(obj); return arr; } /** Reduce function that filters out duplicates */ -export const uniqR = (acc: any[], token: any) => +export const uniqR = (acc: T[], token: T): T[] => inArray(acc, token) ? acc : pushR(acc, token); /** diff --git a/src/state/stateService.ts b/src/state/stateService.ts index ae59a1bfd..7eed7d5e7 100644 --- a/src/state/stateService.ts +++ b/src/state/stateService.ts @@ -291,18 +291,21 @@ export class StateService { const rejectedTransitionHandler = (transition: Transition) => (error: any): Promise => { if (error instanceof Rejection) { if (error.type === RejectType.IGNORED) { + // Consider ignored `Transition.run()` as a successful `transitionTo` router.urlRouter.update(); return services.$q.when(globals.current); } if (error.type === RejectType.SUPERSEDED && error.redirected && error.detail instanceof TargetState) { + // If `Transition.run()` was redirected, allow the `transitionTo()` promise to resolve successfully + // by returning the promise for the new (redirect) `Transition.run()`. let redirect: Transition = transition.redirect(error.detail); return redirect.run().catch(rejectedTransitionHandler(redirect)); } if (error.type === RejectType.ABORTED) { router.urlRouter.update(); - return services.$q.reject(error); + // Fall through to default error handler } } @@ -501,6 +504,10 @@ export class StateService { * The error handler is called when a [[Transition]] is rejected or when any error occurred during the Transition. * This includes errors caused by resolves and transition hooks. * + * Note: + * This handler does not receive certain Transition rejections. + * Redirected and Ignored Transitions are not considered to be errors by [[StateService.transitionTo]]. + * * The built-in default error handler logs the error to the console. * * You can provide your own custom handler. diff --git a/src/transition/interface.ts b/src/transition/interface.ts index 89edd8d96..6d4fa6a8e 100644 --- a/src/transition/interface.ts +++ b/src/transition/interface.ts @@ -624,9 +624,9 @@ export interface IHookRegistry { onSuccess(matchCriteria: HookMatchCriteria, callback: TransitionHookFn, options?: HookRegOptions): Function; /** - * Registers a [[TransitionHookFn]], called after a successful transition completed. + * Registers a [[TransitionHookFn]], called after a transition has errored. * - * Registers a transition lifecycle hook, which is invoked after a transition successfully completes. + * Registers a transition lifecycle hook, which is invoked after a transition has been rejected for any reason. * * See [[TransitionHookFn]] for the signature of the function. * @@ -634,11 +634,29 @@ export interface IHookRegistry { * * ### Lifecycle * - * `onError` hooks are chained off the Transition's promise (see [[Transition.promise]]). - * If the Transition fails and its promise is rejected, then the `onError` hooks are invoked. - * Since these hooks are run after the transition is over, their return value is ignored. + * The `onError` hooks are chained off the Transition's promise (see [[Transition.promise]]). + * If a Transition fails, its promise is rejected and the `onError` hooks are invoked. * The `onError` hooks are invoked in priority order. * + * Since these hooks are run after the transition is over, their return value is ignored. + * + * A transition "errors" if it was started, but failed to complete (for any reason). + * A *non-exhaustive list* of reasons a transition can error: + * + * - A transition was cancelled because a new transition started while it was still running + * - A transition was cancelled by a Transition Hook returning false + * - A transition was redirected by a Transition Hook returning a [[TargetState]] + * - A transition was invalid because the target state/parameters are not valid + * - A transition was ignored because the target state/parameters are exactly the current state/parameters + * - A Transition Hook or resolve function threw an error + * - A Transition Hook returned a rejected promise + * - A resolve function returned a rejected promise + * + * To check the failure reason, inspect the return value of [[Transition.error]]. + * + * Note: `onError` should be used for targeted error handling, or error recovery. + * For simple catch-all error reporting, use [[StateService.defaultErrorHandler]]. + * * ### Return value * * Since the Transition is already completed, the hook's return value is ignored diff --git a/test/ng1/stateEventsSpec.js b/test/ng1/stateEventsSpec.js index b824644f8..26a87f0cc 100644 --- a/test/ng1/stateEventsSpec.js +++ b/test/ng1/stateEventsSpec.js @@ -95,6 +95,7 @@ describe('UI-Router v0.2.x $state events', function () { })); it('can be cancelled by preventDefault() in $stateChangeStart', inject(function ($state, $q, $rootScope) { + $state.defaultErrorHandler(function() {}); initStateTo(A); var called; $rootScope.$on('$stateChangeStart', function (ev) { diff --git a/test/ng1/stateSpec.js b/test/ng1/stateSpec.js index 2a2faa965..3b1784413 100644 --- a/test/ng1/stateSpec.js +++ b/test/ng1/stateSpec.js @@ -2000,6 +2000,7 @@ describe('transition hook', function() { // Test for #2611 it("aborts should reset the URL to the prevous state's", inject(function($transitions, $q, $state, $location) { + $state.defaultErrorHandler(function() {}); $q.flush(); $transitions.onStart({ to: 'home.foo' }, function() { return false; }); $location.path('/home/foo'); $q.flush(); diff --git a/test/ng1/viewHookSpec.ts b/test/ng1/viewHookSpec.ts index 67b60c373..bc5a191d6 100644 --- a/test/ng1/viewHookSpec.ts +++ b/test/ng1/viewHookSpec.ts @@ -61,6 +61,7 @@ describe("view hooks", () => { }; it("can cancel a transition that would exit the view's state by returning false", () => { + $state.defaultErrorHandler(function() {}); ctrl.prototype.uiCanExit = function() { log += "canexit;"; return false; }; initial(); @@ -112,6 +113,7 @@ describe("view hooks", () => { })); it("can wait for a promise and then reject the transition", inject(($timeout) => { + $state.defaultErrorHandler(function() {}); ctrl.prototype.uiCanExit = function() { log += "canexit;"; return $timeout(() => { log += "delay;"; return false; }, 1000);