Skip to content

Commit 0dc2c19

Browse files
refactor(plugin): Add transition plugin API
feat(transition): Allow plugins to define own transition events like `onEnter` refactory(dynamic): Detect dynamic transitions checking: reload, to/from length, to/from equality, parameter values
1 parent f044f53 commit 0dc2c19

10 files changed

+185
-89
lines changed

src/common/common.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ export interface Obj extends Object {
8686
* @param bindTo The object which the functions will be bound to
8787
* @param fnNames The function names which will be bound (Defaults to all the functions found on the 'from' object)
8888
*/
89-
export function bindFunctions(from: Obj, to: Obj, bindTo: Obj, fnNames: string[] = Object.keys(from)) {
90-
return fnNames.filter(name => typeof from[name] === 'function')
89+
export function bindFunctions(from: Obj, to: Obj, bindTo: Obj, fnNames: string[] = Object.keys(from)): Obj {
90+
fnNames.filter(name => typeof from[name] === 'function')
9191
.forEach(name => to[name] = from[name].bind(bindTo));
92+
return to;
9293
}
9394

9495

src/transition/hookBuilder.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {TransitionHook} from "./transitionHook";
1313
import {State} from "../state/stateObject";
1414
import {PathNode} from "../path/node";
1515
import {TransitionService} from "./transitionService";
16-
import {TransitionHookType} from "./transitionHookType";
16+
import {TransitionEventType} from "./transitionEventType";
1717
import {RegisteredHook} from "./hookRegistry";
1818

1919
/**
@@ -54,7 +54,7 @@ export class HookBuilder {
5454
}
5555

5656
buildHooksForPhase(phase: TransitionHookPhase): TransitionHook[] {
57-
return this.$transitions.getTransitionHookTypes(phase)
57+
return this.$transitions._pluginapi.getTransitionEventTypes(phase)
5858
.map(type => this.buildHooks(type))
5959
.reduce(unnestR, [])
6060
.filter(identity);
@@ -69,7 +69,7 @@ export class HookBuilder {
6969
*
7070
* @param hookType the type of the hook registration function, e.g., 'onEnter', 'onFinish'.
7171
*/
72-
buildHooks(hookType: TransitionHookType): TransitionHook[] {
72+
buildHooks(hookType: TransitionEventType): TransitionHook[] {
7373
// Find all the matching registered hooks for a given hook type
7474
let matchingHooks = this.getMatchingHooks(hookType, this.treeChanges);
7575
if (!matchingHooks) return [];
@@ -110,7 +110,7 @@ export class HookBuilder {
110110
*
111111
* @returns an array of matched [[RegisteredHook]]s
112112
*/
113-
public getMatchingHooks(hookType: TransitionHookType, treeChanges: TreeChanges): RegisteredHook[] {
113+
public getMatchingHooks(hookType: TransitionEventType, treeChanges: TreeChanges): RegisteredHook[] {
114114
let isCreate = hookType.hookPhase === TransitionHookPhase.CREATE;
115115

116116
// Instance and Global hook registries

src/transition/hookRegistry.ts

+89-33
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
/** @coreapi @module transition */ /** for typedoc */
2-
import {extend, removeFrom, allTrueR, tail} from "../common/common";
2+
import { extend, removeFrom, allTrueR, tail, uniqR, pushTo, equals, values, identity } from "../common/common";
33
import {isString, isFunction} from "../common/predicates";
44
import {PathNode} from "../path/node";
5-
import {TransitionStateHookFn, TransitionHookFn} from "./interface"; // has or is using
5+
import {
6+
TransitionStateHookFn, TransitionHookFn, TransitionHookPhase, TransitionHookScope, IHookRegistry
7+
} from "./interface"; // has or is using
68

79
import {
810
HookRegOptions, HookMatchCriteria, IHookRegistration, TreeChanges,
911
HookMatchCriterion, IMatchingNodes, HookFn
1012
} from "./interface";
1113
import {Glob} from "../common/glob";
1214
import {State} from "../state/stateObject";
13-
import {TransitionHookType} from "./transitionHookType";
15+
import {TransitionEventType} from "./transitionEventType";
16+
import { TransitionService } from "./transitionService";
1417

1518
/**
1619
* Determines if the given state matches the matchCriteria
@@ -49,53 +52,100 @@ export function matchState(state: State, criterion: HookMatchCriterion) {
4952
* The registration data for a registered transition hook
5053
*/
5154
export class RegisteredHook implements RegisteredHook {
52-
hookType: TransitionHookType;
53-
callback: HookFn;
5455
matchCriteria: HookMatchCriteria;
5556
priority: number;
5657
bind: any;
5758
_deregistered: boolean;
5859

59-
constructor(hookType: TransitionHookType,
60+
constructor(public tranSvc: TransitionService,
61+
public eventType: TransitionEventType,
62+
public callback: HookFn,
6063
matchCriteria: HookMatchCriteria,
61-
callback: HookFn,
62-
options: HookRegOptions = <any>{}) {
63-
this.hookType = hookType;
64-
this.callback = callback;
65-
this.matchCriteria = extend({ to: true, from: true, exiting: true, retained: true, entering: true }, matchCriteria);
64+
options: HookRegOptions = {} as any) {
65+
this.matchCriteria = extend(this._getDefaultMatchCriteria(), matchCriteria);
6666
this.priority = options.priority || 0;
6767
this.bind = options.bind || null;
6868
this._deregistered = false;
6969
}
7070

71-
private static _matchingNodes(nodes: PathNode[], criterion: HookMatchCriterion): PathNode[] {
71+
/**
72+
* Given an array of PathNodes, and a HookMatchCriteria, returns an array containing
73+
* the PathNodes that the criteria matches, or null if there were no matching nodes.
74+
*
75+
* Returning null is significant to distinguish between the default
76+
* "match-all criterion value" of `true` compared to a () => true,
77+
* when the nodes is an empty array.
78+
*
79+
* This is useful to allow a transition match criteria of `entering: true`
80+
* to still match a transition, even when `entering === []`. Contrast that
81+
* with `entering: (state) => true` which only matches when a state is actually
82+
* being entered.
83+
*/
84+
private _matchingNodes(nodes: PathNode[], criterion: HookMatchCriterion): PathNode[] {
7285
if (criterion === true) return nodes;
7386
let matching = nodes.filter(node => matchState(node.state, criterion));
7487
return matching.length ? matching : null;
7588
}
7689

90+
/**
91+
* Returns an object which has all the criteria match paths as keys and `true` as values, i.e.:
92+
*
93+
* { to: true, from: true, entering: true, exiting: true, retained: true }
94+
*/
95+
private _getDefaultMatchCriteria(): HookMatchCriteria {
96+
return this.tranSvc._pluginapi.getTransitionEventTypes()
97+
.map(type => type.criteriaMatchPath)
98+
.reduce<any[]>(uniqR, [])
99+
.reduce((acc, path) => (acc[path] = true, acc), {});
100+
}
101+
102+
/**
103+
* For all the criteria match paths in all TransitionHookTypes,
104+
* return an object where: keys are pathname, vals are TransitionHookScope
105+
*/
106+
private _getPathScopes(): { [key: string]: TransitionHookScope } {
107+
return this.tranSvc._pluginapi.getTransitionEventTypes().reduce((paths, type) => {
108+
paths[type.criteriaMatchPath] = type.hookScope;
109+
return paths
110+
}, {});
111+
}
112+
113+
/**
114+
* Create a IMatchingNodes object from the TransitionHookTypes that basically looks like this:
115+
*
116+
* let matches: IMatchingNodes = {
117+
* to: _matchingNodes([tail(treeChanges.to)], mc.to),
118+
* from: _matchingNodes([tail(treeChanges.from)], mc.from),
119+
* exiting: _matchingNodes(treeChanges.exiting, mc.exiting),
120+
* retained: _matchingNodes(treeChanges.retained, mc.retained),
121+
* entering: _matchingNodes(treeChanges.entering, mc.entering),
122+
* };
123+
*/
124+
private _getMatchingNodes(treeChanges: TreeChanges): IMatchingNodes {
125+
let pathScopes: { [key: string]: TransitionHookScope } = this._getPathScopes();
126+
127+
return Object.keys(pathScopes).reduce((mn: IMatchingNodes, pathName: string) => {
128+
// STATE scope criteria matches against every node in the path.
129+
// TRANSITION scope criteria matches against only the last node in the path
130+
let isStateHook = pathScopes[pathName] === TransitionHookScope.STATE;
131+
let nodes: PathNode[] = isStateHook ? treeChanges[pathName] : [tail(treeChanges[pathName])];
132+
133+
mn[pathName] = this._matchingNodes(nodes, this.matchCriteria[pathName]);
134+
return mn;
135+
}, {} as IMatchingNodes);
136+
}
137+
77138
/**
78139
* Determines if this hook's [[matchCriteria]] match the given [[TreeChanges]]
79140
*
80141
* @returns an IMatchingNodes object, or null. If an IMatchingNodes object is returned, its values
81142
* are the matching [[PathNode]]s for each [[HookMatchCriterion]] (to, from, exiting, retained, entering)
82143
*/
83144
matches(treeChanges: TreeChanges): IMatchingNodes {
84-
let mc = this.matchCriteria, _matchingNodes = RegisteredHook._matchingNodes;
85-
86-
let matches: IMatchingNodes = {
87-
to: _matchingNodes([tail(treeChanges.to)], mc.to),
88-
from: _matchingNodes([tail(treeChanges.from)], mc.from),
89-
exiting: _matchingNodes(treeChanges.exiting, mc.exiting),
90-
retained: _matchingNodes(treeChanges.retained, mc.retained),
91-
entering: _matchingNodes(treeChanges.entering, mc.entering),
92-
};
145+
let matches = this._getMatchingNodes(treeChanges);
93146

94147
// Check if all the criteria matched the TreeChanges object
95-
let allMatched: boolean = ["to", "from", "exiting", "retained", "entering"]
96-
.map(prop => matches[prop])
97-
.reduce(allTrueR, true);
98-
148+
let allMatched = values(matches).every(identity);
99149
return allMatched ? matches : null;
100150
}
101151
}
@@ -106,17 +156,23 @@ export interface RegisteredHooks {
106156
}
107157

108158
/** @hidden Return a registration function of the requested type. */
109-
export function makeHookRegistrationFn(registeredHooks: RegisteredHooks, type: TransitionHookType): IHookRegistration {
110-
let name = type.name;
111-
registeredHooks[name] = [];
159+
export function makeEvent(registry: IHookRegistry, transitionService: TransitionService, eventType: TransitionEventType) {
160+
// Create the object which holds the registered transition hooks.
161+
let _registeredHooks = registry._registeredHooks = (registry._registeredHooks || {});
162+
let hooks = _registeredHooks[eventType.name] = [];
112163

113-
return function (matchObject, callback, options = {}) {
114-
let registeredHook = new RegisteredHook(type, matchObject, callback, options);
115-
registeredHooks[name].push(registeredHook);
164+
// Create hook registration function on the IHookRegistry for the event
165+
registry[eventType.name] = hookRegistrationFn;
166+
167+
function hookRegistrationFn(matchObject, callback, options = {}) {
168+
let registeredHook = new RegisteredHook(transitionService, eventType, callback, matchObject, options);
169+
hooks.push(registeredHook);
116170

117171
return function deregisterEventHook() {
118172
registeredHook._deregistered = true;
119-
removeFrom(registeredHooks[name])(registeredHook);
173+
removeFrom(hooks)(registeredHook);
120174
};
121-
};
175+
}
176+
177+
return hookRegistrationFn;
122178
}

src/transition/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ export * from "./hookRegistry";
1616
export * from "./rejectFactory";
1717
export * from "./transition";
1818
export * from "./transitionHook";
19+
export * from "./transitionEventType";
1920
export * from "./transitionService";
2021

src/transition/interface.ts

+3
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,9 @@ export interface IHookRegistry {
694694
* ```
695695
*/
696696
getHooks(hookName: string): RegisteredHook[];
697+
698+
/** @hidden place to store the hooks */
699+
_registeredHooks: { [key: string]: RegisteredHook[] }
697700
}
698701

699702
/** A predicate type which takes a [[State]] and returns a boolean */

src/transition/transition.ts

+21-11
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
import { TransitionStateHookFn, TransitionHookFn } from "./interface"; // has or is using
1919

2020
import {TransitionHook} from "./transitionHook";
21-
import {matchState, RegisteredHooks, makeHookRegistrationFn, RegisteredHook} from "./hookRegistry";
21+
import {matchState, RegisteredHooks, makeEvent, RegisteredHook} from "./hookRegistry";
2222
import {HookBuilder} from "./hookBuilder";
2323
import {PathNode} from "../path/node";
2424
import {PathFactory} from "../path/pathFactory";
@@ -84,7 +84,7 @@ export class Transition implements IHookRegistry {
8484
private _error: any;
8585

8686
/** @hidden Holds the hook registration functions such as those passed to Transition.onStart() */
87-
private _transitionHooks: RegisteredHooks = { };
87+
_registeredHooks: RegisteredHooks = { };
8888

8989
/** @hidden */
9090
private _options: TransitionOptions;
@@ -116,14 +116,14 @@ export class Transition implements IHookRegistry {
116116
* (which can then be used to register hooks)
117117
*/
118118
private createTransitionHookRegFns() {
119-
this.router.transitionService.getTransitionHookTypes()
119+
this.router.transitionService._pluginapi.getTransitionEventTypes()
120120
.filter(type => type.hookPhase !== TransitionHookPhase.CREATE)
121-
.forEach(type => this[type.name] = makeHookRegistrationFn(this._transitionHooks, type));
121+
.forEach(type => makeEvent(this, this.router.transitionService, type));
122122
}
123123

124124
/** @hidden @internalapi */
125125
getHooks(hookName: string): RegisteredHook[] {
126-
return this._transitionHooks[hookName];
126+
return this._registeredHooks[hookName];
127127
}
128128

129129
/**
@@ -509,13 +509,23 @@ export class Transition implements IHookRegistry {
509509
/** @hidden If a transition doesn't exit/enter any states, returns any [[Param]] whose value changed */
510510
private _changedParams(): Param[] {
511511
let tc = this._treeChanges;
512-
let to = tc.to;
513-
let from = tc.from;
514512

515-
if (this._options.reload || tc.entering.length || tc.exiting.length) return undefined;
516-
517-
let nodeSchemas: Param[][] = to.map((node: PathNode) => node.paramSchema);
518-
let [toValues, fromValues] = [to, from].map(path => path.map(x => x.paramValues));
513+
/** Return undefined if it's not a "dynamic" transition, for the following reasons */
514+
// If user explicitly wants a reload
515+
if (this._options.reload) return undefined;
516+
// If any states are exiting or entering
517+
if (tc.exiting.length || tc.entering.length) return undefined;
518+
// If to/from path lengths differ
519+
if (tc.to.length !== tc.from.length) return undefined;
520+
// If the to/from paths are different
521+
let pathsDiffer: boolean = arrayTuples(tc.to, tc.from)
522+
.map(tuple => tuple[0].state !== tuple[1].state)
523+
.reduce(anyTrueR, false);
524+
if (pathsDiffer) return undefined;
525+
526+
// Find any parameter values that differ
527+
let nodeSchemas: Param[][] = tc.to.map((node: PathNode) => node.paramSchema);
528+
let [toValues, fromValues] = [tc.to, tc.from].map(path => path.map(x => x.paramValues));
519529
let tuples = arrayTuples(nodeSchemas, toValues, fromValues);
520530

521531
return tuples.map(([schema, toVals, fromVals]) => Param.changed(schema, toVals, fromVals)).reduce(unnestR, []);

src/transition/transitionHookType.ts renamed to src/transition/transitionEventType.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ import {GetErrorHandler, GetResultHandler, TransitionHook} from "./transitionHoo
1010
* @interalapi
1111
* @module transition
1212
*/
13-
export class TransitionHookType {
13+
export class TransitionEventType {
1414

1515
constructor(public name: string,
1616
public hookPhase: TransitionHookPhase,
1717
public hookScope: TransitionHookScope,
1818
public hookOrder: number,
1919
public criteriaMatchPath: string,
2020
public reverseSort: boolean = false,
21-
public getResultHandler: GetResultHandler = TransitionHook.HANDLE_RESULT,
22-
public getErrorHandler: GetErrorHandler = TransitionHook.REJECT_ERROR,
21+
public getResultHandler: GetResultHandler = TransitionHook.HANDLE_RESULT,
22+
public getErrorHandler: GetErrorHandler = TransitionHook.REJECT_ERROR,
2323
public rejectIfSuperseded: boolean = true,
2424
) { }
2525
}

src/transition/transitionHook.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {Rejection} from "./rejectFactory";
1111
import {TargetState} from "../state/targetState";
1212
import {Transition} from "./transition";
1313
import {State} from "../state/stateObject";
14-
import {TransitionHookType} from "./transitionHookType";
14+
import {TransitionEventType} from "./transitionEventType";
1515
import {StateService} from "../state/stateService"; // has or is using
1616
import {RegisteredHook} from "./hookRegistry"; // has or is using
1717

@@ -58,7 +58,7 @@ export class TransitionHook {
5858
undefined;
5959

6060
private rejectIfSuperseded = () =>
61-
this.registeredHook.hookType.rejectIfSuperseded && this.options.current() !== this.options.transition;
61+
this.registeredHook.eventType.rejectIfSuperseded && this.options.current() !== this.options.transition;
6262

6363
invokeHook(): Promise<HookResult> {
6464
let hook = this.registeredHook;
@@ -76,8 +76,8 @@ export class TransitionHook {
7676
let trans = this.transition;
7777
let state = this.stateContext;
7878

79-
let errorHandler = hook.hookType.getErrorHandler(this);
80-
let resultHandler = hook.hookType.getResultHandler(this);
79+
let errorHandler = hook.eventType.getErrorHandler(this);
80+
let resultHandler = hook.eventType.getResultHandler(this);
8181
resultHandler = resultHandler || identity;
8282

8383
if (!errorHandler) {

0 commit comments

Comments
 (0)