Skip to content

Commit 44579ec

Browse files
feat(StateRegistry): Add deregister method.
feat(StateRegistry): Add state registered/deregistered callbacks. Closes #1095 Closes #2711
1 parent e7bedc2 commit 44579ec

File tree

5 files changed

+319
-11
lines changed

5 files changed

+319
-11
lines changed

src/state/stateQueueManager.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {StateBuilder} from "./stateBuilder";
77
import {StateService} from "./stateService";
88
import {UrlRouterProvider} from "../url/urlRouter";
99
import {RawParams} from "../params/interface";
10+
import {StateRegistry, StateRegistryListener} from "./stateRegistry";
1011

1112
export class StateQueueManager {
1213
queue: State[];
@@ -15,7 +16,8 @@ export class StateQueueManager {
1516
constructor(
1617
public states: { [key: string]: State; },
1718
public builder: StateBuilder,
18-
public $urlRouterProvider: UrlRouterProvider) {
19+
public $urlRouterProvider: UrlRouterProvider,
20+
public listeners: StateRegistryListener[]) {
1921
this.queue = [];
2022
}
2123

@@ -43,20 +45,22 @@ export class StateQueueManager {
4345

4446
flush($state: StateService) {
4547
let {queue, states, builder} = this;
46-
let result: State, state: State, orphans: State[] = [], orphanIdx: number,
47-
previousQueueLength = {};
48+
let registered: State[] = [], // states that got registered
49+
orphans: State[] = [], // states that dodn't yet have a parent registered
50+
previousQueueLength = {}; // keep track of how long the queue when an orphan was first encountered
4851

4952
while (queue.length > 0) {
50-
state = queue.shift();
51-
result = builder.build(state);
52-
orphanIdx = orphans.indexOf(state);
53+
let state: State = queue.shift();
54+
let result: State = builder.build(state);
55+
let orphanIdx: number = orphans.indexOf(state);
5356

5457
if (result) {
5558
if (states.hasOwnProperty(state.name))
5659
throw new Error(`State '${name}' is already defined`);
5760
states[state.name] = state;
5861
this.attachRoute($state, state);
5962
if (orphanIdx >= 0) orphans.splice(orphanIdx, 1);
63+
registered.push(state);
6064
continue;
6165
}
6266

@@ -65,13 +69,19 @@ export class StateQueueManager {
6569
if (orphanIdx >= 0 && prev === queue.length) {
6670
// Wait until two consecutive iterations where no additional states were dequeued successfully.
6771
// throw new Error(`Cannot register orphaned state '${state.name}'`);
72+
queue.push(state);
6873
return states;
6974
} else if (orphanIdx < 0) {
7075
orphans.push(state);
7176
}
7277

7378
queue.push(state);
7479
}
80+
81+
if (registered.length) {
82+
this.listeners.forEach(listener => listener("registered", registered.map(s => s.self)));
83+
}
84+
7585
return states;
7686
}
7787

src/state/stateRegistry.ts

+131-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ import {StateDeclaration} from "./interface";
99
import {BuilderFunction} from "./stateBuilder";
1010
import {StateOrName} from "./interface";
1111
import {UrlRouterProvider} from "../url/urlRouter";
12+
import {removeFrom} from "../common/common";
13+
14+
/**
15+
* The signature for the callback function provided to [[StateRegistry.onStateRegistryEvent]].
16+
*
17+
* This callback receives two parameters:
18+
*
19+
* @param event a string; either "registered" or "deregistered"
20+
* @param states the list of [[StateDeclaration]]s that were registered (or deregistered).
21+
*/
22+
export type StateRegistryListener = (event: "registered"|"deregistered", states: StateDeclaration[]) => void;
1223

1324
export class StateRegistry {
1425
private _root: State;
@@ -18,10 +29,12 @@ export class StateRegistry {
1829
private builder: StateBuilder;
1930
stateQueue: StateQueueManager;
2031

32+
listeners: StateRegistryListener[] = [];
33+
2134
constructor(urlMatcherFactory: UrlMatcherFactory, urlRouterProvider: UrlRouterProvider) {
2235
this.matcher = new StateMatcher(this.states);
2336
this.builder = new StateBuilder(this.matcher, urlMatcherFactory);
24-
this.stateQueue = new StateQueueManager(this.states, this.builder, urlRouterProvider);
37+
this.stateQueue = new StateQueueManager(this.states, this.builder, urlRouterProvider, this.listeners);
2538

2639
let rootStateDef: StateDeclaration = {
2740
name: '',
@@ -37,16 +50,131 @@ export class StateRegistry {
3750
_root.navigable = null;
3851
}
3952

53+
/**
54+
* Listen for a State Registry events
55+
*
56+
* Adds a callback that is invoked when states are registered or deregistered with the StateRegistry.
57+
*
58+
* @example
59+
* ```js
60+
*
61+
* let allStates = registry.get();
62+
*
63+
* // Later, invoke deregisterFn() to remove the listener
64+
* let deregisterFn = registry.onStatesChanged((event, states) => {
65+
* switch(event) {
66+
* case: 'registered':
67+
* states.forEach(state => allStates.push(state));
68+
* break;
69+
* case: 'deregistered':
70+
* states.forEach(state => {
71+
* let idx = allStates.indexOf(state);
72+
* if (idx !== -1) allStates.splice(idx, 1);
73+
* });
74+
* break;
75+
* }
76+
* });
77+
* ```
78+
*
79+
* @param listener a callback function invoked when the registered states changes.
80+
* The function receives two parameters, `event` and `state`.
81+
* See [[StateRegistryListener]]
82+
* @return a function that deregisters the listener
83+
*/
84+
onStatesChanged(listener: StateRegistryListener): () => void {
85+
this.listeners.push(listener);
86+
return function deregisterListener() {
87+
removeFrom(this.listeners)(listener);
88+
}.bind(this);
89+
}
90+
91+
/**
92+
* Gets the implicit root state
93+
*
94+
* Gets the root of the state tree.
95+
* The root state is implicitly created by UI-Router.
96+
* Note: this returns the internal [[State]] representation, not a [[StateDeclaration]]
97+
*
98+
* @return the root [[State]]
99+
*/
40100
root() {
41101
return this._root;
42102
}
43103

44-
register(stateDefinition: StateDeclaration) {
104+
/**
105+
* Adds a state to the registry
106+
*
107+
* Registers a [[StateDefinition]] or queues it for registration.
108+
*
109+
* Note: a state will be queued if the state's parent isn't yet registered.
110+
* It will also be queued if the queue is not yet in [[StateQueueManager.autoFlush]] mode.
111+
*
112+
* @param stateDefinition the definition of the state to register.
113+
* @returns the internal [[State]] object.
114+
* If the state was successfully registered, then the object is fully built (See: [[StateBuilder]]).
115+
* If the state was only queued, then the object is not fully built.
116+
*/
117+
register(stateDefinition: StateDeclaration): State {
45118
return this.stateQueue.register(stateDefinition);
46119
}
47120

121+
/** @hidden */
122+
private _deregisterTree(state: State) {
123+
let all = this.get().map(s => s.$$state());
124+
const getChildren = (states: State[]) => {
125+
let children = all.filter(s => states.indexOf(s.parent) !== -1);
126+
return children.length === 0 ? children : children.concat(getChildren(children));
127+
};
128+
129+
let children = getChildren([state]);
130+
let deregistered = [state].concat(children).reverse();
131+
132+
deregistered.forEach(state => {
133+
state.url && state.url.config.$$removeRule();
134+
delete this.states[state.name];
135+
});
136+
137+
return deregistered;
138+
}
139+
140+
/**
141+
* Removes a state from the registry
142+
*
143+
* This removes a state from the registry.
144+
* If the state has children, they are are also removed from the registry.
145+
*
146+
* @param stateOrName the state's name or object representation
147+
* @returns {State[]} a list of removed states
148+
*/
149+
deregister(stateOrName: StateOrName) {
150+
let _state = this.get(stateOrName);
151+
if (!_state) throw new Error("Can't deregister state; not found: " + stateOrName);
152+
let deregisteredStates = this._deregisterTree(_state.$$state());
153+
154+
this.listeners.forEach(listener => listener("deregistered", deregisteredStates.map(s => s.self)));
155+
return deregisteredStates;
156+
}
157+
158+
/**
159+
* Gets all registered states
160+
*
161+
* Calling this method with no arguments will return a list of all the states that are currently registered.
162+
* Note: this does not return states that are *queued* but not yet registered.
163+
*
164+
* @return a list of [[StateDeclaration]]s
165+
*/
48166
get(): StateDeclaration[];
49-
get(stateOrName: StateOrName, base: StateOrName): StateDeclaration;
167+
168+
/**
169+
* Gets a registered state
170+
*
171+
* Given a state or a name, finds and returns the [[StateDeclaration]] from the registry.
172+
* Note: this does not return states that are *queued* but not yet registered.
173+
*
174+
* @param stateOrName either the name of a state, or a state object.
175+
* @return a registered [[StateDeclaration]] that matched the `stateOrName`, or null if the state isn't registered.
176+
*/
177+
get(stateOrName: StateOrName, base?: StateOrName): StateDeclaration;
50178
get(stateOrName?: StateOrName, base?: StateOrName): any {
51179
if (arguments.length === 0)
52180
return <StateDeclaration[]> Object.keys(this.states).map(name => this.states[name].self);

src/url/urlRouter.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,15 @@ export class UrlRouterProvider {
125125
return this;
126126
};
127127

128+
/** @hidden */
129+
private $$removeRule(rule) {
130+
let idx = this.rules.indexOf(rule);
131+
if (idx !== -1) {
132+
this.rules.splice(idx, 1);
133+
}
134+
return (idx !== -1);
135+
}
136+
128137
/**
129138
* Defines the path or behavior to use when no url can be matched.
130139
*
@@ -239,7 +248,13 @@ export class UrlRouterProvider {
239248
};
240249

241250
for (var n in check) {
242-
if (check[n]) return this.rule(strategies[n](what, handler));
251+
if (check[n]) {
252+
let rule = strategies[n](what, handler);
253+
if (check.matcher && what['config']) {
254+
what['config'].$$removeRule = () => this.$$removeRule(rule);
255+
}
256+
return this.rule(rule);
257+
}
243258
}
244259

245260
throw new Error("invalid 'what' in when()");

0 commit comments

Comments
 (0)