diff --git a/src/hooks/ignoredTransition.ts b/src/hooks/ignoredTransition.ts new file mode 100644 index 00000000..3ddd11e2 --- /dev/null +++ b/src/hooks/ignoredTransition.ts @@ -0,0 +1,23 @@ + +import { trace } from '../common/trace'; +import { Rejection } from '../transition/rejectFactory'; +import { TransitionService } from '../transition/transitionService'; +import { Transition } from '../transition/transition'; + +/** + * A [[TransitionHookFn]] that skips a transition if it should be ignored + * + * This hook is invoked at the end of the onBefore phase. + * + * If the transition should be ignored (because no parameter or states changed) + * then the transition is ignored and not processed. + */ +function ignoredHook(trans: Transition) { + if (trans.ignored()) { + trace.traceTransitionIgnored(this); + return Rejection.ignored().toPromise(); + } +} + +export const registerIgnoredTransitionHook = (transitionService: TransitionService) => + transitionService.onBefore({}, ignoredHook, { priority: -9999 }); diff --git a/src/hooks/invalidTransition.ts b/src/hooks/invalidTransition.ts new file mode 100644 index 00000000..d4d87dab --- /dev/null +++ b/src/hooks/invalidTransition.ts @@ -0,0 +1,18 @@ +import { TransitionService } from '../transition/transitionService'; +import { Transition } from '../transition/transition'; + +/** + * A [[TransitionHookFn]] that rejects the Transition if it is invalid + * + * This hook is invoked at the end of the onBefore phase. + * If the transition is invalid (for example, param values do not validate) + * then the transition is rejected. + */ +function invalidTransitionHook(trans: Transition) { + if (!trans.valid()) { + throw new Error(trans.error()); + } +} + +export const registerInvalidTransitionHook = (transitionService: TransitionService) => + transitionService.onBefore({}, invalidTransitionHook, { priority: -10000 }); diff --git a/src/transition/interface.ts b/src/transition/interface.ts index 1ab5f3f9..fcb425a2 100644 --- a/src/transition/interface.ts +++ b/src/transition/interface.ts @@ -816,5 +816,5 @@ export interface PathType { */ export type HookMatchCriterion = (string|IStateMatch|boolean) -export enum TransitionHookPhase { CREATE, BEFORE, ASYNC, SUCCESS, ERROR } +export enum TransitionHookPhase { CREATE, BEFORE, RUN, SUCCESS, ERROR } export enum TransitionHookScope { TRANSITION, STATE } diff --git a/src/transition/transition.ts b/src/transition/transition.ts index 2d0f2c25..aec488a2 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -1,41 +1,35 @@ /** * @coreapi * @module transition - */ /** for typedoc */ -import {stringify} from "../common/strings"; -import {trace} from "../common/trace"; -import {services} from "../common/coreservices"; + */ +/** for typedoc */ +import { trace } from '../common/trace'; +import { services } from '../common/coreservices'; import { - map, find, extend, mergeR, tail, - omit, toJson, arrayTuples, unnestR, identity, anyTrueR -} from "../common/common"; -import { isObject, isArray } from "../common/predicates"; -import { prop, propEq, val, not, is } from "../common/hof"; - -import {StateDeclaration, StateOrName} from "../state/interface"; + map, find, extend, mergeR, tail, omit, toJson, arrayTuples, unnestR, identity, anyTrueR +} from '../common/common'; +import { isObject } from '../common/predicates'; +import { prop, propEq, val, not, is } from '../common/hof'; +import { StateDeclaration, StateOrName } from '../state/interface'; import { - TransitionOptions, TreeChanges, IHookRegistry, TransitionHookPhase, - RegisteredHooks, HookRegOptions, HookMatchCriteria -} from "./interface"; - -import { TransitionStateHookFn, TransitionHookFn } from "./interface"; // has or is using - -import {TransitionHook} from "./transitionHook"; -import {matchState, makeEvent, RegisteredHook} from "./hookRegistry"; -import {HookBuilder} from "./hookBuilder"; -import {PathNode} from "../path/node"; -import {PathFactory} from "../path/pathFactory"; -import {State} from "../state/stateObject"; -import {TargetState} from "../state/targetState"; -import {Param} from "../params/param"; -import {Resolvable} from "../resolve/resolvable"; -import {ViewConfig} from "../view/interface"; -import {Rejection} from "./rejectFactory"; -import {ResolveContext} from "../resolve/resolveContext"; -import {UIRouter} from "../router"; -import {UIInjector} from "../interface"; -import {RawParams} from "../params/interface"; -import { ResolvableLiteral } from "../resolve/interface"; + TransitionOptions, TreeChanges, IHookRegistry, TransitionHookPhase, RegisteredHooks, HookRegOptions, + HookMatchCriteria, TransitionStateHookFn, TransitionHookFn +} from './interface'; // has or is using +import { TransitionHook } from './transitionHook'; +import { matchState, makeEvent, RegisteredHook } from './hookRegistry'; +import { HookBuilder } from './hookBuilder'; +import { PathNode } from '../path/node'; +import { PathFactory } from '../path/pathFactory'; +import { State } from '../state/stateObject'; +import { TargetState } from '../state/targetState'; +import { Param } from '../params/param'; +import { Resolvable } from '../resolve/resolvable'; +import { ViewConfig } from '../view/interface'; +import { ResolveContext } from '../resolve/resolveContext'; +import { UIRouter } from '../router'; +import { UIInjector } from '../interface'; +import { RawParams } from '../params/interface'; +import { ResolvableLiteral } from '../resolve/interface'; /** @hidden */ const stateSelf: (_state: State) => StateDeclaration = prop("self"); @@ -630,28 +624,6 @@ export class Transition implements IHookRegistry { let globals = this.router.globals; globals.transitionHistory.enqueue(this); - let onBeforeHooks = hookBuilder.buildHooksForPhase(TransitionHookPhase.BEFORE); - let syncResult = TransitionHook.runOnBeforeHooks(onBeforeHooks); - - if (Rejection.isTransitionRejectionPromise(syncResult)) { - syncResult.catch(() => 0); // issue #2676 - let rejectReason = ( syncResult)._transitionRejection; - this._deferred.reject(rejectReason); - return this.promise; - } - - if (!this.valid()) { - let error = new Error(this.error()); - this._deferred.reject(error); - return this.promise; - } - - if (this.ignored()) { - trace.traceTransitionIgnored(this); - this._deferred.reject(Rejection.ignored()); - return this.promise; - } - // When the chain is complete, then resolve or reject the deferred const transitionSuccess = () => { trace.traceSuccess(this.$to(), this); @@ -670,16 +642,16 @@ export class Transition implements IHookRegistry { runAllHooks(onErrorHooks); }; - trace.traceTransitionStart(this); - - // Chain the next hook off the previous - const appendHookToChain = (prev: Promise, nextHook: TransitionHook) => - prev.then(() => nextHook.invokeHook()); - - // Run the hooks, then resolve or reject the overall deferred in the .then() handler - let asyncHooks = hookBuilder.buildHooksForPhase(TransitionHookPhase.ASYNC); + // Builds a chain of transition hooks for the given phase + // Each hook is invoked after the previous one completes + const chainFor = (phase: TransitionHookPhase) => + TransitionHook.chain(hookBuilder.buildHooksForPhase(phase)); - asyncHooks.reduce(appendHookToChain, syncResult) + services.$q.when() + .then(() => chainFor(TransitionHookPhase.BEFORE)) + // This waits to build the RUN hook chain until after the "BEFORE" hooks complete + // This allows a BEFORE hook to dynamically add RUN hooks via the Transition object. + .then(() => chainFor(TransitionHookPhase.RUN)) .then(transitionSuccess, transitionError); return this.promise; diff --git a/src/transition/transitionHook.ts b/src/transition/transitionHook.ts index c8361ffa..b3e070ea 100644 --- a/src/transition/transitionHook.ts +++ b/src/transition/transitionHook.ts @@ -143,34 +143,36 @@ export class TransitionHook { } /** - * Run all TransitionHooks, ignoring their return value. + * Chains together an array of TransitionHooks. + * + * Given a list of [[TransitionHook]] objects, chains them together. + * Each hook is invoked after the previous one completes. + * + * #### Example: + * ```js + * var hooks: TransitionHook[] = getHooks(); + * let promise: Promise = TransitionHook.chain(hooks); + * + * promise.then(handleSuccess, handleError); + * ``` + * + * @param hooks the list of hooks to chain together + * @param waitFor if provided, the chain is `.then()`'ed off this promise + * @returns a `Promise` for sequentially invoking the hooks (in order) */ - static runAllHooks(hooks: TransitionHook[]): void { - hooks.forEach(hook => hook.invokeHook()); + static chain(hooks: TransitionHook[], waitFor?: Promise): Promise { + // Chain the next hook off the previous + const createHookChainR = (prev: Promise, nextHook: TransitionHook) => + prev.then(() => nextHook.invokeHook()); + return hooks.reduce(createHookChainR, waitFor || services.$q.when()); } + /** - * Given an array of TransitionHooks, runs each one synchronously and sequentially. - * Should any hook return a Rejection synchronously, the remaining hooks will not run. - * - * Returns a promise chain composed of any promises returned from each hook.invokeStep() call + * Run all TransitionHooks, ignoring their return value. */ - static runOnBeforeHooks(hooks: TransitionHook[]): Promise { - let results: Promise[] = []; - - for (let hook of hooks) { - let hookResult = hook.invokeHook(); - - if (Rejection.isTransitionRejectionPromise(hookResult)) { - // Break on first thrown error or false/TargetState - return hookResult; - } - - results.push(hookResult); - } - - return results - .filter(isPromise) - .reduce((chain: Promise, promise: Promise) => chain.then(val(promise)), services.$q.when()); + static runAllHooks(hooks: TransitionHook[]): void { + hooks.forEach(hook => hook.invokeHook()); } + } \ No newline at end of file diff --git a/src/transition/transitionService.ts b/src/transition/transitionService.ts index 7f0e1550..d31d1805 100644 --- a/src/transition/transitionService.ts +++ b/src/transition/transitionService.ts @@ -27,6 +27,8 @@ import { isDefined } from "../common/predicates"; import { removeFrom, values, createProxyFunctions } from "../common/common"; import { Disposable } from "../interface"; // has or is using import { val } from "../common/hof"; +import { registerIgnoredTransitionHook } from '../hooks/ignoredTransition'; +import { registerInvalidTransitionHook } from '../hooks/invalidTransition'; /** * The default [[Transition]] options. @@ -170,6 +172,8 @@ export class TransitionService implements IHookRegistry, Disposable { */ _deregisterHookFns: { addCoreResolves: Function; + ignored: Function; + invalid: Function; redirectTo: Function; onExit: Function; onRetain: Function; @@ -196,9 +200,8 @@ export class TransitionService implements IHookRegistry, Disposable { 'getHooks', ]); - this._defineDefaultPaths(); - this._defineDefaultEvents(); - + this._defineCorePaths(); + this._defineCoreEvents(); this._registerCoreTransitionHooks(); } @@ -228,7 +231,7 @@ export class TransitionService implements IHookRegistry, Disposable { } /** @hidden */ - private _defineDefaultEvents() { + private _defineCoreEvents() { const Phase = TransitionHookPhase; const TH = TransitionHook; const paths = this._criteriaPaths; @@ -237,18 +240,18 @@ export class TransitionService implements IHookRegistry, Disposable { this._defineEvent("onBefore", Phase.BEFORE, 0, paths.to, false, TH.HANDLE_RESULT); - this._defineEvent("onStart", Phase.ASYNC, 0, paths.to); - this._defineEvent("onExit", Phase.ASYNC, 100, paths.exiting, true); - this._defineEvent("onRetain", Phase.ASYNC, 200, paths.retained); - this._defineEvent("onEnter", Phase.ASYNC, 300, paths.entering); - this._defineEvent("onFinish", Phase.ASYNC, 400, paths.to); + this._defineEvent("onStart", Phase.RUN, 0, paths.to); + this._defineEvent("onExit", Phase.RUN, 100, paths.exiting, true); + this._defineEvent("onRetain", Phase.RUN, 200, paths.retained); + this._defineEvent("onEnter", Phase.RUN, 300, paths.entering); + this._defineEvent("onFinish", Phase.RUN, 400, paths.to); this._defineEvent("onSuccess", Phase.SUCCESS, 0, paths.to, false, TH.IGNORE_RESULT, TH.LOG_ERROR, false); this._defineEvent("onError", Phase.ERROR, 0, paths.to, false, TH.IGNORE_RESULT, TH.LOG_ERROR, false); } /** @hidden */ - private _defineDefaultPaths() { + private _defineCorePaths() { const { STATE, TRANSITION } = TransitionHookScope; this._definePathType("to", TRANSITION); @@ -318,6 +321,8 @@ export class TransitionService implements IHookRegistry, Disposable { let fns = this._deregisterHookFns; fns.addCoreResolves = registerAddCoreResolvables(this); + fns.ignored = registerIgnoredTransitionHook(this); + fns.invalid = registerInvalidTransitionHook(this); // Wire up redirectTo hook fns.redirectTo = registerRedirectToHook(this);