Skip to content

Commit

Permalink
feat(onCreate): Add onCreate transition hook
Browse files Browse the repository at this point in the history
refactor(TransitionHook): Add HookType to TransitionHook object
refactor(TransitionHook): Use strategies for handler/error logic
refactor(TransitionHookType): Store result and error handler strategieson the hook type object

BREAKING CHANGE: Hook errors are all normalized to a "Rejection" type.  To access the  detail of the error thrown (`throw "Error 123"`), use `.detail`, i.e.:
### Before
```js
$state.go('foo').catch(err => { if (err === "Error 123") .. });
```
### New way
```js
$state.go('foo').catch(err => { if (err.detail === "Error 123") .. });
```
  • Loading branch information
christopherthielen committed Nov 29, 2016
1 parent 3f146e6 commit f486ced
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 66 deletions.
2 changes: 1 addition & 1 deletion karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ module.exports = function (karma) {
],

webpack: {
devtool: 'source-map',
devtool: 'inline-source-map',

resolve: {
modulesDirectories: ['node_modules'],
Expand Down
20 changes: 12 additions & 8 deletions src/transition/hookBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class HookBuilder {
*/
buildHooks(hookType: TransitionHookType): TransitionHook[] {
// Find all the matching registered hooks for a given hook type
let matchingHooks = this._matchingHooks(hookType.name, this.treeChanges);
let matchingHooks = this.getMatchingHooks(hookType, this.treeChanges);
if (!matchingHooks) return [];

const makeTransitionHooks = (hook: IEventHook) => {
Expand All @@ -87,7 +87,7 @@ export class HookBuilder {
}, this.baseHookOptions);

let state = hookType.hookScope === TransitionHookScope.STATE ? node.state : null;
let transitionHook = new TransitionHook(this.transition, state, hook, _options);
let transitionHook = new TransitionHook(this.transition, state, hook, hookType, _options);
return <HookTuple> { hook, node, transitionHook };
});
};
Expand All @@ -109,12 +109,16 @@ export class HookBuilder {
*
* @returns an array of matched [[IEventHook]]s
*/
private _matchingHooks(hookName: string, treeChanges: TreeChanges): IEventHook[] {
return [ this.transition, this.$transitions ] // Instance and Global hook registries
.map((reg: IHookRegistry) => reg.getHooks(hookName)) // Get named hooks from registries
.filter(assertPredicate(isArray, `broken event named: ${hookName}`)) // Sanity check
.reduce(unnestR, []) // Un-nest IEventHook[][] to IEventHook[] array
.filter(hook => hook.matches(treeChanges)); // Only those satisfying matchCriteria
public getMatchingHooks(hookType: TransitionHookType, treeChanges: TreeChanges): IEventHook[] {
let isCreate = hookType.hookPhase === TransitionHookPhase.CREATE;

// Instance and Global hook registries
let registries = isCreate ? [ this.$transitions ] : [ this.transition, this.$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 IEventHook[][] to IEventHook[] array
.filter(hook => hook.matches(treeChanges)); // Only those satisfying matchCriteria
}
}

Expand Down
18 changes: 14 additions & 4 deletions src/transition/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,6 @@ export interface TreeChanges {
entering: PathNode[];
}

export type IErrorHandler = (error: Error) => void;

export type IHookGetter = (hookName: string) => IEventHook[];
export type IHookRegistration = (matchCriteria: HookMatchCriteria, callback: HookFn, options?: HookRegOptions) => Function;

/**
Expand Down Expand Up @@ -214,7 +211,20 @@ export interface TransitionStateHookFn {
(transition: Transition, state: State) : HookResult
}

export type HookFn = (TransitionHookFn|TransitionStateHookFn);
/**
* The signature for Transition onCreate Hooks.
*
* Transition onCreate Hooks are callbacks that allow customization or preprocessing of
* a Transition before it is returned from [[TransitionService.create]]
*
* @param transition the [[Transition]] that was just created
* @return a [[Transition]] which will then be returned from [[TransitionService.create]]
*/
export interface TransitionCreateHookFn {
(transition: Transition): void
}

export type HookFn = (TransitionHookFn|TransitionStateHookFn|TransitionCreateHookFn);

/**
* The return value of a [[TransitionHookFn]] or [[TransitionStateHookFn]]
Expand Down
24 changes: 18 additions & 6 deletions src/transition/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { isObject, isArray } from "../common/predicates";
import { prop, propEq, val, not } from "../common/hof";

import {StateDeclaration, StateOrName} from "../state/interface";
import { TransitionOptions, TreeChanges, IHookRegistry, IEventHook, TransitionHookPhase } from "./interface";
import {
TransitionOptions, TreeChanges, IHookRegistry, IEventHook, TransitionHookPhase,
TransitionCreateHookFn
} from "./interface";

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

Expand Down Expand Up @@ -150,9 +153,21 @@ export class Transition implements IHookRegistry {
this.$id = transitionCount++;
let toPath = PathFactory.buildToPath(fromPath, targetState);
this._treeChanges = PathFactory.treeChanges(fromPath, toPath, this._options.reloadState);
this.createTransitionHookRegFns();

let onCreateHooks = this.hookBuilder().buildHooksForPhase(TransitionHookPhase.CREATE);
TransitionHook.runAllHooks(onCreateHooks);

this.applyViewConfigs(router);
this.applyRootResolvables(router);
}

private applyViewConfigs(router: UIRouter) {
let enteringStates = this._treeChanges.entering.map(node => node.state);
PathFactory.applyViewConfigs(router.transitionService.$view, this._treeChanges.to, enteringStates);
}

private applyRootResolvables(router: UIRouter) {
let rootResolvables: Resolvable[] = [
new Resolvable(UIRouter, () => router, [], undefined, router),
new Resolvable(Transition, () => this, [], undefined, this),
Expand All @@ -163,8 +178,6 @@ 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 @@ -548,14 +561,13 @@ export class Transition implements IHookRegistry {
* @returns a promise for a successful transition.
*/
run(): Promise<any> {
let runSynchronousHooks = TransitionHook.runSynchronousHooks;
let runAllHooks = TransitionHook.runAllHooks;
let hookBuilder = this.hookBuilder();
let globals = <Globals> this.router.globals;
globals.transitionHistory.enqueue(this);

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

if (Rejection.isTransitionRejectionPromise(syncResult)) {
syncResult.catch(() => 0); // issue #2676
Expand Down Expand Up @@ -601,7 +613,7 @@ export class Transition implements IHookRegistry {
prev.then(() => nextHook.invokeHook());

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

asyncHooks.reduce(appendHookToChain, syncResult)
.then(transitionSuccess, transitionError);
Expand Down
102 changes: 70 additions & 32 deletions src/transition/transitionHook.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
/** @coreapi @module transition */ /** for typedoc */
import {TransitionHookOptions, IEventHook, HookResult} from "./interface";
import {defaults, noop} from "../common/common";
import {defaults, noop, identity} from "../common/common";
import {fnToString, maxLength} from "../common/strings";
import {isPromise} from "../common/predicates";
import {val, is, parse } from "../common/hof";
import {val, is, parse} from "../common/hof";
import {trace} from "../common/trace";
import {services} from "../common/coreservices";

import {Rejection} from "./rejectFactory";
import {TargetState} from "../state/targetState";
import {Transition} from "./transition";
import {State} from "../state/stateObject";
import {StateService} from "../state/stateService"; // has or is using
import {TransitionHookType} from "./transitionHookType"; // has or is using

let defaultOptions: TransitionHookOptions = {
async: true,
Expand All @@ -21,30 +23,77 @@ let defaultOptions: TransitionHookOptions = {
bind: null
};

export type GetResultHandler = (hook: TransitionHook) => ResultHandler;
export type GetErrorHandler = (hook: TransitionHook) => ErrorHandler;

export type ResultHandler = (result: HookResult) => Promise<HookResult>;
export type ErrorHandler = (error) => Promise<any>;

/** @hidden */
export class TransitionHook {
constructor(private transition: Transition,
private stateContext: State,
private eventHook: IEventHook,
private hookType: TransitionHookType,
private options: TransitionHookOptions) {
this.options = defaults(options, defaultOptions);
}

private isSuperseded = () =>
this.options.current() !== this.options.transition;
stateService = () => this.transition.router.stateService;

static HANDLE_RESULT: GetResultHandler = (hook: TransitionHook) =>
(result: HookResult) =>
hook.handleHookResult(result);

static IGNORE_RESULT: GetResultHandler = (hook: TransitionHook) =>
(result: HookResult) => undefined;

static LOG_ERROR: GetErrorHandler = (hook: TransitionHook) =>
(error) =>
(hook.stateService().defaultErrorHandler()(error), undefined);

static REJECT_ERROR: GetErrorHandler = (hook: TransitionHook) =>
(error) =>
Rejection.errored(error).toPromise();

static THROW_ERROR: GetErrorHandler = (hook: TransitionHook) =>
undefined;

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

invokeHook(): Promise<HookResult> {
let { options, eventHook } = this;
if (this.eventHook._deregistered) return;

let options = this.options;
trace.traceHookInvocation(this, options);
if (options.rejectIfSuperseded && this.isSuperseded()) {

if (this.rejectForSuperseded()) {
return Rejection.superseded(options.current()).toPromise();
}

let synchronousHookResult = !eventHook._deregistered
? eventHook.callback.call(options.bind, this.transition, this.stateContext)
: undefined;
let errorHandler = this.hookType.errorHandler(this);
let resultHandler = this.hookType.resultHandler(this);

return this.handleHookResult(synchronousHookResult);
return this._invokeCallback(resultHandler, errorHandler);
}

private _invokeCallback(resultHandler: ResultHandler, errorHandler: ErrorHandler): Promise<HookResult> {
let cb = this.eventHook.callback;
let bind = this.options.bind;
let trans = this.transition;
let state = this.stateContext;
resultHandler = resultHandler || identity;

if (!errorHandler) {
return resultHandler(cb.call(bind, trans, state));
}

try {
return resultHandler(cb.call(bind, trans, state));
} catch (error) {
return errorHandler(error);
}
}

/**
Expand All @@ -56,10 +105,10 @@ export class TransitionHook {
* This also handles "transition superseded" -- when a new transition
* was started while the hook was still running
*/
handleHookResult(result: HookResult): Promise<any> {
handleHookResult(result: HookResult): Promise<HookResult> {
// This transition is no longer current.
// Another transition started while this hook was still running.
if (this.isSuperseded()) {
if (this.rejectForSuperseded()) {
// Abort this transition
return Rejection.superseded(this.options.current()).toPromise();
}
Expand Down Expand Up @@ -98,14 +147,7 @@ export class TransitionHook {
* Run all TransitionHooks, ignoring their return value.
*/
static runAllHooks(hooks: TransitionHook[]): void {
hooks.forEach(hook => {
try {
hook.invokeHook();
} catch (exception) {
let errorHandler = hook.transition.router.stateService.defaultErrorHandler();
errorHandler(exception);
}
});
hooks.forEach(hook => hook.invokeHook());
}

/**
Expand All @@ -114,22 +156,18 @@ export class TransitionHook {
*
* Returns a promise chain composed of any promises returned from each hook.invokeStep() call
*/
static runSynchronousHooks(hooks: TransitionHook[]): Promise<any> {
static runOnBeforeHooks(hooks: TransitionHook[]): Promise<any> {
let results: Promise<HookResult>[] = [];

for (let hook of hooks) {
try {
let hookResult = hook.invokeHook();

if (Rejection.isTransitionRejectionPromise(hookResult)) {
// Break on first thrown error or false/TargetState
return hookResult;
}

results.push(hookResult);
} catch (exception) {
return Rejection.errored(exception).toPromise();
let hookResult = hook.invokeHook();

if (Rejection.isTransitionRejectionPromise(hookResult)) {
// Break on first thrown error or false/TargetState
return hookResult;
}

results.push(hookResult);
}

return results
Expand Down
16 changes: 13 additions & 3 deletions src/transition/transitionHookType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {TransitionHookScope, TransitionHookPhase} from "./interface";
import {PathNode} from "../path/node";
import {Transition} from "./transition";
import {isString} from "../common/predicates";
import {GetErrorHandler, GetResultHandler, TransitionHook} from "./transitionHook";
/**
* 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`.
Expand All @@ -12,20 +13,26 @@ import {isString} from "../common/predicates";
export class TransitionHookType {

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

constructor(name: string,
hookScope: TransitionHookScope,
hookPhase: TransitionHookPhase,
hookScope: TransitionHookScope,
hookOrder: number,
criteriaMatchPath: string,
resolvePath: ((trans: Transition) => PathNode[]) | string,
reverseSort: boolean = false
reverseSort: boolean = false,
resultHandler: GetResultHandler = TransitionHook.HANDLE_RESULT,
errorHandler: GetErrorHandler = TransitionHook.REJECT_ERROR,
rejectIfSuperseded: boolean = true,
) {
this.name = name;
this.hookScope = hookScope;
Expand All @@ -34,5 +41,8 @@ export class TransitionHookType {
this.criteriaMatchPath = criteriaMatchPath;
this.resolvePath = isString(resolvePath) ? (trans: Transition) => trans.treeChanges(resolvePath) : resolvePath;
this.reverseSort = reverseSort;
this.resultHandler = resultHandler;
this.errorHandler = errorHandler;
this.rejectIfSuperseded = rejectIfSuperseded;
}
}
Loading

0 comments on commit f486ced

Please sign in to comment.