Skip to content

Commit 3f146e6

Browse files
feat(HookBuilder): Allow custom hook types (to be defined by a plugin)
1 parent f0dcdce commit 3f146e6

File tree

6 files changed

+221
-136
lines changed

6 files changed

+221
-136
lines changed

src/transition/hookBuilder.ts

+31-42
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
import {extend, tail, assertPredicate, unnestR, identity} from "../common/common";
44
import {isArray} from "../common/predicates";
55

6-
import {TransitionOptions, TransitionHookOptions, IHookRegistry, TreeChanges, IEventHook, IMatchingNodes} from "./interface";
6+
import {
7+
TransitionOptions, TransitionHookOptions, IHookRegistry, TreeChanges, IEventHook, IMatchingNodes,
8+
TransitionHookPhase, TransitionHookScope
9+
} from "./interface";
710

811
import {Transition} from "./transition";
912
import {TransitionHook} from "./transitionHook";
1013
import {State} from "../state/stateObject";
1114
import {PathNode} from "../path/node";
1215
import {TransitionService} from "./transitionService";
13-
import {ResolveContext} from "../resolve/resolveContext";
16+
import {TransitionHookType} from "./transitionHookType";
1417

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

34+
private $transitions: TransitionService;
35+
private baseHookOptions: TransitionHookOptions;
36+
3137
treeChanges: TreeChanges;
3238
transitionOptions: TransitionOptions;
3339

3440
toState: State;
3541
fromState: State;
3642

37-
constructor(private $transitions: TransitionService, private transition: Transition, private baseHookOptions: TransitionHookOptions) {
43+
constructor(private transition: Transition) {
3844
this.treeChanges = transition.treeChanges();
45+
this.transitionOptions = transition.options();
3946
this.toState = tail(this.treeChanges.to).state;
4047
this.fromState = tail(this.treeChanges.from).state;
41-
this.transitionOptions = transition.options();
48+
this.$transitions = transition.router.transitionService;
49+
this.baseHookOptions = <TransitionHookOptions> {
50+
transition: transition,
51+
current: transition.options().current
52+
};
4253
}
4354

44-
getOnBeforeHooks = () => this._buildNodeHooks("onBefore", "to", tupleSort());
45-
getOnStartHooks = () => this._buildNodeHooks("onStart", "to", tupleSort());
46-
getOnExitHooks = () => this._buildNodeHooks("onExit", "exiting", tupleSort(true), { stateHook: true });
47-
getOnRetainHooks = () => this._buildNodeHooks("onRetain", "retained", tupleSort(false), { stateHook: true });
48-
getOnEnterHooks = () => this._buildNodeHooks("onEnter", "entering", tupleSort(false), { stateHook: true });
49-
getOnFinishHooks = () => this._buildNodeHooks("onFinish", "to", tupleSort());
50-
getOnSuccessHooks = () => this._buildNodeHooks("onSuccess", "to", tupleSort(), { rejectIfSuperseded: false });
51-
getOnErrorHooks = () => this._buildNodeHooks("onError", "to", tupleSort(), { rejectIfSuperseded: false });
52-
53-
asyncHooks() {
54-
let onStartHooks = this.getOnStartHooks();
55-
let onExitHooks = this.getOnExitHooks();
56-
let onRetainHooks = this.getOnRetainHooks();
57-
let onEnterHooks = this.getOnEnterHooks();
58-
let onFinishHooks = this.getOnFinishHooks();
59-
60-
let asyncHooks = [onStartHooks, onExitHooks, onRetainHooks, onEnterHooks, onFinishHooks];
61-
return asyncHooks.reduce(unnestR, []).filter(identity);
55+
buildHooksForPhase(phase: TransitionHookPhase): TransitionHook[] {
56+
return this.$transitions.getTransitionHookTypes(phase)
57+
.map(type => this.buildHooks(type))
58+
.reduce(unnestR, [])
59+
.filter(identity);
6260
}
6361

6462
/**
@@ -68,44 +66,35 @@ export class HookBuilder {
6866
* - Finds [[PathNode]] (or `PathNode[]`) to use as the TransitionHook context(s)
6967
* - For each of the [[PathNode]]s, creates a TransitionHook
7068
*
71-
* @param hookType the name of the hook registration function, e.g., 'onEnter', 'onFinish'.
72-
* @param matchingNodesProp selects which [[PathNode]]s from the [[IMatchingNodes]] object to create hooks for.
73-
* @param getLocals a function which accepts a [[PathNode]] and returns additional locals to provide to the hook as injectables
74-
* @param sortHooksFn a function which compares two HookTuple and returns <1, 0, or >1
75-
* @param options any specific Transition Hook Options
69+
* @param hookType the type of the hook registration function, e.g., 'onEnter', 'onFinish'.
7670
*/
77-
private _buildNodeHooks(hookType: string,
78-
matchingNodesProp: string,
79-
sortHooksFn: (l: HookTuple, r: HookTuple) => number,
80-
options?: TransitionHookOptions): TransitionHook[] {
81-
71+
buildHooks(hookType: TransitionHookType): TransitionHook[] {
8272
// Find all the matching registered hooks for a given hook type
83-
let matchingHooks = this._matchingHooks(hookType, this.treeChanges);
73+
let matchingHooks = this._matchingHooks(hookType.name, this.treeChanges);
8474
if (!matchingHooks) return [];
8575

8676
const makeTransitionHooks = (hook: IEventHook) => {
8777
// Fetch the Nodes that caused this hook to match.
8878
let matches: IMatchingNodes = hook.matches(this.treeChanges);
8979
// Select the PathNode[] that will be used as TransitionHook context objects
90-
let matchingNodes: PathNode[] = matches[matchingNodesProp];
91-
92-
// When invoking 'exiting' hooks, give them the "from path" for resolve data.
93-
// Everything else gets the "to path"
94-
let resolvePath = matchingNodesProp === 'exiting' ? this.treeChanges.from : this.treeChanges.to;
95-
let resolveContext = new ResolveContext(resolvePath);
80+
let matchingNodes: PathNode[] = matches[hookType.criteriaMatchPath];
9681

9782
// Return an array of HookTuples
9883
return matchingNodes.map(node => {
99-
let _options = extend({ bind: hook.bind, traceData: { hookType, context: node} }, this.baseHookOptions, options);
100-
let state = _options.stateHook ? node.state : null;
84+
let _options = extend({
85+
bind: hook.bind,
86+
traceData: { hookType: hookType.name, context: node}
87+
}, this.baseHookOptions);
88+
89+
let state = hookType.hookScope === TransitionHookScope.STATE ? node.state : null;
10190
let transitionHook = new TransitionHook(this.transition, state, hook, _options);
10291
return <HookTuple> { hook, node, transitionHook };
10392
});
10493
};
10594

10695
return matchingHooks.map(makeTransitionHooks)
10796
.reduce(unnestR, [])
108-
.sort(sortHooksFn)
97+
.sort(tupleSort(hookType.reverseSort))
10998
.map(tuple => tuple.transitionHook);
11099
}
111100

src/transition/interface.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,8 @@ export type IStateMatch = Predicate<State>
745745
* ```
746746
*/
747747
export interface HookMatchCriteria {
748+
[key: string]: HookMatchCriterion;
749+
748750
/** A [[HookMatchCriterion]] to match the destination state */
749751
to?: HookMatchCriterion;
750752
/** A [[HookMatchCriterion]] to match the original (from) state */
@@ -759,6 +761,7 @@ export interface HookMatchCriteria {
759761

760762
export interface IMatchingNodes {
761763
[key: string]: PathNode[];
764+
762765
to: PathNode[];
763766
from: PathNode[];
764767
exiting: PathNode[];
@@ -785,4 +788,7 @@ export interface IEventHook {
785788
bind?: any;
786789
matches: (treeChanges: TreeChanges) => IMatchingNodes;
787790
_deregistered: boolean;
788-
}
791+
}
792+
793+
export enum TransitionHookPhase { CREATE, BEFORE, ASYNC, SUCCESS, ERROR }
794+
export enum TransitionHookScope { TRANSITION, STATE }

src/transition/transition.ts

+34-28
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,14 @@ import {stringify} from "../common/strings";
33
import {trace} from "../common/trace";
44
import {services} from "../common/coreservices";
55
import {
6-
map, find, extend, mergeR, tail,
6+
map, find, extend, mergeR, tail,
77
omit, toJson, arrayTuples, unnestR, identity, anyTrueR
88
} from "../common/common";
99
import { isObject, isArray } from "../common/predicates";
1010
import { prop, propEq, val, not } from "../common/hof";
1111

1212
import {StateDeclaration, StateOrName} from "../state/interface";
13-
import {
14-
TransitionOptions, TransitionHookOptions, TreeChanges, IHookRegistry,
15-
IHookGetter, HookMatchCriteria, HookRegOptions, IEventHook
16-
} from "./interface";
13+
import { TransitionOptions, TreeChanges, IHookRegistry, IEventHook, TransitionHookPhase } from "./interface";
1714

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

@@ -86,7 +83,7 @@ export class Transition implements IHookRegistry {
8683
private _error: any;
8784

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

9188
/** @hidden */
9289
private _options: TransitionOptions;
@@ -95,31 +92,37 @@ export class Transition implements IHookRegistry {
9592
/** @hidden */
9693
private _targetState: TargetState;
9794

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

10396
/** @hidden */
104-
onBefore = this.createHookRegFn('onBefore');
97+
onBefore;
10598
/** @inheritdoc */
106-
onStart = this.createHookRegFn('onStart');
99+
onStart;
107100
/** @inheritdoc */
108-
onExit = this.createHookRegFn('onExit');
101+
onExit;
109102
/** @inheritdoc */
110-
onRetain = this.createHookRegFn('onRetain');
103+
onRetain;
111104
/** @inheritdoc */
112-
onEnter = this.createHookRegFn('onEnter');
105+
onEnter;
113106
/** @inheritdoc */
114-
onFinish = this.createHookRegFn('onFinish');
107+
onFinish;
115108
/** @inheritdoc */
116-
onSuccess = this.createHookRegFn('onSuccess');
109+
onSuccess;
117110
/** @inheritdoc */
118-
onError = this.createHookRegFn('onError');
111+
onError;
112+
113+
/** @hidden
114+
* Creates the transition-level hook registration functions
115+
* (which can then be used to register hooks)
116+
*/
117+
private createTransitionHookRegFns() {
118+
this.router.transitionService.getTransitionHookTypes()
119+
.filter(type => type.hookPhase !== TransitionHookPhase.CREATE)
120+
.forEach(type => this[type.name] = makeHookRegistrationFn(this._transitionHooks, type.name));
121+
}
119122

120123
/** @hidden @internalapi */
121124
getHooks(hookName: string): IEventHook[] {
122-
return this._transitionEvents[hookName];
125+
return this._transitionHooks[hookName];
123126
}
124127

125128
/**
@@ -160,6 +163,8 @@ export class Transition implements IHookRegistry {
160163
let rootNode: PathNode = this._treeChanges.to[0];
161164
let context = new ResolveContext(this._treeChanges.to);
162165
context.addResolvables(rootResolvables, rootNode.state);
166+
167+
this.createTransitionHookRegFns();
163168
}
164169

165170
/**
@@ -530,10 +535,7 @@ export class Transition implements IHookRegistry {
530535
* @hidden
531536
*/
532537
hookBuilder(): HookBuilder {
533-
return new HookBuilder(this.router.transitionService, this, <TransitionHookOptions> {
534-
transition: this,
535-
current: this._options.current
536-
});
538+
return new HookBuilder(this);
537539
}
538540

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

555-
let syncResult = runSynchronousHooks(hookBuilder.getOnBeforeHooks());
557+
let onBeforeHooks = hookBuilder.buildHooksForPhase(TransitionHookPhase.BEFORE);
558+
let syncResult = runSynchronousHooks(onBeforeHooks);
556559

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

584588
const transitionError = (reason: any) => {
585589
trace.traceError(reason, this);
586590
this.success = false;
587591
this._deferred.reject(reason);
588592
this._error = reason;
589-
runAllHooks(hookBuilder.getOnErrorHooks());
593+
let onErrorHooks = hookBuilder.buildHooksForPhase(TransitionHookPhase.ERROR);
594+
runAllHooks(onErrorHooks);
590595
};
591596

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

598603
// Run the hooks, then resolve or reject the overall deferred in the .then() handler
599-
hookBuilder.asyncHooks()
600-
.reduce(appendHookToChain, syncResult)
604+
let asyncHooks = hookBuilder.buildHooksForPhase(TransitionHookPhase.ASYNC)
605+
606+
asyncHooks.reduce(appendHookToChain, syncResult)
601607
.then(transitionSuccess, transitionError);
602608

603609
return this.promise;

src/transition/transitionHookType.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {TransitionHookScope, TransitionHookPhase} from "./interface";
2+
import {PathNode} from "../path/node";
3+
import {Transition} from "./transition";
4+
import {isString} from "../common/predicates";
5+
/**
6+
* This class defines a type of hook, such as `onBefore` or `onEnter`.
7+
* Plugins can define custom hook types, such as sticky states does for `onInactive`.
8+
*
9+
* @interalapi
10+
* @module transition
11+
*/
12+
export class TransitionHookType {
13+
14+
public name: string;
15+
public hookScope: TransitionHookScope;
16+
public hookPhase: TransitionHookPhase;
17+
public hookOrder: number;
18+
public criteriaMatchPath: string;
19+
public resolvePath: (trans: Transition) => PathNode[];
20+
public reverseSort: boolean;
21+
22+
constructor(name: string,
23+
hookScope: TransitionHookScope,
24+
hookPhase: TransitionHookPhase,
25+
hookOrder: number,
26+
criteriaMatchPath: string,
27+
resolvePath: ((trans: Transition) => PathNode[]) | string,
28+
reverseSort: boolean = false
29+
) {
30+
this.name = name;
31+
this.hookScope = hookScope;
32+
this.hookPhase = hookPhase;
33+
this.hookOrder = hookOrder;
34+
this.criteriaMatchPath = criteriaMatchPath;
35+
this.resolvePath = isString(resolvePath) ? (trans: Transition) => trans.treeChanges(resolvePath) : resolvePath;
36+
this.reverseSort = reverseSort;
37+
}
38+
}

0 commit comments

Comments
 (0)