Skip to content

Commit 2cb17ef

Browse files
feat(TransitionHook): Add hook registration option invokeLimit to limit the number of times a hook is invoked before being auto-deregistered.
1 parent c677f84 commit 2cb17ef

File tree

4 files changed

+68
-19
lines changed

4 files changed

+68
-19
lines changed

src/transition/hookRegistry.ts

+18-13
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import { extend, removeFrom, tail, values, identity, map } from "../common/commo
66
import {isString, isFunction} from "../common/predicates";
77
import {PathNode} from "../path/pathNode";
88
import {
9-
TransitionStateHookFn, TransitionHookFn, TransitionHookPhase, TransitionHookScope, IHookRegistry, PathType
9+
TransitionStateHookFn, TransitionHookFn, TransitionHookPhase, TransitionHookScope, IHookRegistry, PathType,
1010
} from "./interface"; // has or is using
1111

1212
import {
1313
HookRegOptions, HookMatchCriteria, TreeChanges,
14-
HookMatchCriterion, IMatchingNodes, HookFn
14+
HookMatchCriterion, IMatchingNodes, HookFn,
1515
} from "./interface";
1616
import {Glob} from "../common/glob";
1717
import {StateObject} from "../state/stateObject";
@@ -57,16 +57,19 @@ export function matchState(state: StateObject, criterion: HookMatchCriterion) {
5757
export class RegisteredHook {
5858
priority: number;
5959
bind: any;
60-
_deregistered: boolean;
60+
invokeCount = 0;
61+
invokeLimit: number;
62+
_deregistered = false;
6163

6264
constructor(public tranSvc: TransitionService,
6365
public eventType: TransitionEventType,
6466
public callback: HookFn,
6567
public matchCriteria: HookMatchCriteria,
68+
public removeHookFromRegistry: (hook: RegisteredHook) => void,
6669
options: HookRegOptions = {} as any) {
6770
this.priority = options.priority || 0;
6871
this.bind = options.bind || null;
69-
this._deregistered = false;
72+
this.invokeLimit = options.invokeLimit;
7073
}
7174

7275
/**
@@ -152,26 +155,28 @@ export class RegisteredHook {
152155
let allMatched = values(matches).every(identity);
153156
return allMatched ? matches : null;
154157
}
158+
159+
deregister() {
160+
this.removeHookFromRegistry(this);
161+
this._deregistered = true;
162+
}
155163
}
156164

157165
/** @hidden Return a registration function of the requested type. */
158166
export function makeEvent(registry: IHookRegistry, transitionService: TransitionService, eventType: TransitionEventType) {
159167
// Create the object which holds the registered transition hooks.
160-
let _registeredHooks = registry._registeredHooks = (registry._registeredHooks || {});
161-
let hooks = _registeredHooks[eventType.name] = [];
168+
const _registeredHooks = registry._registeredHooks = (registry._registeredHooks || {});
169+
const hooks = _registeredHooks[eventType.name] = [];
170+
const removeHookFn: (hook: RegisteredHook) => void = removeFrom(hooks);
162171

163172
// Create hook registration function on the IHookRegistry for the event
164173
registry[eventType.name] = hookRegistrationFn;
165174

166175
function hookRegistrationFn(matchObject, callback, options = {}) {
167-
let registeredHook = new RegisteredHook(transitionService, eventType, callback, matchObject, options);
176+
const registeredHook = new RegisteredHook(transitionService, eventType, callback, matchObject, removeHookFn, options);
168177
hooks.push(registeredHook);
169-
170-
return function deregisterEventHook() {
171-
registeredHook._deregistered = true;
172-
removeFrom(hooks)(registeredHook);
173-
};
178+
return registeredHook.deregister.bind(registeredHook);
174179
}
175180

176181
return hookRegistrationFn;
177-
}
182+
}

src/transition/interface.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,12 @@ export interface HookRegOptions {
262262
* Specifies what `this` is bound to during hook invocation.
263263
*/
264264
bind?: any;
265+
266+
/**
267+
* Limits the number of times that the hook will be invoked.
268+
* Once the hook has been invoked this many times, it is automatically deregistered.
269+
*/
270+
invokeLimit?: number;
265271
}
266272

267273
/**
@@ -443,7 +449,7 @@ export interface IHookRegistry {
443449
* Registers a lifecycle hook, which is invoked (during a transition) when a specific state is being entered.
444450
*
445451
* Since this hook is run only when the specific state is being *entered*, it can be useful for
446-
* performing tasks when entering a submodule/feature area such as initializing a stateful service,
452+
* performing tasks when entering a submodule/feature area such as initializing a stateful service,
447453
* or for guarding access to a submodule/feature area.
448454
*
449455
* See [[TransitionStateHookFn]] for the signature of the function.
@@ -470,7 +476,7 @@ export interface IHookRegistry {
470476
*
471477
* Instead of registering `onEnter` hooks using the [[TransitionService]], you may define an `onEnter` hook
472478
* directly on a state declaration (see: [[StateDeclaration.onEnter]]).
473-
*
479+
*
474480
*
475481
* ### Examples
476482
*
@@ -514,7 +520,7 @@ export interface IHookRegistry {
514520
*
515521
* Registers a lifecycle hook, which is invoked (during a transition) for
516522
* a specific state that was previously active will remain active (is not being entered nor exited).
517-
*
523+
*
518524
* This hook is invoked when a state is "retained" or "kept".
519525
* It means the transition is coming *from* a substate of the retained state *to* a substate of the retained state.
520526
* This hook can be used to perform actions when the user moves from one substate to another, such as between steps in a wizard.
@@ -553,7 +559,7 @@ export interface IHookRegistry {
553559
* Registers a lifecycle hook, which is invoked (during a transition) when a specific state is being exited.
554560
*
555561
* Since this hook is run only when the specific state is being *exited*, it can be useful for
556-
* performing tasks when leaving a submodule/feature area such as cleaning up a stateful service,
562+
* performing tasks when leaving a submodule/feature area such as cleaning up a stateful service,
557563
* or for preventing the user from leaving a state or submodule until some criteria is satisfied.
558564
*
559565
* See [[TransitionStateHookFn]] for the signature of the function.
@@ -671,7 +677,7 @@ export interface IHookRegistry {
671677
* - A resolve function returned a rejected promise
672678
*
673679
* To check the failure reason, inspect the return value of [[Transition.error]].
674-
*
680+
*
675681
* Note: `onError` should be used for targeted error handling, or error recovery.
676682
* For simple catch-all error reporting, use [[StateService.defaultErrorHandler]].
677683
*

src/transition/transitionHook.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ export class TransitionHook {
113113
} catch (err) {
114114
// If callback throws (synchronously)
115115
return handleError(Rejection.normalize(err));
116+
} finally {
117+
if (hook.invokeLimit && ++hook.invokeCount >= hook.invokeLimit) {
118+
hook.deregister();
119+
}
116120
}
117121
}
118122

@@ -243,4 +247,4 @@ export class TransitionHook {
243247
hooks.forEach(hook => hook.invokeHook());
244248
}
245249

246-
}
250+
}

test/transitionSpec.ts

+34
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,40 @@ describe('transition', function () {
705705

706706
done();
707707
});
708+
709+
it('should be invoked only once if invokeLimit is 1', (done) => {
710+
let count = 0;
711+
$transitions.onStart({ }, function() { count++; }, { invokeLimit: 1 });
712+
713+
Promise.resolve()
714+
.then(() => expect(count).toBe(0))
715+
.then(go("A", "D"))
716+
.then(() => expect(count).toBe(1))
717+
.then(go("D", "A"))
718+
.then(() => expect(count).toBe(1))
719+
.then(go("A", "D"))
720+
.then(() => expect(count).toBe(1))
721+
.then(done);
722+
});
723+
724+
it('should be invoked only twice if invokeLimit is 2', (done) => {
725+
let count = 0;
726+
$transitions.onStart({ }, () => {
727+
count++; }, { invokeLimit: 2 });
728+
729+
Promise.resolve()
730+
.then(() => expect(count).toBe(0))
731+
.then(go("A", "D"))
732+
.then(() =>
733+
expect(count).toBe(1))
734+
.then(go("D", "A"))
735+
.then(() =>
736+
expect(count).toBe(2))
737+
.then(go("A", "D"))
738+
.then(() =>
739+
expect(count).toBe(2))
740+
.then(done);
741+
});
708742
});
709743

710744
describe('redirected transition', () => {

0 commit comments

Comments
 (0)