diff --git a/src/common/common.ts b/src/common/common.ts index 5fac6621..6f408793 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -136,6 +136,10 @@ export const removeFrom = curry((array: any[], obj: any) => { return array; }); +/** pushes a values to an array and returns the value */ +export const pushTo = (arr: T[], val: T) => + (arr.push(val), val); + /** * Applies a set of defaults to an options object. The options object is filtered * to only those properties of the objects in the defaultsList. diff --git a/src/common/trace.ts b/src/common/trace.ts index 40b78c3e..390dcf3d 100644 --- a/src/common/trace.ts +++ b/src/common/trace.ts @@ -144,12 +144,12 @@ export class Trace { } /** @internalapi called by ui-router code */ - traceTransitionStart(transition: Transition) { + traceTransitionStart(trans: Transition) { if (!this.enabled(Category.TRANSITION)) return; - let tid = transition.$id, + let tid = trans.$id, digest = this.approximateDigests, - transitionStr = stringify(transition); - console.log(`Transition #${tid} Digest #${digest}: Started -> ${transitionStr}`); + transitionStr = stringify(trans); + console.log(`Transition #${tid} r${trans.router.$id}: Started -> ${transitionStr}`); } /** @internalapi called by ui-router code */ @@ -158,27 +158,27 @@ export class Trace { let tid = trans && trans.$id, digest = this.approximateDigests, transitionStr = stringify(trans); - console.log(`Transition #${tid} Digest #${digest}: Ignored <> ${transitionStr}`); + console.log(`Transition #${tid} r${trans.router.$id}: Ignored <> ${transitionStr}`); } /** @internalapi called by ui-router code */ - traceHookInvocation(step: TransitionHook, options: any) { + traceHookInvocation(step: TransitionHook, trans: Transition, options: any) { if (!this.enabled(Category.HOOK)) return; let tid = parse("transition.$id")(options), digest = this.approximateDigests, event = parse("traceData.hookType")(options) || "internal", context = parse("traceData.context.state.name")(options) || parse("traceData.context")(options) || "unknown", name = functionToString((step as any).registeredHook.callback); - console.log(`Transition #${tid} Digest #${digest}: Hook -> ${event} context: ${context}, ${maxLength(200, name)}`); + console.log(`Transition #${tid} r${trans.router.$id}: Hook -> ${event} context: ${context}, ${maxLength(200, name)}`); } /** @internalapi called by ui-router code */ - traceHookResult(hookResult: HookResult, transitionOptions: any) { + traceHookResult(hookResult: HookResult, trans: Transition, transitionOptions: any) { if (!this.enabled(Category.HOOK)) return; let tid = parse("transition.$id")(transitionOptions), digest = this.approximateDigests, hookResultStr = stringify(hookResult); - console.log(`Transition #${tid} Digest #${digest}: <- Hook returned: ${maxLength(200, hookResultStr)}`); + console.log(`Transition #${tid} r${trans.router.$id}: <- Hook returned: ${maxLength(200, hookResultStr)}`); } /** @internalapi called by ui-router code */ @@ -187,7 +187,7 @@ export class Trace { let tid = trans && trans.$id, digest = this.approximateDigests, pathStr = path && path.toString(); - console.log(`Transition #${tid} Digest #${digest}: Resolving ${pathStr} (${when})`); + console.log(`Transition #${tid} r${trans.router.$id}: Resolving ${pathStr} (${when})`); } /** @internalapi called by ui-router code */ @@ -197,7 +197,7 @@ export class Trace { digest = this.approximateDigests, resolvableStr = resolvable && resolvable.toString(), result = stringify(resolvable.data); - console.log(`Transition #${tid} Digest #${digest}: <- Resolved ${resolvableStr} to: ${maxLength(200, result)}`); + console.log(`Transition #${tid} r${trans.router.$id}: <- Resolved ${resolvableStr} to: ${maxLength(200, result)}`); } /** @internalapi called by ui-router code */ @@ -206,7 +206,7 @@ export class Trace { let tid = trans && trans.$id, digest = this.approximateDigests, transitionStr = stringify(trans); - console.log(`Transition #${tid} Digest #${digest}: <- Rejected ${transitionStr}, reason: ${reason}`); + console.log(`Transition #${tid} r${trans.router.$id}: <- Rejected ${transitionStr}, reason: ${reason}`); } /** @internalapi called by ui-router code */ @@ -216,7 +216,7 @@ export class Trace { digest = this.approximateDigests, state = finalState.name, transitionStr = stringify(trans); - console.log(`Transition #${tid} Digest #${digest}: <- Success ${transitionStr}, final state: ${state}`); + console.log(`Transition #${tid} r${trans.router.$id}: <- Success ${transitionStr}, final state: ${state}`); } /** @internalapi called by ui-router code */ diff --git a/src/interface.ts b/src/interface.ts index 0d5458a2..b89762eb 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -83,10 +83,15 @@ export interface UIInjector { getNative(token: any): T; } -export interface UIRouterPlugin { +export interface UIRouterPlugin extends Disposable { name: string; } export abstract class UIRouterPluginBase implements UIRouterPlugin { abstract name: string; + dispose(router: UIRouter) { } +} + +export interface Disposable { + dispose(router?: UIRouter); } \ No newline at end of file diff --git a/src/params/paramTypes.ts b/src/params/paramTypes.ts index 9137af5f..d91d43df 100644 --- a/src/params/paramTypes.ts +++ b/src/params/paramTypes.ts @@ -86,6 +86,11 @@ export class ParamTypes { this.types = inherit(map(this.defaultTypes, makeType), {}); } + /** @internalapi */ + dispose() { + this.types = {}; + } + type(name: string, definition?: ParamTypeDefinition, definitionFn?: () => ParamTypeDefinition) { if (!isDefined(definition)) return this.types[name]; if (this.types.hasOwnProperty(name)) throw new Error(`A type named '${name}' has already been defined.`); diff --git a/src/router.ts b/src/router.ts index 1bdaf8b2..4d4af6d1 100644 --- a/src/router.ts +++ b/src/router.ts @@ -7,8 +7,11 @@ import { ViewService } from "./view/view"; import { StateRegistry } from "./state/stateRegistry"; import { StateService } from "./state/stateService"; import { UIRouterGlobals, Globals } from "./globals"; -import { UIRouterPlugin } from "./interface"; -import { values } from "./common/common"; +import { UIRouterPlugin, Disposable } from "./interface"; +import { values, removeFrom } from "./common/common"; + +/** @hidden */ +let _routerInstance = 0; /** * The master class used to instantiate an instance of UI-Router. @@ -22,6 +25,9 @@ import { values } from "./common/common"; * Tell UI-Router to monitor the URL by calling `uiRouter.urlRouter.listen()` ([[UrlRouter.listen]]) */ export class UIRouter { + /** @hidden */ + $id: number = _routerInstance++; + viewService = new ViewService(); transitionService: TransitionService = new TransitionService(this); @@ -38,10 +44,32 @@ export class UIRouter { stateService = new StateService(this); + private _disposables: Disposable[] = []; + + /** Registers an object to be notified when the router is disposed */ + disposable(disposable: Disposable) { + this._disposables.push(disposable); + } + + dispose() { + this._disposables.slice().forEach(d => { + try { + typeof d.dispose === 'function' && d.dispose(this); + removeFrom(this._disposables, d); + } catch (ignored) {} + }); + } + constructor() { this.viewService.rootContext(this.stateRegistry.root()); this.globals.$current = this.stateRegistry.root(); this.globals.current = this.globals.$current.self; + + this.disposable(this.transitionService); + this.disposable(this.urlRouterProvider); + this.disposable(this.urlRouter); + this.disposable(this.stateRegistry); + } private _plugins: { [key: string]: UIRouterPlugin } = {}; @@ -108,6 +136,7 @@ export class UIRouter { plugin(plugin: any, options: any = {}): T { let pluginInstance = new plugin(this, options); if (!pluginInstance.name) throw new Error("Required property `name` missing on plugin: " + pluginInstance); + this._disposables.push(pluginInstance); return this._plugins[pluginInstance.name] = pluginInstance; } diff --git a/src/state/stateQueueManager.ts b/src/state/stateQueueManager.ts index cae49edb..1f28a619 100644 --- a/src/state/stateQueueManager.ts +++ b/src/state/stateQueueManager.ts @@ -9,8 +9,9 @@ import {UrlRouterProvider} from "../url/urlRouter"; import {RawParams} from "../params/interface"; import {StateRegistry, StateRegistryListener} from "./stateRegistry"; import { Param } from "../params/param"; +import { Disposable } from "../interface"; -export class StateQueueManager { +export class StateQueueManager implements Disposable { queue: State[]; private $state: StateService; @@ -23,6 +24,11 @@ export class StateQueueManager { this.queue = []; } + /** @internalapi */ + dispose() { + this.queue = []; + } + register(config: StateDeclaration) { let {states, queue, $state} = this; // Wrap a new object around the state so we can store our private details easily. diff --git a/src/state/stateRegistry.ts b/src/state/stateRegistry.ts index 957872ca..011c8b2c 100644 --- a/src/state/stateRegistry.ts +++ b/src/state/stateRegistry.ts @@ -1,15 +1,15 @@ /** @coreapi @module state */ /** for typedoc */ -import {State} from "./stateObject"; -import {StateMatcher} from "./stateMatcher"; -import {StateBuilder} from "./stateBuilder"; -import {StateQueueManager} from "./stateQueueManager"; -import {UrlMatcherFactory} from "../url/urlMatcherFactory"; -import {StateDeclaration} from "./interface"; -import {BuilderFunction} from "./stateBuilder"; -import {StateOrName} from "./interface"; -import {UrlRouterProvider} from "../url/urlRouter"; -import {removeFrom} from "../common/common"; +import { State } from "./stateObject"; +import { StateMatcher } from "./stateMatcher"; +import { StateBuilder } from "./stateBuilder"; +import { StateQueueManager } from "./stateQueueManager"; +import { UrlMatcherFactory } from "../url/urlMatcherFactory"; +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]]. @@ -50,6 +50,13 @@ export class StateRegistry { _root.navigable = null; } + /** @internalapi */ + dispose() { + this.stateQueue.dispose(); + this.listeners = []; + this.get().forEach(state => this.get(state) && this.deregister(state)); + } + /** * Listen for a State Registry events * diff --git a/src/state/stateService.ts b/src/state/stateService.ts index a8fa292b..58df72ed 100644 --- a/src/state/stateService.ts +++ b/src/state/stateService.ts @@ -1,5 +1,5 @@ /** @coreapi @module state */ /** */ -import {extend, defaults, silentRejection, silenceUncaughtInPromise, removeFrom} from "../common/common"; +import { extend, defaults, silentRejection, silenceUncaughtInPromise, removeFrom, noop } from "../common/common"; import {isDefined, isObject, isString} from "../common/predicates"; import {Queue} from "../common/queue"; import {services} from "../common/coreservices"; @@ -74,6 +74,12 @@ export class StateService { bindFunctions(StateService.prototype, this, this, boundFns); } + /** @internalapi */ + dispose() { + this.defaultErrorHandler(noop); + this.invalidCallbacks = []; + } + /** * Handler for when [[transitionTo]] is called with an invalid state. * diff --git a/src/transition/transition.ts b/src/transition/transition.ts index 39f04a1d..e628c1c0 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -34,8 +34,6 @@ import {Globals} from "../globals"; import {UIInjector} from "../interface"; import {RawParams} from "../params/interface"; -/** @hidden */ -let transitionCount = 0; /** @hidden */ const stateSelf: (_state: State) => StateDeclaration = prop("self"); @@ -150,7 +148,7 @@ export class Transition implements IHookRegistry { // current() is assumed to come from targetState.options, but provide a naive implementation otherwise. this._options = extend({ current: val(this) }, targetState.options()); - this.$id = transitionCount++; + this.$id = router.transitionService._transitionCount++; let toPath = PathFactory.buildToPath(fromPath, targetState); this._treeChanges = PathFactory.treeChanges(fromPath, toPath, this._options.reloadState); this.createTransitionHookRegFns(); diff --git a/src/transition/transitionHook.ts b/src/transition/transitionHook.ts index 1b6545e4..85ec9c35 100644 --- a/src/transition/transitionHook.ts +++ b/src/transition/transitionHook.ts @@ -65,7 +65,7 @@ export class TransitionHook { if (hook._deregistered) return; let options = this.options; - trace.traceHookInvocation(this, options); + trace.traceHookInvocation(this, this.transition, options); if (this.rejectIfSuperseded()) { return Rejection.superseded(options.current()).toPromise(); @@ -114,7 +114,7 @@ export class TransitionHook { return result.then(this.handleHookResult.bind(this)); } - trace.traceHookResult(result, this.options); + trace.traceHookResult(result, this.transition, this.options); // Hook returned false if (result === false) { diff --git a/src/transition/transitionService.ts b/src/transition/transitionService.ts index ec2a5659..84759255 100644 --- a/src/transition/transitionService.ts +++ b/src/transition/transitionService.ts @@ -24,6 +24,8 @@ import {registerLazyLoadHook} from "../hooks/lazyLoad"; import {TransitionHookType} from "./transitionHookType"; import {TransitionHook} from "./transitionHook"; import {isDefined} from "../common/predicates"; +import { removeFrom, values } from "../common/common"; +import { Disposable } from "../interface"; /** * The default [[Transition]] options. @@ -52,7 +54,9 @@ export let defaultTransOpts: TransitionOptions = { * * At bootstrap, [[UIRouter]] creates a single instance (singleton) of this class. */ -export class TransitionService implements IHookRegistry { +export class TransitionService implements IHookRegistry, Disposable { + /** @hidden */ + _transitionCount = 0; /** * Registers a [[TransitionHookFn]], called *while a transition is being constructed*. @@ -128,6 +132,16 @@ export class TransitionService implements IHookRegistry { this.registerTransitionHooks(); } + /** @internalapi */ + dispose() { + delete this._router.globals.transition; + + values(this._transitionHooks).forEach(hooksArray => hooksArray.forEach(hook => { + hook._deregistered = true; + removeFrom(hooksArray, hook); + })); + } + /** * Creates a new [[Transition]] object * diff --git a/src/url/urlMatcherFactory.ts b/src/url/urlMatcherFactory.ts index 9519643d..ee22586c 100644 --- a/src/url/urlMatcherFactory.ts +++ b/src/url/urlMatcherFactory.ts @@ -7,6 +7,7 @@ import {matcherConfig} from "./urlMatcherConfig"; import {Param} from "../params/param"; import {ParamTypes} from "../params/paramTypes"; import {ParamTypeDefinition} from "../params/interface"; +import { Disposable } from "../interface"; /** @hidden */ function getDefaultConfig() { @@ -22,7 +23,7 @@ function getDefaultConfig() { * The factory is available to ng1 services as * `$urlMatcherFactor` or ng1 providers as `$urlMatcherFactoryProvider`. */ -export class UrlMatcherFactory { +export class UrlMatcherFactory implements Disposable { paramTypes = new ParamTypes(); constructor() { @@ -123,4 +124,9 @@ export class UrlMatcherFactory { this.paramTypes._flushTypeQueue(); return this; }; + + /** @internalapi */ + dispose() { + this.paramTypes.dispose(); + } } diff --git a/src/url/urlRouter.ts b/src/url/urlRouter.ts index 8c4efe7f..d2b23516 100644 --- a/src/url/urlRouter.ts +++ b/src/url/urlRouter.ts @@ -6,6 +6,7 @@ import {services, $InjectorLike, LocationServices} from "../common/coreservices" import {UrlMatcherFactory} from "./urlMatcherFactory"; import {StateParams} from "../params/stateParams"; import {RawParams} from "../params/interface"; +import { Disposable } from "../interface"; /** @hidden Returns a string that is a prefix of all strings matching the RegExp */ function regExpPrefix(re: RegExp) { @@ -66,7 +67,7 @@ function update(rules: Function[], otherwiseFn: Function, evt?: any) { * * This class manages the router rules for what to do when the URL changes. */ -export class UrlRouterProvider { +export class UrlRouterProvider implements Disposable { /** @hidden */ rules: Function[] = []; /** @hidden */ @@ -84,6 +85,12 @@ export class UrlRouterProvider { this.$stateParams = $stateParams; } + /** @internalapi */ + dispose() { + this.rules = []; + delete this.otherwiseFn; + } + /** * Registers a url handler function. * @@ -292,7 +299,7 @@ export class UrlRouterProvider { }; } -export class UrlRouter { +export class UrlRouter implements Disposable { /** @hidden */ private location: string; /** @hidden */ @@ -307,6 +314,12 @@ export class UrlRouter { bindFunctions(UrlRouter.prototype, this, this); } + /** @internalapi */ + dispose() { + this.listener && this.listener(); + delete this.listener; + } + /** * Checks the current URL for a matching rule *