Skip to content

Commit

Permalink
refactor(plugin): Add transition plugin API
Browse files Browse the repository at this point in the history
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
  • Loading branch information
christopherthielen committed Dec 9, 2016
1 parent f044f53 commit 0dc2c19
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 89 deletions.
5 changes: 3 additions & 2 deletions src/common/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ export interface Obj extends Object {
* @param bindTo The object which the functions will be bound to
* @param fnNames The function names which will be bound (Defaults to all the functions found on the 'from' object)
*/
export function bindFunctions(from: Obj, to: Obj, bindTo: Obj, fnNames: string[] = Object.keys(from)) {
return fnNames.filter(name => typeof from[name] === 'function')
export function bindFunctions(from: Obj, to: Obj, bindTo: Obj, fnNames: string[] = Object.keys(from)): Obj {
fnNames.filter(name => typeof from[name] === 'function')
.forEach(name => to[name] = from[name].bind(bindTo));
return to;
}


Expand Down
8 changes: 4 additions & 4 deletions src/transition/hookBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {TransitionHook} from "./transitionHook";
import {State} from "../state/stateObject";
import {PathNode} from "../path/node";
import {TransitionService} from "./transitionService";
import {TransitionHookType} from "./transitionHookType";
import {TransitionEventType} from "./transitionEventType";
import {RegisteredHook} from "./hookRegistry";

/**
Expand Down Expand Up @@ -54,7 +54,7 @@ export class HookBuilder {
}

buildHooksForPhase(phase: TransitionHookPhase): TransitionHook[] {
return this.$transitions.getTransitionHookTypes(phase)
return this.$transitions._pluginapi.getTransitionEventTypes(phase)
.map(type => this.buildHooks(type))
.reduce(unnestR, [])
.filter(identity);
Expand All @@ -69,7 +69,7 @@ export class HookBuilder {
*
* @param hookType the type of the hook registration function, e.g., 'onEnter', 'onFinish'.
*/
buildHooks(hookType: TransitionHookType): TransitionHook[] {
buildHooks(hookType: TransitionEventType): TransitionHook[] {
// Find all the matching registered hooks for a given hook type
let matchingHooks = this.getMatchingHooks(hookType, this.treeChanges);
if (!matchingHooks) return [];
Expand Down Expand Up @@ -110,7 +110,7 @@ export class HookBuilder {
*
* @returns an array of matched [[RegisteredHook]]s
*/
public getMatchingHooks(hookType: TransitionHookType, treeChanges: TreeChanges): RegisteredHook[] {
public getMatchingHooks(hookType: TransitionEventType, treeChanges: TreeChanges): RegisteredHook[] {
let isCreate = hookType.hookPhase === TransitionHookPhase.CREATE;

// Instance and Global hook registries
Expand Down
122 changes: 89 additions & 33 deletions src/transition/hookRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
/** @coreapi @module transition */ /** for typedoc */
import {extend, removeFrom, allTrueR, tail} from "../common/common";
import { extend, removeFrom, allTrueR, tail, uniqR, pushTo, equals, values, identity } from "../common/common";
import {isString, isFunction} from "../common/predicates";
import {PathNode} from "../path/node";
import {TransitionStateHookFn, TransitionHookFn} from "./interface"; // has or is using
import {
TransitionStateHookFn, TransitionHookFn, TransitionHookPhase, TransitionHookScope, IHookRegistry
} from "./interface"; // has or is using

import {
HookRegOptions, HookMatchCriteria, IHookRegistration, TreeChanges,
HookMatchCriterion, IMatchingNodes, HookFn
} from "./interface";
import {Glob} from "../common/glob";
import {State} from "../state/stateObject";
import {TransitionHookType} from "./transitionHookType";
import {TransitionEventType} from "./transitionEventType";
import { TransitionService } from "./transitionService";

/**
* Determines if the given state matches the matchCriteria
Expand Down Expand Up @@ -49,53 +52,100 @@ export function matchState(state: State, criterion: HookMatchCriterion) {
* The registration data for a registered transition hook
*/
export class RegisteredHook implements RegisteredHook {
hookType: TransitionHookType;
callback: HookFn;
matchCriteria: HookMatchCriteria;
priority: number;
bind: any;
_deregistered: boolean;

constructor(hookType: TransitionHookType,
constructor(public tranSvc: TransitionService,
public eventType: TransitionEventType,
public callback: HookFn,
matchCriteria: HookMatchCriteria,
callback: HookFn,
options: HookRegOptions = <any>{}) {
this.hookType = hookType;
this.callback = callback;
this.matchCriteria = extend({ to: true, from: true, exiting: true, retained: true, entering: true }, matchCriteria);
options: HookRegOptions = {} as any) {
this.matchCriteria = extend(this._getDefaultMatchCriteria(), matchCriteria);
this.priority = options.priority || 0;
this.bind = options.bind || null;
this._deregistered = false;
}

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

/**
* Returns an object which has all the criteria match paths as keys and `true` as values, i.e.:
*
* { to: true, from: true, entering: true, exiting: true, retained: true }
*/
private _getDefaultMatchCriteria(): HookMatchCriteria {
return this.tranSvc._pluginapi.getTransitionEventTypes()
.map(type => type.criteriaMatchPath)
.reduce<any[]>(uniqR, [])
.reduce((acc, path) => (acc[path] = true, acc), {});
}

/**
* For all the criteria match paths in all TransitionHookTypes,
* return an object where: keys are pathname, vals are TransitionHookScope
*/
private _getPathScopes(): { [key: string]: TransitionHookScope } {
return this.tranSvc._pluginapi.getTransitionEventTypes().reduce((paths, type) => {
paths[type.criteriaMatchPath] = type.hookScope;
return paths
}, {});
}

/**
* Create a IMatchingNodes object from the TransitionHookTypes that basically looks like this:
*
* let matches: IMatchingNodes = {
* to: _matchingNodes([tail(treeChanges.to)], mc.to),
* from: _matchingNodes([tail(treeChanges.from)], mc.from),
* exiting: _matchingNodes(treeChanges.exiting, mc.exiting),
* retained: _matchingNodes(treeChanges.retained, mc.retained),
* entering: _matchingNodes(treeChanges.entering, mc.entering),
* };
*/
private _getMatchingNodes(treeChanges: TreeChanges): IMatchingNodes {
let pathScopes: { [key: string]: TransitionHookScope } = this._getPathScopes();

return Object.keys(pathScopes).reduce((mn: IMatchingNodes, pathName: string) => {
// STATE scope criteria matches against every node in the path.
// TRANSITION scope criteria matches against only the last node in the path
let isStateHook = pathScopes[pathName] === TransitionHookScope.STATE;
let nodes: PathNode[] = isStateHook ? treeChanges[pathName] : [tail(treeChanges[pathName])];

mn[pathName] = this._matchingNodes(nodes, this.matchCriteria[pathName]);
return mn;
}, {} as IMatchingNodes);
}

/**
* Determines if this hook's [[matchCriteria]] match the given [[TreeChanges]]
*
* @returns an IMatchingNodes object, or null. If an IMatchingNodes object is returned, its values
* are the matching [[PathNode]]s for each [[HookMatchCriterion]] (to, from, exiting, retained, entering)
*/
matches(treeChanges: TreeChanges): IMatchingNodes {
let mc = this.matchCriteria, _matchingNodes = RegisteredHook._matchingNodes;

let matches: IMatchingNodes = {
to: _matchingNodes([tail(treeChanges.to)], mc.to),
from: _matchingNodes([tail(treeChanges.from)], mc.from),
exiting: _matchingNodes(treeChanges.exiting, mc.exiting),
retained: _matchingNodes(treeChanges.retained, mc.retained),
entering: _matchingNodes(treeChanges.entering, mc.entering),
};
let matches = this._getMatchingNodes(treeChanges);

// Check if all the criteria matched the TreeChanges object
let allMatched: boolean = ["to", "from", "exiting", "retained", "entering"]
.map(prop => matches[prop])
.reduce(allTrueR, true);

let allMatched = values(matches).every(identity);
return allMatched ? matches : null;
}
}
Expand All @@ -106,17 +156,23 @@ export interface RegisteredHooks {
}

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

return function (matchObject, callback, options = {}) {
let registeredHook = new RegisteredHook(type, matchObject, callback, options);
registeredHooks[name].push(registeredHook);
// Create hook registration function on the IHookRegistry for the event
registry[eventType.name] = hookRegistrationFn;

function hookRegistrationFn(matchObject, callback, options = {}) {
let registeredHook = new RegisteredHook(transitionService, eventType, callback, matchObject, options);
hooks.push(registeredHook);

return function deregisterEventHook() {
registeredHook._deregistered = true;
removeFrom(registeredHooks[name])(registeredHook);
removeFrom(hooks)(registeredHook);
};
};
}

return hookRegistrationFn;
}
1 change: 1 addition & 0 deletions src/transition/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export * from "./hookRegistry";
export * from "./rejectFactory";
export * from "./transition";
export * from "./transitionHook";
export * from "./transitionEventType";
export * from "./transitionService";

3 changes: 3 additions & 0 deletions src/transition/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,9 @@ export interface IHookRegistry {
* ```
*/
getHooks(hookName: string): RegisteredHook[];

/** @hidden place to store the hooks */
_registeredHooks: { [key: string]: RegisteredHook[] }
}

/** A predicate type which takes a [[State]] and returns a boolean */
Expand Down
32 changes: 21 additions & 11 deletions src/transition/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { TransitionStateHookFn, TransitionHookFn } from "./interface"; // has or is using

import {TransitionHook} from "./transitionHook";
import {matchState, RegisteredHooks, makeHookRegistrationFn, RegisteredHook} from "./hookRegistry";
import {matchState, RegisteredHooks, makeEvent, RegisteredHook} from "./hookRegistry";
import {HookBuilder} from "./hookBuilder";
import {PathNode} from "../path/node";
import {PathFactory} from "../path/pathFactory";
Expand Down Expand Up @@ -84,7 +84,7 @@ export class Transition implements IHookRegistry {
private _error: any;

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

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

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

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

if (this._options.reload || tc.entering.length || tc.exiting.length) return undefined;

let nodeSchemas: Param[][] = to.map((node: PathNode) => node.paramSchema);
let [toValues, fromValues] = [to, from].map(path => path.map(x => x.paramValues));
/** Return undefined if it's not a "dynamic" transition, for the following reasons */
// If user explicitly wants a reload
if (this._options.reload) return undefined;
// If any states are exiting or entering
if (tc.exiting.length || tc.entering.length) return undefined;
// If to/from path lengths differ
if (tc.to.length !== tc.from.length) return undefined;
// If the to/from paths are different
let pathsDiffer: boolean = arrayTuples(tc.to, tc.from)
.map(tuple => tuple[0].state !== tuple[1].state)
.reduce(anyTrueR, false);
if (pathsDiffer) return undefined;

// Find any parameter values that differ
let nodeSchemas: Param[][] = tc.to.map((node: PathNode) => node.paramSchema);
let [toValues, fromValues] = [tc.to, tc.from].map(path => path.map(x => x.paramValues));
let tuples = arrayTuples(nodeSchemas, toValues, fromValues);

return tuples.map(([schema, toVals, fromVals]) => Param.changed(schema, toVals, fromVals)).reduce(unnestR, []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import {GetErrorHandler, GetResultHandler, TransitionHook} from "./transitionHoo
* @interalapi
* @module transition
*/
export class TransitionHookType {
export class TransitionEventType {

constructor(public name: string,
public hookPhase: TransitionHookPhase,
public hookScope: TransitionHookScope,
public hookOrder: number,
public criteriaMatchPath: string,
public reverseSort: boolean = false,
public getResultHandler: GetResultHandler = TransitionHook.HANDLE_RESULT,
public getErrorHandler: GetErrorHandler = TransitionHook.REJECT_ERROR,
public getResultHandler: GetResultHandler = TransitionHook.HANDLE_RESULT,
public getErrorHandler: GetErrorHandler = TransitionHook.REJECT_ERROR,
public rejectIfSuperseded: boolean = true,
) { }
}
8 changes: 4 additions & 4 deletions src/transition/transitionHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {Rejection} from "./rejectFactory";
import {TargetState} from "../state/targetState";
import {Transition} from "./transition";
import {State} from "../state/stateObject";
import {TransitionHookType} from "./transitionHookType";
import {TransitionEventType} from "./transitionEventType";
import {StateService} from "../state/stateService"; // has or is using
import {RegisteredHook} from "./hookRegistry"; // has or is using

Expand Down Expand Up @@ -58,7 +58,7 @@ export class TransitionHook {
undefined;

private rejectIfSuperseded = () =>
this.registeredHook.hookType.rejectIfSuperseded && this.options.current() !== this.options.transition;
this.registeredHook.eventType.rejectIfSuperseded && this.options.current() !== this.options.transition;

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

let errorHandler = hook.hookType.getErrorHandler(this);
let resultHandler = hook.hookType.getResultHandler(this);
let errorHandler = hook.eventType.getErrorHandler(this);
let resultHandler = hook.eventType.getResultHandler(this);
resultHandler = resultHandler || identity;

if (!errorHandler) {
Expand Down
Loading

0 comments on commit 0dc2c19

Please sign in to comment.