Skip to content

Commit

Permalink
feat(HookBuilder): Allow custom hook types (to be defined by a plugin)
Browse files Browse the repository at this point in the history
  • Loading branch information
christopherthielen committed Nov 29, 2016
1 parent f0dcdce commit 3f146e6
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 136 deletions.
73 changes: 31 additions & 42 deletions src/transition/hookBuilder.ts
Expand Up @@ -3,14 +3,17 @@
import {extend, tail, assertPredicate, unnestR, identity} from "../common/common";
import {isArray} from "../common/predicates";

import {TransitionOptions, TransitionHookOptions, IHookRegistry, TreeChanges, IEventHook, IMatchingNodes} from "./interface";
import {
TransitionOptions, TransitionHookOptions, IHookRegistry, TreeChanges, IEventHook, IMatchingNodes,
TransitionHookPhase, TransitionHookScope
} from "./interface";

import {Transition} from "./transition";
import {TransitionHook} from "./transitionHook";
import {State} from "../state/stateObject";
import {PathNode} from "../path/node";
import {TransitionService} from "./transitionService";
import {ResolveContext} from "../resolve/resolveContext";
import {TransitionHookType} from "./transitionHookType";

/**
* This class returns applicable TransitionHooks for a specific Transition instance.
Expand All @@ -28,37 +31,32 @@ import {ResolveContext} from "../resolve/resolveContext";
*/
export class HookBuilder {

private $transitions: TransitionService;
private baseHookOptions: TransitionHookOptions;

treeChanges: TreeChanges;
transitionOptions: TransitionOptions;

toState: State;
fromState: State;

constructor(private $transitions: TransitionService, private transition: Transition, private baseHookOptions: TransitionHookOptions) {
constructor(private transition: Transition) {
this.treeChanges = transition.treeChanges();
this.transitionOptions = transition.options();
this.toState = tail(this.treeChanges.to).state;
this.fromState = tail(this.treeChanges.from).state;
this.transitionOptions = transition.options();
this.$transitions = transition.router.transitionService;
this.baseHookOptions = <TransitionHookOptions> {
transition: transition,
current: transition.options().current
};
}

getOnBeforeHooks = () => this._buildNodeHooks("onBefore", "to", tupleSort());
getOnStartHooks = () => this._buildNodeHooks("onStart", "to", tupleSort());
getOnExitHooks = () => this._buildNodeHooks("onExit", "exiting", tupleSort(true), { stateHook: true });
getOnRetainHooks = () => this._buildNodeHooks("onRetain", "retained", tupleSort(false), { stateHook: true });
getOnEnterHooks = () => this._buildNodeHooks("onEnter", "entering", tupleSort(false), { stateHook: true });
getOnFinishHooks = () => this._buildNodeHooks("onFinish", "to", tupleSort());
getOnSuccessHooks = () => this._buildNodeHooks("onSuccess", "to", tupleSort(), { rejectIfSuperseded: false });
getOnErrorHooks = () => this._buildNodeHooks("onError", "to", tupleSort(), { rejectIfSuperseded: false });

asyncHooks() {
let onStartHooks = this.getOnStartHooks();
let onExitHooks = this.getOnExitHooks();
let onRetainHooks = this.getOnRetainHooks();
let onEnterHooks = this.getOnEnterHooks();
let onFinishHooks = this.getOnFinishHooks();

let asyncHooks = [onStartHooks, onExitHooks, onRetainHooks, onEnterHooks, onFinishHooks];
return asyncHooks.reduce(unnestR, []).filter(identity);
buildHooksForPhase(phase: TransitionHookPhase): TransitionHook[] {
return this.$transitions.getTransitionHookTypes(phase)
.map(type => this.buildHooks(type))
.reduce(unnestR, [])
.filter(identity);
}

/**
Expand All @@ -68,44 +66,35 @@ export class HookBuilder {
* - Finds [[PathNode]] (or `PathNode[]`) to use as the TransitionHook context(s)
* - For each of the [[PathNode]]s, creates a TransitionHook
*
* @param hookType the name of the hook registration function, e.g., 'onEnter', 'onFinish'.
* @param matchingNodesProp selects which [[PathNode]]s from the [[IMatchingNodes]] object to create hooks for.
* @param getLocals a function which accepts a [[PathNode]] and returns additional locals to provide to the hook as injectables
* @param sortHooksFn a function which compares two HookTuple and returns <1, 0, or >1
* @param options any specific Transition Hook Options
* @param hookType the type of the hook registration function, e.g., 'onEnter', 'onFinish'.
*/
private _buildNodeHooks(hookType: string,
matchingNodesProp: string,
sortHooksFn: (l: HookTuple, r: HookTuple) => number,
options?: TransitionHookOptions): TransitionHook[] {

buildHooks(hookType: TransitionHookType): TransitionHook[] {
// Find all the matching registered hooks for a given hook type
let matchingHooks = this._matchingHooks(hookType, this.treeChanges);
let matchingHooks = this._matchingHooks(hookType.name, this.treeChanges);
if (!matchingHooks) return [];

const makeTransitionHooks = (hook: IEventHook) => {
// Fetch the Nodes that caused this hook to match.
let matches: IMatchingNodes = hook.matches(this.treeChanges);
// Select the PathNode[] that will be used as TransitionHook context objects
let matchingNodes: PathNode[] = matches[matchingNodesProp];

// When invoking 'exiting' hooks, give them the "from path" for resolve data.
// Everything else gets the "to path"
let resolvePath = matchingNodesProp === 'exiting' ? this.treeChanges.from : this.treeChanges.to;
let resolveContext = new ResolveContext(resolvePath);
let matchingNodes: PathNode[] = matches[hookType.criteriaMatchPath];

// Return an array of HookTuples
return matchingNodes.map(node => {
let _options = extend({ bind: hook.bind, traceData: { hookType, context: node} }, this.baseHookOptions, options);
let state = _options.stateHook ? node.state : null;
let _options = extend({
bind: hook.bind,
traceData: { hookType: hookType.name, context: node}
}, this.baseHookOptions);

let state = hookType.hookScope === TransitionHookScope.STATE ? node.state : null;
let transitionHook = new TransitionHook(this.transition, state, hook, _options);
return <HookTuple> { hook, node, transitionHook };
});
};

return matchingHooks.map(makeTransitionHooks)
.reduce(unnestR, [])
.sort(sortHooksFn)
.sort(tupleSort(hookType.reverseSort))
.map(tuple => tuple.transitionHook);
}

Expand Down
8 changes: 7 additions & 1 deletion src/transition/interface.ts
Expand Up @@ -745,6 +745,8 @@ export type IStateMatch = Predicate<State>
* ```
*/
export interface HookMatchCriteria {
[key: string]: HookMatchCriterion;

/** A [[HookMatchCriterion]] to match the destination state */
to?: HookMatchCriterion;
/** A [[HookMatchCriterion]] to match the original (from) state */
Expand All @@ -759,6 +761,7 @@ export interface HookMatchCriteria {

export interface IMatchingNodes {
[key: string]: PathNode[];

to: PathNode[];
from: PathNode[];
exiting: PathNode[];
Expand All @@ -785,4 +788,7 @@ export interface IEventHook {
bind?: any;
matches: (treeChanges: TreeChanges) => IMatchingNodes;
_deregistered: boolean;
}
}

export enum TransitionHookPhase { CREATE, BEFORE, ASYNC, SUCCESS, ERROR }
export enum TransitionHookScope { TRANSITION, STATE }
62 changes: 34 additions & 28 deletions src/transition/transition.ts
Expand Up @@ -3,17 +3,14 @@ import {stringify} from "../common/strings";
import {trace} from "../common/trace";
import {services} from "../common/coreservices";
import {
map, find, extend, mergeR, tail,
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 } from "../common/hof";

import {StateDeclaration, StateOrName} from "../state/interface";
import {
TransitionOptions, TransitionHookOptions, TreeChanges, IHookRegistry,
IHookGetter, HookMatchCriteria, HookRegOptions, IEventHook
} from "./interface";
import { TransitionOptions, TreeChanges, IHookRegistry, IEventHook, TransitionHookPhase } from "./interface";

import { TransitionStateHookFn, TransitionHookFn } from "./interface"; // has or is using

Expand Down Expand Up @@ -86,7 +83,7 @@ export class Transition implements IHookRegistry {
private _error: any;

/** @hidden Holds the hook registration functions such as those passed to Transition.onStart() */
private _transitionEvents: IEventHooks = { };
private _transitionHooks: IEventHooks = { };

/** @hidden */
private _options: TransitionOptions;
Expand All @@ -95,31 +92,37 @@ export class Transition implements IHookRegistry {
/** @hidden */
private _targetState: TargetState;

/** @hidden Creates a hook registration function (which can then be used to register hooks) */
private createHookRegFn (hookName: string) {
return makeHookRegistrationFn(this._transitionEvents, hookName);
}

/** @hidden */
onBefore = this.createHookRegFn('onBefore');
onBefore;
/** @inheritdoc */
onStart = this.createHookRegFn('onStart');
onStart;
/** @inheritdoc */
onExit = this.createHookRegFn('onExit');
onExit;
/** @inheritdoc */
onRetain = this.createHookRegFn('onRetain');
onRetain;
/** @inheritdoc */
onEnter = this.createHookRegFn('onEnter');
onEnter;
/** @inheritdoc */
onFinish = this.createHookRegFn('onFinish');
onFinish;
/** @inheritdoc */
onSuccess = this.createHookRegFn('onSuccess');
onSuccess;
/** @inheritdoc */
onError = this.createHookRegFn('onError');
onError;

/** @hidden
* Creates the transition-level hook registration functions
* (which can then be used to register hooks)
*/
private createTransitionHookRegFns() {
this.router.transitionService.getTransitionHookTypes()
.filter(type => type.hookPhase !== TransitionHookPhase.CREATE)
.forEach(type => this[type.name] = makeHookRegistrationFn(this._transitionHooks, type.name));
}

/** @hidden @internalapi */
getHooks(hookName: string): IEventHook[] {
return this._transitionEvents[hookName];
return this._transitionHooks[hookName];
}

/**
Expand Down Expand Up @@ -160,6 +163,8 @@ export class Transition implements IHookRegistry {
let rootNode: PathNode = this._treeChanges.to[0];
let context = new ResolveContext(this._treeChanges.to);
context.addResolvables(rootResolvables, rootNode.state);

this.createTransitionHookRegFns();
}

/**
Expand Down Expand Up @@ -530,10 +535,7 @@ export class Transition implements IHookRegistry {
* @hidden
*/
hookBuilder(): HookBuilder {
return new HookBuilder(this.router.transitionService, this, <TransitionHookOptions> {
transition: this,
current: this._options.current
});
return new HookBuilder(this);
}

/**
Expand All @@ -552,7 +554,8 @@ export class Transition implements IHookRegistry {
let globals = <Globals> this.router.globals;
globals.transitionHistory.enqueue(this);

let syncResult = runSynchronousHooks(hookBuilder.getOnBeforeHooks());
let onBeforeHooks = hookBuilder.buildHooksForPhase(TransitionHookPhase.BEFORE);
let syncResult = runSynchronousHooks(onBeforeHooks);

if (Rejection.isTransitionRejectionPromise(syncResult)) {
syncResult.catch(() => 0); // issue #2676
Expand All @@ -578,15 +581,17 @@ export class Transition implements IHookRegistry {
trace.traceSuccess(this.$to(), this);
this.success = true;
this._deferred.resolve(this.to());
runAllHooks(hookBuilder.getOnSuccessHooks());
let onSuccessHooks = hookBuilder.buildHooksForPhase(TransitionHookPhase.SUCCESS);
runAllHooks(onSuccessHooks);
};

const transitionError = (reason: any) => {
trace.traceError(reason, this);
this.success = false;
this._deferred.reject(reason);
this._error = reason;
runAllHooks(hookBuilder.getOnErrorHooks());
let onErrorHooks = hookBuilder.buildHooksForPhase(TransitionHookPhase.ERROR);
runAllHooks(onErrorHooks);
};

trace.traceTransitionStart(this);
Expand All @@ -596,8 +601,9 @@ export class Transition implements IHookRegistry {
prev.then(() => nextHook.invokeHook());

// Run the hooks, then resolve or reject the overall deferred in the .then() handler
hookBuilder.asyncHooks()
.reduce(appendHookToChain, syncResult)
let asyncHooks = hookBuilder.buildHooksForPhase(TransitionHookPhase.ASYNC)

asyncHooks.reduce(appendHookToChain, syncResult)
.then(transitionSuccess, transitionError);

return this.promise;
Expand Down
38 changes: 38 additions & 0 deletions src/transition/transitionHookType.ts
@@ -0,0 +1,38 @@
import {TransitionHookScope, TransitionHookPhase} from "./interface";
import {PathNode} from "../path/node";
import {Transition} from "./transition";
import {isString} from "../common/predicates";
/**
* This class defines a type of hook, such as `onBefore` or `onEnter`.
* Plugins can define custom hook types, such as sticky states does for `onInactive`.
*
* @interalapi
* @module transition
*/
export class TransitionHookType {

public name: string;
public hookScope: TransitionHookScope;
public hookPhase: TransitionHookPhase;
public hookOrder: number;
public criteriaMatchPath: string;
public resolvePath: (trans: Transition) => PathNode[];
public reverseSort: boolean;

constructor(name: string,
hookScope: TransitionHookScope,
hookPhase: TransitionHookPhase,
hookOrder: number,
criteriaMatchPath: string,
resolvePath: ((trans: Transition) => PathNode[]) | string,
reverseSort: boolean = false
) {
this.name = name;
this.hookScope = hookScope;
this.hookPhase = hookPhase;
this.hookOrder = hookOrder;
this.criteriaMatchPath = criteriaMatchPath;
this.resolvePath = isString(resolvePath) ? (trans: Transition) => trans.treeChanges(resolvePath) : resolvePath;
this.reverseSort = reverseSort;
}
}

0 comments on commit 3f146e6

Please sign in to comment.