Skip to content

Commit

Permalink
feat(StateRegistry): Add deregister method.
Browse files Browse the repository at this point in the history
feat(StateRegistry): Add state registered/deregistered callbacks.
Closes #1095
Closes #2711
  • Loading branch information
christopherthielen committed Aug 31, 2016
1 parent e7bedc2 commit 44579ec
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 11 deletions.
22 changes: 16 additions & 6 deletions src/state/stateQueueManager.ts
Expand Up @@ -7,6 +7,7 @@ import {StateBuilder} from "./stateBuilder";
import {StateService} from "./stateService";
import {UrlRouterProvider} from "../url/urlRouter";
import {RawParams} from "../params/interface";
import {StateRegistry, StateRegistryListener} from "./stateRegistry";

export class StateQueueManager {
queue: State[];
Expand All @@ -15,7 +16,8 @@ export class StateQueueManager {
constructor(
public states: { [key: string]: State; },
public builder: StateBuilder,
public $urlRouterProvider: UrlRouterProvider) {
public $urlRouterProvider: UrlRouterProvider,
public listeners: StateRegistryListener[]) {
this.queue = [];
}

Expand Down Expand Up @@ -43,20 +45,22 @@ export class StateQueueManager {

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

while (queue.length > 0) {
state = queue.shift();
result = builder.build(state);
orphanIdx = orphans.indexOf(state);
let state: State = queue.shift();
let result: State = builder.build(state);
let orphanIdx: number = orphans.indexOf(state);

if (result) {
if (states.hasOwnProperty(state.name))
throw new Error(`State '${name}' is already defined`);
states[state.name] = state;
this.attachRoute($state, state);
if (orphanIdx >= 0) orphans.splice(orphanIdx, 1);
registered.push(state);
continue;
}

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

queue.push(state);
}

if (registered.length) {
this.listeners.forEach(listener => listener("registered", registered.map(s => s.self)));
}

return states;
}

Expand Down
134 changes: 131 additions & 3 deletions src/state/stateRegistry.ts
Expand Up @@ -9,6 +9,17 @@ import {StateDeclaration} from "./interface";
import {BuilderFunction} from "./stateBuilder";
import {StateOrName} from "./interface";
import {UrlRouterProvider} from "../url/urlRouter";
import {removeFrom} from "../common/common";

/**
* The signature for the callback function provided to [[StateRegistry.onStateRegistryEvent]].
*
* This callback receives two parameters:
*
* @param event a string; either "registered" or "deregistered"
* @param states the list of [[StateDeclaration]]s that were registered (or deregistered).
*/
export type StateRegistryListener = (event: "registered"|"deregistered", states: StateDeclaration[]) => void;

export class StateRegistry {
private _root: State;
Expand All @@ -18,10 +29,12 @@ export class StateRegistry {
private builder: StateBuilder;
stateQueue: StateQueueManager;

listeners: StateRegistryListener[] = [];

constructor(urlMatcherFactory: UrlMatcherFactory, urlRouterProvider: UrlRouterProvider) {
this.matcher = new StateMatcher(this.states);
this.builder = new StateBuilder(this.matcher, urlMatcherFactory);
this.stateQueue = new StateQueueManager(this.states, this.builder, urlRouterProvider);
this.stateQueue = new StateQueueManager(this.states, this.builder, urlRouterProvider, this.listeners);

let rootStateDef: StateDeclaration = {
name: '',
Expand All @@ -37,16 +50,131 @@ export class StateRegistry {
_root.navigable = null;
}

/**
* Listen for a State Registry events
*
* Adds a callback that is invoked when states are registered or deregistered with the StateRegistry.
*
* @example
* ```js
*
* let allStates = registry.get();
*
* // Later, invoke deregisterFn() to remove the listener
* let deregisterFn = registry.onStatesChanged((event, states) => {
* switch(event) {
* case: 'registered':
* states.forEach(state => allStates.push(state));
* break;
* case: 'deregistered':
* states.forEach(state => {
* let idx = allStates.indexOf(state);
* if (idx !== -1) allStates.splice(idx, 1);
* });
* break;
* }
* });
* ```
*
* @param listener a callback function invoked when the registered states changes.
* The function receives two parameters, `event` and `state`.
* See [[StateRegistryListener]]
* @return a function that deregisters the listener
*/
onStatesChanged(listener: StateRegistryListener): () => void {
this.listeners.push(listener);
return function deregisterListener() {
removeFrom(this.listeners)(listener);
}.bind(this);
}

/**
* Gets the implicit root state
*
* Gets the root of the state tree.
* The root state is implicitly created by UI-Router.
* Note: this returns the internal [[State]] representation, not a [[StateDeclaration]]
*
* @return the root [[State]]
*/
root() {
return this._root;
}

register(stateDefinition: StateDeclaration) {
/**
* Adds a state to the registry
*
* Registers a [[StateDefinition]] or queues it for registration.
*
* Note: a state will be queued if the state's parent isn't yet registered.
* It will also be queued if the queue is not yet in [[StateQueueManager.autoFlush]] mode.
*
* @param stateDefinition the definition of the state to register.
* @returns the internal [[State]] object.
* If the state was successfully registered, then the object is fully built (See: [[StateBuilder]]).
* If the state was only queued, then the object is not fully built.
*/
register(stateDefinition: StateDeclaration): State {
return this.stateQueue.register(stateDefinition);
}

/** @hidden */
private _deregisterTree(state: State) {
let all = this.get().map(s => s.$$state());
const getChildren = (states: State[]) => {
let children = all.filter(s => states.indexOf(s.parent) !== -1);
return children.length === 0 ? children : children.concat(getChildren(children));
};

let children = getChildren([state]);
let deregistered = [state].concat(children).reverse();

deregistered.forEach(state => {
state.url && state.url.config.$$removeRule();
delete this.states[state.name];
});

return deregistered;
}

/**
* Removes a state from the registry
*
* This removes a state from the registry.
* If the state has children, they are are also removed from the registry.
*
* @param stateOrName the state's name or object representation
* @returns {State[]} a list of removed states
*/
deregister(stateOrName: StateOrName) {
let _state = this.get(stateOrName);
if (!_state) throw new Error("Can't deregister state; not found: " + stateOrName);
let deregisteredStates = this._deregisterTree(_state.$$state());

this.listeners.forEach(listener => listener("deregistered", deregisteredStates.map(s => s.self)));
return deregisteredStates;
}

/**
* Gets all registered states
*
* Calling this method with no arguments will return a list of all the states that are currently registered.
* Note: this does not return states that are *queued* but not yet registered.
*
* @return a list of [[StateDeclaration]]s
*/
get(): StateDeclaration[];
get(stateOrName: StateOrName, base: StateOrName): StateDeclaration;

/**
* Gets a registered state
*
* Given a state or a name, finds and returns the [[StateDeclaration]] from the registry.
* Note: this does not return states that are *queued* but not yet registered.
*
* @param stateOrName either the name of a state, or a state object.
* @return a registered [[StateDeclaration]] that matched the `stateOrName`, or null if the state isn't registered.
*/
get(stateOrName: StateOrName, base?: StateOrName): StateDeclaration;
get(stateOrName?: StateOrName, base?: StateOrName): any {
if (arguments.length === 0)
return <StateDeclaration[]> Object.keys(this.states).map(name => this.states[name].self);
Expand Down
17 changes: 16 additions & 1 deletion src/url/urlRouter.ts
Expand Up @@ -125,6 +125,15 @@ export class UrlRouterProvider {
return this;
};

/** @hidden */
private $$removeRule(rule) {
let idx = this.rules.indexOf(rule);
if (idx !== -1) {
this.rules.splice(idx, 1);
}
return (idx !== -1);
}

/**
* Defines the path or behavior to use when no url can be matched.
*
Expand Down Expand Up @@ -239,7 +248,13 @@ export class UrlRouterProvider {
};

for (var n in check) {
if (check[n]) return this.rule(strategies[n](what, handler));
if (check[n]) {
let rule = strategies[n](what, handler);
if (check.matcher && what['config']) {
what['config'].$$removeRule = () => this.$$removeRule(rule);
}
return this.rule(rule);
}
}

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

3 comments on commit 44579ec

@sudarsunperu
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @christopherthielen , I'm new here. Sorry if it sounds very basic. I'm using angular-ui-router v1.0.0-rc.1. I'm not sure how to use the deregister function. Can you mention its usage?

@christopherthielen
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@StevieSun docs are here: https://ui-router.github.io/ng1/docs/latest/classes/state.stateregistry.html#deregister

You need to access the state registry. In AngularJS (ng1), either inject $stateRegistry, or inject $uiRouter then access via $uiRouter.stateRegistry

@sudarsunperu
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christopherthielen Thank you so much for the quick response.

Please sign in to comment.