/
hookBuilder.ts
151 lines (134 loc) · 5.6 KB
/
hookBuilder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/**
* @coreapi
* @module transition
*/ /** for typedoc */
import { extend, tail, assertPredicate, unnestR, identity } from '../common/common';
import { isArray } from '../common/predicates';
import {
TransitionOptions,
TransitionHookOptions,
IHookRegistry,
TreeChanges,
IMatchingNodes,
TransitionHookPhase,
TransitionHookScope,
} from './interface';
import { Transition } from './transition';
import { TransitionHook } from './transitionHook';
import { StateObject } from '../state/stateObject';
import { PathNode } from '../path/pathNode';
import { TransitionService } from './transitionService';
import { TransitionEventType } from './transitionEventType';
import { RegisteredHook } from './hookRegistry';
/**
* This class returns applicable TransitionHooks for a specific Transition instance.
*
* Hooks ([[RegisteredHook]]) may be registered globally, e.g., $transitions.onEnter(...), or locally, e.g.
* myTransition.onEnter(...). The HookBuilder finds matching RegisteredHooks (where the match criteria is
* determined by the type of hook)
*
* The HookBuilder also converts RegisteredHooks objects to TransitionHook objects, which are used to run a Transition.
*
* The HookBuilder constructor is given the $transitions service and a Transition instance. Thus, a HookBuilder
* instance may only be used for one specific Transition object. (side note: the _treeChanges accessor is private
* in the Transition class, so we must also provide the Transition's _treeChanges)
*
*/
export class HookBuilder {
constructor(private transition: Transition) {}
buildHooksForPhase(phase: TransitionHookPhase): TransitionHook[] {
const $transitions = this.transition.router.transitionService;
return $transitions._pluginapi
._getEvents(phase)
.map(type => this.buildHooks(type))
.reduce(unnestR, [])
.filter(identity);
}
/**
* Returns an array of newly built TransitionHook objects.
*
* - Finds all RegisteredHooks registered for the given `hookType` which matched the transition's [[TreeChanges]].
* - Finds [[PathNode]] (or `PathNode[]`) to use as the TransitionHook context(s)
* - For each of the [[PathNode]]s, creates a TransitionHook
*
* @param hookType the type of the hook registration function, e.g., 'onEnter', 'onFinish'.
*/
buildHooks(hookType: TransitionEventType): TransitionHook[] {
const transition = this.transition;
const treeChanges = transition.treeChanges();
// Find all the matching registered hooks for a given hook type
const matchingHooks = this.getMatchingHooks(hookType, treeChanges);
if (!matchingHooks) return [];
const baseHookOptions = <TransitionHookOptions>{
transition: transition,
current: transition.options().current,
};
const makeTransitionHooks = (hook: RegisteredHook) => {
// Fetch the Nodes that caused this hook to match.
const matches: IMatchingNodes = hook.matches(treeChanges);
// Select the PathNode[] that will be used as TransitionHook context objects
const matchingNodes: PathNode[] = matches[hookType.criteriaMatchPath.name];
// Return an array of HookTuples
return matchingNodes.map(node => {
const _options = extend(
{
bind: hook.bind,
traceData: { hookType: hookType.name, context: node },
},
baseHookOptions,
);
const state = hookType.criteriaMatchPath.scope === TransitionHookScope.STATE ? node.state.self : null;
const transitionHook = new TransitionHook(transition, state, hook, _options);
return <HookTuple>{ hook, node, transitionHook };
});
};
return matchingHooks
.map(makeTransitionHooks)
.reduce(unnestR, [])
.sort(tupleSort(hookType.reverseSort))
.map(tuple => tuple.transitionHook);
}
/**
* Finds all RegisteredHooks from:
* - The Transition object instance hook registry
* - The TransitionService ($transitions) global hook registry
*
* which matched:
* - the eventType
* - the matchCriteria (to, from, exiting, retained, entering)
*
* @returns an array of matched [[RegisteredHook]]s
*/
public getMatchingHooks(hookType: TransitionEventType, treeChanges: TreeChanges): RegisteredHook[] {
const isCreate = hookType.hookPhase === TransitionHookPhase.CREATE;
// Instance and Global hook registries
const $transitions = this.transition.router.transitionService;
const registries = isCreate ? [$transitions] : [this.transition, $transitions];
return registries
.map((reg: IHookRegistry) => reg.getHooks(hookType.name)) // Get named hooks from registries
.filter(assertPredicate(isArray, `broken event named: ${hookType.name}`)) // Sanity check
.reduce(unnestR, []) // Un-nest RegisteredHook[][] to RegisteredHook[] array
.filter(hook => hook.matches(treeChanges)); // Only those satisfying matchCriteria
}
}
interface HookTuple {
hook: RegisteredHook;
node: PathNode;
transitionHook: TransitionHook;
}
/**
* A factory for a sort function for HookTuples.
*
* The sort function first compares the PathNode depth (how deep in the state tree a node is), then compares
* the EventHook priority.
*
* @param reverseDepthSort a boolean, when true, reverses the sort order for the node depth
* @returns a tuple sort function
*/
function tupleSort(reverseDepthSort = false) {
return function nodeDepthThenPriority(l: HookTuple, r: HookTuple): number {
const factor = reverseDepthSort ? -1 : 1;
const depthDelta = (l.node.state.path.length - r.node.state.path.length) * factor;
return depthDelta !== 0 ? depthDelta : r.hook.priority - l.hook.priority;
};
}