Skip to content

Commit f486ced

Browse files
feat(onCreate): Add onCreate transition hook
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") .. }); ```
1 parent 3f146e6 commit f486ced

8 files changed

+228
-66
lines changed

karma.conf.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ module.exports = function (karma) {
3434
],
3535

3636
webpack: {
37-
devtool: 'source-map',
37+
devtool: 'inline-source-map',
3838

3939
resolve: {
4040
modulesDirectories: ['node_modules'],

src/transition/hookBuilder.ts

+12-8
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export class HookBuilder {
7070
*/
7171
buildHooks(hookType: TransitionHookType): TransitionHook[] {
7272
// Find all the matching registered hooks for a given hook type
73-
let matchingHooks = this._matchingHooks(hookType.name, this.treeChanges);
73+
let matchingHooks = this.getMatchingHooks(hookType, this.treeChanges);
7474
if (!matchingHooks) return [];
7575

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

8989
let state = hookType.hookScope === TransitionHookScope.STATE ? node.state : null;
90-
let transitionHook = new TransitionHook(this.transition, state, hook, _options);
90+
let transitionHook = new TransitionHook(this.transition, state, hook, hookType, _options);
9191
return <HookTuple> { hook, node, transitionHook };
9292
});
9393
};
@@ -109,12 +109,16 @@ export class HookBuilder {
109109
*
110110
* @returns an array of matched [[IEventHook]]s
111111
*/
112-
private _matchingHooks(hookName: string, treeChanges: TreeChanges): IEventHook[] {
113-
return [ this.transition, this.$transitions ] // Instance and Global hook registries
114-
.map((reg: IHookRegistry) => reg.getHooks(hookName)) // Get named hooks from registries
115-
.filter(assertPredicate(isArray, `broken event named: ${hookName}`)) // Sanity check
116-
.reduce(unnestR, []) // Un-nest IEventHook[][] to IEventHook[] array
117-
.filter(hook => hook.matches(treeChanges)); // Only those satisfying matchCriteria
112+
public getMatchingHooks(hookType: TransitionHookType, treeChanges: TreeChanges): IEventHook[] {
113+
let isCreate = hookType.hookPhase === TransitionHookPhase.CREATE;
114+
115+
// Instance and Global hook registries
116+
let registries = isCreate ? [ this.$transitions ] : [ this.transition, this.$transitions ];
117+
118+
return registries.map((reg: IHookRegistry) => reg.getHooks(hookType.name)) // Get named hooks from registries
119+
.filter(assertPredicate(isArray, `broken event named: ${hookType.name}`)) // Sanity check
120+
.reduce(unnestR, []) // Un-nest IEventHook[][] to IEventHook[] array
121+
.filter(hook => hook.matches(treeChanges)); // Only those satisfying matchCriteria
118122
}
119123
}
120124

src/transition/interface.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,6 @@ export interface TreeChanges {
158158
entering: PathNode[];
159159
}
160160

161-
export type IErrorHandler = (error: Error) => void;
162-
163-
export type IHookGetter = (hookName: string) => IEventHook[];
164161
export type IHookRegistration = (matchCriteria: HookMatchCriteria, callback: HookFn, options?: HookRegOptions) => Function;
165162

166163
/**
@@ -214,7 +211,20 @@ export interface TransitionStateHookFn {
214211
(transition: Transition, state: State) : HookResult
215212
}
216213

217-
export type HookFn = (TransitionHookFn|TransitionStateHookFn);
214+
/**
215+
* The signature for Transition onCreate Hooks.
216+
*
217+
* Transition onCreate Hooks are callbacks that allow customization or preprocessing of
218+
* a Transition before it is returned from [[TransitionService.create]]
219+
*
220+
* @param transition the [[Transition]] that was just created
221+
* @return a [[Transition]] which will then be returned from [[TransitionService.create]]
222+
*/
223+
export interface TransitionCreateHookFn {
224+
(transition: Transition): void
225+
}
226+
227+
export type HookFn = (TransitionHookFn|TransitionStateHookFn|TransitionCreateHookFn);
218228

219229
/**
220230
* The return value of a [[TransitionHookFn]] or [[TransitionStateHookFn]]

src/transition/transition.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ 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 { TransitionOptions, TreeChanges, IHookRegistry, IEventHook, TransitionHookPhase } from "./interface";
13+
import {
14+
TransitionOptions, TreeChanges, IHookRegistry, IEventHook, TransitionHookPhase,
15+
TransitionCreateHookFn
16+
} from "./interface";
1417

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

@@ -150,9 +153,21 @@ export class Transition implements IHookRegistry {
150153
this.$id = transitionCount++;
151154
let toPath = PathFactory.buildToPath(fromPath, targetState);
152155
this._treeChanges = PathFactory.treeChanges(fromPath, toPath, this._options.reloadState);
156+
this.createTransitionHookRegFns();
157+
158+
let onCreateHooks = this.hookBuilder().buildHooksForPhase(TransitionHookPhase.CREATE);
159+
TransitionHook.runAllHooks(onCreateHooks);
160+
161+
this.applyViewConfigs(router);
162+
this.applyRootResolvables(router);
163+
}
164+
165+
private applyViewConfigs(router: UIRouter) {
153166
let enteringStates = this._treeChanges.entering.map(node => node.state);
154167
PathFactory.applyViewConfigs(router.transitionService.$view, this._treeChanges.to, enteringStates);
168+
}
155169

170+
private applyRootResolvables(router: UIRouter) {
156171
let rootResolvables: Resolvable[] = [
157172
new Resolvable(UIRouter, () => router, [], undefined, router),
158173
new Resolvable(Transition, () => this, [], undefined, this),
@@ -163,8 +178,6 @@ export class Transition implements IHookRegistry {
163178
let rootNode: PathNode = this._treeChanges.to[0];
164179
let context = new ResolveContext(this._treeChanges.to);
165180
context.addResolvables(rootResolvables, rootNode.state);
166-
167-
this.createTransitionHookRegFns();
168181
}
169182

170183
/**
@@ -548,14 +561,13 @@ export class Transition implements IHookRegistry {
548561
* @returns a promise for a successful transition.
549562
*/
550563
run(): Promise<any> {
551-
let runSynchronousHooks = TransitionHook.runSynchronousHooks;
552564
let runAllHooks = TransitionHook.runAllHooks;
553565
let hookBuilder = this.hookBuilder();
554566
let globals = <Globals> this.router.globals;
555567
globals.transitionHistory.enqueue(this);
556568

557569
let onBeforeHooks = hookBuilder.buildHooksForPhase(TransitionHookPhase.BEFORE);
558-
let syncResult = runSynchronousHooks(onBeforeHooks);
570+
let syncResult = TransitionHook.runOnBeforeHooks(onBeforeHooks);
559571

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

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

606618
asyncHooks.reduce(appendHookToChain, syncResult)
607619
.then(transitionSuccess, transitionError);

src/transition/transitionHook.ts

+70-32
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
/** @coreapi @module transition */ /** for typedoc */
22
import {TransitionHookOptions, IEventHook, HookResult} from "./interface";
3-
import {defaults, noop} from "../common/common";
3+
import {defaults, noop, identity} from "../common/common";
44
import {fnToString, maxLength} from "../common/strings";
55
import {isPromise} from "../common/predicates";
6-
import {val, is, parse } from "../common/hof";
6+
import {val, is, parse} from "../common/hof";
77
import {trace} from "../common/trace";
88
import {services} from "../common/coreservices";
99

1010
import {Rejection} from "./rejectFactory";
1111
import {TargetState} from "../state/targetState";
1212
import {Transition} from "./transition";
1313
import {State} from "../state/stateObject";
14+
import {StateService} from "../state/stateService"; // has or is using
15+
import {TransitionHookType} from "./transitionHookType"; // has or is using
1416

1517
let defaultOptions: TransitionHookOptions = {
1618
async: true,
@@ -21,30 +23,77 @@ let defaultOptions: TransitionHookOptions = {
2123
bind: null
2224
};
2325

26+
export type GetResultHandler = (hook: TransitionHook) => ResultHandler;
27+
export type GetErrorHandler = (hook: TransitionHook) => ErrorHandler;
28+
29+
export type ResultHandler = (result: HookResult) => Promise<HookResult>;
30+
export type ErrorHandler = (error) => Promise<any>;
31+
2432
/** @hidden */
2533
export class TransitionHook {
2634
constructor(private transition: Transition,
2735
private stateContext: State,
2836
private eventHook: IEventHook,
37+
private hookType: TransitionHookType,
2938
private options: TransitionHookOptions) {
3039
this.options = defaults(options, defaultOptions);
3140
}
3241

33-
private isSuperseded = () =>
34-
this.options.current() !== this.options.transition;
42+
stateService = () => this.transition.router.stateService;
43+
44+
static HANDLE_RESULT: GetResultHandler = (hook: TransitionHook) =>
45+
(result: HookResult) =>
46+
hook.handleHookResult(result);
47+
48+
static IGNORE_RESULT: GetResultHandler = (hook: TransitionHook) =>
49+
(result: HookResult) => undefined;
50+
51+
static LOG_ERROR: GetErrorHandler = (hook: TransitionHook) =>
52+
(error) =>
53+
(hook.stateService().defaultErrorHandler()(error), undefined);
54+
55+
static REJECT_ERROR: GetErrorHandler = (hook: TransitionHook) =>
56+
(error) =>
57+
Rejection.errored(error).toPromise();
58+
59+
static THROW_ERROR: GetErrorHandler = (hook: TransitionHook) =>
60+
undefined;
61+
62+
private rejectForSuperseded = () =>
63+
this.hookType.rejectIfSuperseded && this.options.current() !== this.options.transition;
3564

3665
invokeHook(): Promise<HookResult> {
37-
let { options, eventHook } = this;
66+
if (this.eventHook._deregistered) return;
67+
68+
let options = this.options;
3869
trace.traceHookInvocation(this, options);
39-
if (options.rejectIfSuperseded && this.isSuperseded()) {
70+
71+
if (this.rejectForSuperseded()) {
4072
return Rejection.superseded(options.current()).toPromise();
4173
}
4274

43-
let synchronousHookResult = !eventHook._deregistered
44-
? eventHook.callback.call(options.bind, this.transition, this.stateContext)
45-
: undefined;
75+
let errorHandler = this.hookType.errorHandler(this);
76+
let resultHandler = this.hookType.resultHandler(this);
4677

47-
return this.handleHookResult(synchronousHookResult);
78+
return this._invokeCallback(resultHandler, errorHandler);
79+
}
80+
81+
private _invokeCallback(resultHandler: ResultHandler, errorHandler: ErrorHandler): Promise<HookResult> {
82+
let cb = this.eventHook.callback;
83+
let bind = this.options.bind;
84+
let trans = this.transition;
85+
let state = this.stateContext;
86+
resultHandler = resultHandler || identity;
87+
88+
if (!errorHandler) {
89+
return resultHandler(cb.call(bind, trans, state));
90+
}
91+
92+
try {
93+
return resultHandler(cb.call(bind, trans, state));
94+
} catch (error) {
95+
return errorHandler(error);
96+
}
4897
}
4998

5099
/**
@@ -56,10 +105,10 @@ export class TransitionHook {
56105
* This also handles "transition superseded" -- when a new transition
57106
* was started while the hook was still running
58107
*/
59-
handleHookResult(result: HookResult): Promise<any> {
108+
handleHookResult(result: HookResult): Promise<HookResult> {
60109
// This transition is no longer current.
61110
// Another transition started while this hook was still running.
62-
if (this.isSuperseded()) {
111+
if (this.rejectForSuperseded()) {
63112
// Abort this transition
64113
return Rejection.superseded(this.options.current()).toPromise();
65114
}
@@ -98,14 +147,7 @@ export class TransitionHook {
98147
* Run all TransitionHooks, ignoring their return value.
99148
*/
100149
static runAllHooks(hooks: TransitionHook[]): void {
101-
hooks.forEach(hook => {
102-
try {
103-
hook.invokeHook();
104-
} catch (exception) {
105-
let errorHandler = hook.transition.router.stateService.defaultErrorHandler();
106-
errorHandler(exception);
107-
}
108-
});
150+
hooks.forEach(hook => hook.invokeHook());
109151
}
110152

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

120162
for (let hook of hooks) {
121-
try {
122-
let hookResult = hook.invokeHook();
123-
124-
if (Rejection.isTransitionRejectionPromise(hookResult)) {
125-
// Break on first thrown error or false/TargetState
126-
return hookResult;
127-
}
128-
129-
results.push(hookResult);
130-
} catch (exception) {
131-
return Rejection.errored(exception).toPromise();
163+
let hookResult = hook.invokeHook();
164+
165+
if (Rejection.isTransitionRejectionPromise(hookResult)) {
166+
// Break on first thrown error or false/TargetState
167+
return hookResult;
132168
}
169+
170+
results.push(hookResult);
133171
}
134172

135173
return results

src/transition/transitionHookType.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {TransitionHookScope, TransitionHookPhase} from "./interface";
22
import {PathNode} from "../path/node";
33
import {Transition} from "./transition";
44
import {isString} from "../common/predicates";
5+
import {GetErrorHandler, GetResultHandler, TransitionHook} from "./transitionHook";
56
/**
67
* This class defines a type of hook, such as `onBefore` or `onEnter`.
78
* Plugins can define custom hook types, such as sticky states does for `onInactive`.
@@ -12,20 +13,26 @@ import {isString} from "../common/predicates";
1213
export class TransitionHookType {
1314

1415
public name: string;
15-
public hookScope: TransitionHookScope;
1616
public hookPhase: TransitionHookPhase;
17+
public hookScope: TransitionHookScope;
1718
public hookOrder: number;
1819
public criteriaMatchPath: string;
1920
public resolvePath: (trans: Transition) => PathNode[];
2021
public reverseSort: boolean;
22+
public errorHandler: GetErrorHandler;
23+
public resultHandler: GetResultHandler;
24+
public rejectIfSuperseded: boolean;
2125

2226
constructor(name: string,
23-
hookScope: TransitionHookScope,
2427
hookPhase: TransitionHookPhase,
28+
hookScope: TransitionHookScope,
2529
hookOrder: number,
2630
criteriaMatchPath: string,
2731
resolvePath: ((trans: Transition) => PathNode[]) | string,
28-
reverseSort: boolean = false
32+
reverseSort: boolean = false,
33+
resultHandler: GetResultHandler = TransitionHook.HANDLE_RESULT,
34+
errorHandler: GetErrorHandler = TransitionHook.REJECT_ERROR,
35+
rejectIfSuperseded: boolean = true,
2936
) {
3037
this.name = name;
3138
this.hookScope = hookScope;
@@ -34,5 +41,8 @@ export class TransitionHookType {
3441
this.criteriaMatchPath = criteriaMatchPath;
3542
this.resolvePath = isString(resolvePath) ? (trans: Transition) => trans.treeChanges(resolvePath) : resolvePath;
3643
this.reverseSort = reverseSort;
44+
this.resultHandler = resultHandler;
45+
this.errorHandler = errorHandler;
46+
this.rejectIfSuperseded = rejectIfSuperseded;
3747
}
3848
}

0 commit comments

Comments
 (0)