///
namespace <%= prompts.prefix %>.core.router {
export interface IRouterService {
/**
* Flag indicating whether the routing pipeline is currently active.
*/
isNavigating: boolean;
startPipeline(preventDef: Function, toState: ng.ui.IState, toParams: any, fromState: ng.ui.IState, fromParams: any): ng.IPromise;
/**
* Add a middleware to the routing pipeline. Middlewares are executed in
* the order of registration. If the state parameter is used, the middleware
* will only be processed by the pipeline if the target state of the
* navigation is either a child state of or equal to the given state (depending
* on the given matching mode, defaults to include children). Only absolute
* state names are supported, i.e. no wildcards or other patterns are allowed.
*/
addMiddleware(middleware: IRouterMiddleware, name: string, state?: string, stateMatchingMode?: StateMatchingMode): IRouterService;
}
/**
* A router middleware is a function that is executed as part of a pipeline.
* The middleware can perform exactly one of the following actions when executed
* (in addition to any logic the middleware itself contains):
* 1) Call the next middleware in the pipeline
* 2) Redirect to another state, thereby restarting the pipeline
* 3) Cancel the pipeline
*
* The middleware can also pass information to other middlewares that are executed
* after it by modifying the context object. Note that all objects in the context
* are mutable, but only the "toParams" object should be modified by the middleware.
*/
export interface IRouterMiddleware {
(actions: IRouterMiddlewareActions, context: IRouterMiddlewareContext): ng.IPromise;
}
export interface IRouterMiddlewareActions {
/**
* Execute the next middleware in the pipeline.
*/
next(): ng.IPromise;
/**
* Redirect to the given state, restarting the pipeline.
*/
redirect(to: string, params?: any): ng.IPromise;
/**
* Cancel the pipeline.
*/
cancel(): ng.IPromise;
}
export interface IRouterMiddlewareContext {
/**
* The target state of the routing pipeline.
*/
toState: ng.ui.IState;
/**
* The parameters for the target state. This object can be used by middlewares to
* pass additional information to other middlewares or to the controller of the
* target state.
*/
toParams: any;
/**
* The state that was active when the routing pipeline was started.
*/
fromState: ng.ui.IState;
fromParams: any;
}
export enum StateMatchingMode {
Exact,
IncludeChildren
}
interface IMiddlewareContainer {
name: string;
middleware: IRouterMiddleware;
state: string;
stateMatchingMode: StateMatchingMode;
}
class RouterService implements IRouterService {
private log: util.ILogger;
private middlewares: IMiddlewareContainer[] = [];
private sync = false;
private current: { state: ng.ui.IState; params: any } = { state: {}, params: {} };
private lastPipelineId = 0;
isNavigating: boolean;
static $inject = ['$state', '$q', util.ID.LoggerFactory];
constructor(
private $state: ng.ui.IStateService,
private $q: ng.IQService,
loggerFactory: util.ILoggerFactory
) {
this.log = loggerFactory(this);
}
startPipeline = (preventDefault: Function, toState: ng.ui.IState, toParams: any, fromState: ng.ui.IState, fromParams: any) => {
// if we are in the process of finishing pipeline processing, don't do anything
if (this.sync) {
return this.$q.reject('router is syncing');
}
this.lastPipelineId += 1;
return this.startPipelineInternal(preventDefault, toState, toParams, fromState, fromParams, this.lastPipelineId);
}
private startPipelineInternal = (
preventDefault: Function,
toState: ng.ui.IState,
toParams: any,
fromState: ng.ui.IState,
fromParams: any,
pipelineId: number
) => {
this.log.debug(`Starting routing pipeline [ID: ${pipelineId}; to: ${toState.name}; from: ${fromState.name}]...`);
this.isNavigating = true;
preventDefault();
// by calling $state.go with notify set to false, we update the URL
// in the browser address bar without triggering navigation. This is
// necessary, since otherwise the preventDefault() call above would
// cause the old URL to be displayed
this.$state.go(toState.name, {}, { notify: false });
var context = { toState, toParams, fromState, fromParams };
var redirect = (middlewareName: string) => (to: string, params: any = {}): ng.IPromise => {
this.log.debug(`Middleware '${middlewareName}' redirected to '${to}'!`);
// this check is slightly different from $state.is(), since it
// takes into account all params instead of only the ones defined
// in the params definition of the state
if (to.indexOf(this.current.state.name) === 0 && angular.equals(this.current.params, params)) {
// if we got redirected to the last active state, we need to restore
// its URL
this.$state.go(to, params, { notify: false });
return this.endPipeline(pipelineId, this.current.state, params).then(() => this.$q.reject('pipeline ended in source state'));
} else {
return this.startPipelineInternal(() => { ; }, this.$state.get(to), params, fromState, fromParams, pipelineId);
}
};
var cancel = (middlewareName: string) => (): ng.IPromise => {
this.log.debug(`Middleware '${middlewareName}' canceled navigation!`);
// if navigation is canceled, we need to restore the URL of the
// original state
this.$state.go(fromState.name, fromParams, { notify: false });
return this.endPipeline(pipelineId, fromState, fromParams).then(() => this.$q.reject('canceled'));
};
var next = (remaining: IMiddlewareContainer[]) => (): ng.IPromise => {
if (remaining.length === 0) {
var isInitialRequest = fromState.name === '';
// before we transition to the final state, we quickly return to the original
// state, but without triggering any navigation; however, on initial page load
// there is no previous state to return to, so skip the restore
var restore = () => this.$state.go(fromState.name, fromParams, { notify: false });
var go = () => this.$state.go(context.toState.name, context.toParams);
this.sync = true;
return (isInitialRequest ? go() : restore().then(go))
.then(() => this.endPipeline(pipelineId, context.toState, context.toParams))
.finally(() => this.sync = false)
.then(() => context);
}
var container = remaining.splice(0, 1)[0];
var executeMiddleware =
container.stateMatchingMode === StateMatchingMode.Exact
? toState.name === container.state
: toState.includes(container.state);
if (executeMiddleware) {
this.log.debug(`Executing middleware '${container.name}'...`);
var guardActive = false;
var guard = (fn: Function) => (...rest: any[]) => {
if (guardActive) {
return this.$q.reject(new Error(`Middleware ${container.name} is trying to execute more than one action!`));
}
guardActive = true;
if (pipelineId !== this.lastPipelineId) {
return this.cancelPipeline(pipelineId);
}
return fn.apply(null, rest);
};
var actions = {
next: guard(next(remaining)),
cancel: guard(cancel(container.name)),
redirect: guard(redirect(container.name))
};
return container.middleware(actions, context).then(() => {
if (!guardActive) {
return this.$q.reject(new Error(`Middleware ${container.name} did not execute any action!`));
}
return this.$q.when();
});
} else {
this.log.debug(`Skipping middleware '${container.name}'...`);
return next(remaining)();
}
};
var clone = this.middlewares.slice(0);
return next(clone)();
};
addMiddleware = (
middleware: IRouterMiddleware,
name: string,
state: string = '',
stateMatchingMode = StateMatchingMode.IncludeChildren
) => {
this.middlewares.push({ middleware, state, name, stateMatchingMode });
return this;
};
private endPipeline = (pipelineId: number, state: ng.ui.IState, params: any) => {
this.log.debug(`Finished routing pipeline [ID: ${pipelineId}] in state '${state.name}'!`);
this.isNavigating = false;
this.current = { state, params };
return this.$q.when();
};
private cancelPipeline = (pipelineId: number) => {
this.log.debug(`Canceled routing pipeline [ID: ${pipelineId}]!`);
this.isNavigating = false;
return this.$q.when();
};
}
angular
.module(ID.RouterService, [
'ui.router',
'ui.router.router',
'ui.router.state',
util.ID.LoggerFactory
])
.service(ID.RouterService, RouterService);
}