diff --git a/src/common/queue.ts b/src/common/queue.ts index 4e2551920..affae08b5 100644 --- a/src/common/queue.ts +++ b/src/common/queue.ts @@ -1,10 +1,12 @@ /** @module common */ /** for typedoc */ export class Queue { - constructor(private _items: T[] = []) { } + constructor(private _items: T[] = [], private _limit: number = null) { } enqueue(item: T) { - this._items.push(item); + let items = this._items; + items.push(item); + if (this._limit && items.length > this._limit) items.shift(); return item; } diff --git a/src/globals.ts b/src/globals.ts new file mode 100644 index 000000000..f132a93a5 --- /dev/null +++ b/src/globals.ts @@ -0,0 +1,72 @@ +/** @module common */ /** */ +import {StateParams} from "./params/stateParams"; +import {StateDeclaration} from "./state/interface"; +import {State} from "./state/stateObject"; +import {Transition} from "./transition/transition"; +import {Queue} from "./common/queue"; +import {TransitionService} from "./transition/transitionService"; +import {copy} from "./common/common"; + +/** + * Global mutable state + */ +export class UIRouterGlobals { + /** + * Current parameter values + * + * The parameter values from the latest successful transition + */ + params: StateParams = new StateParams(); + /** + * Current state + * + * The to-state from the latest successful transition + */ + current: StateDeclaration; + /** + * Current state + * + * The to-state from the latest successful transition + */ + $current: State; + /** + * The current transition (in progress) + */ + transition: Transition; + /** + * The transition history + * + * This queue's size is limited to a maximum number (default: 1) + */ + transitionHistory = new Queue([], 1); + /** + * The history of successful transitions + * + * This queue's size is limited to a maximum number (default: 1) + */ + successfulTransitions = new Queue([], 1); + + constructor(transitionService: TransitionService) { + const beforeNewTransition = ($transition$) => { + + this.transition = $transition$; + this.transitionHistory.enqueue($transition$); + + const updateGlobalState = () => { + this.successfulTransitions.enqueue($transition$); + this.$current = $transition$.$to(); + this.current = this.$current.self; + copy($transition$.params(), this.params); + }; + + $transition$.onSuccess({}, updateGlobalState, {priority: 10000}); + + const clearCurrentTransition = () => { if (this.transition === $transition$) this.transition = null; }; + + $transition$.promise.finally(clearCurrentTransition) + + }; + + transitionService.onBefore({}, ['$transition$', beforeNewTransition]); + } +} \ No newline at end of file diff --git a/src/ng1/services.ts b/src/ng1/services.ts index 86d7f8042..0282b1f04 100644 --- a/src/ng1/services.ts +++ b/src/ng1/services.ts @@ -226,8 +226,8 @@ const resolveFactory = () => ({ } }); -function $stateParamsFactory(ng1UIRouter, $rootScope) { - return router.stateParams; +function $stateParamsFactory(ng1UIRouter) { + return ng1UIRouter.globals.params; } // The 'ui.router' ng1 module depends on 'ui.router.init' module. @@ -265,7 +265,8 @@ angular.module('ui.router.state').provider('$state', ['ng1UIRouterProvider', get angular.module('ui.router.state').run(['$state', function($state) { }]); // $stateParams service -angular.module('ui.router.state').factory('$stateParams', ['ng1UIRouter', '$rootScope', $stateParamsFactory]); +angular.module('ui.router.state').factory('$stateParams', ['ng1UIRouter', (ng1UIRouter) => + ng1UIRouter.globals.params]); // $transitions service and $transitionsProvider function getTransitionsProvider() { diff --git a/src/params/stateParams.ts b/src/params/stateParams.ts index f87bc93eb..4a2234f8b 100644 --- a/src/params/stateParams.ts +++ b/src/params/stateParams.ts @@ -32,6 +32,3 @@ export class StateParams { }; } -export function stateParamsFactory() { - return new StateParams(); -} diff --git a/src/router.ts b/src/router.ts index 2994296e4..1e45ec4a4 100644 --- a/src/router.ts +++ b/src/router.ts @@ -2,12 +2,12 @@ import {UrlMatcherFactory} from "./url/urlMatcherFactory"; import {UrlRouterProvider} from "./url/urlRouter"; import {StateProvider} from "./state/state"; -import {stateParamsFactory, StateParams} from "./params/stateParams"; import {UrlRouter} from "./url/urlRouter"; import {TransitionService} from "./transition/transitionService"; import {ViewService} from "./view/view"; import {StateRegistry} from "./state/stateRegistry"; import {StateService} from "./state/stateService"; +import {UIRouterGlobals} from "./globals"; /** * The master class used to instantiate an instance of UI-Router. @@ -19,27 +19,29 @@ import {StateService} from "./state/stateService"; * the URL by calling `urlRouter.listen()` ([[URLRouter.listen]]) */ export class UIRouter { + viewService = new ViewService(); + + transitionService: TransitionService = new TransitionService(this.viewService); - stateParams: StateParams = stateParamsFactory(); + globals: UIRouterGlobals = new UIRouterGlobals(this.transitionService); urlMatcherFactory: UrlMatcherFactory = new UrlMatcherFactory(); - urlRouterProvider: UrlRouterProvider = new UrlRouterProvider(this.urlMatcherFactory, this.stateParams); + urlRouterProvider: UrlRouterProvider = new UrlRouterProvider(this.urlMatcherFactory, this.globals.params); urlRouter: UrlRouter = new UrlRouter(this.urlRouterProvider); - viewService = new ViewService(); - - transitionService: TransitionService = new TransitionService(this.viewService); - stateRegistry: StateRegistry = new StateRegistry(this.urlMatcherFactory, this.urlRouterProvider); - // TODO: move this to ng1.ts + /** @hidden TODO: move this to ng1.ts */ stateProvider = new StateProvider(this.stateRegistry); - stateService = new StateService(this.viewService, this.stateParams, this.urlRouter, this.transitionService, this.stateRegistry, this.stateProvider); + stateService = new StateService(this.viewService, this.urlRouter, this.transitionService, this.stateRegistry, this.stateProvider, this.globals); constructor() { this.viewService.rootContext(this.stateRegistry.root()); + this.globals.$current = this.stateRegistry.root(); + this.globals.current = this.globals.$current.self; } } + diff --git a/src/state/hooks/transitionManager.ts b/src/state/hooks/transitionManager.ts index 2463306fa..d1b86d38c 100644 --- a/src/state/hooks/transitionManager.ts +++ b/src/state/hooks/transitionManager.ts @@ -1,7 +1,5 @@ /** @module state */ /** for typedoc */ -import {copy} from "../../common/common"; import {prop} from "../../common/hof"; -import {Queue} from "../../common/queue"; import {Param} from "../../params/param"; import {TreeChanges} from "../../transition/interface"; @@ -15,6 +13,8 @@ import {ViewHooks} from "./viewHooks"; import {EnterExitHooks} from "./enterExitHooks"; import {ResolveHooks} from "./resolveHooks"; import {UrlRouter} from "../../url/urlRouter"; +import {services} from "../../common/coreservices"; +import {UIRouterGlobals} from "../../globals"; /** * This class: @@ -38,6 +38,7 @@ export class TransitionManager { private enterExitHooks: EnterExitHooks; private viewHooks: ViewHooks; private resolveHooks: ResolveHooks; + private $q; constructor( private transition: Transition, @@ -45,11 +46,9 @@ export class TransitionManager { private $urlRouter: UrlRouter, private $view, // service private $state: StateService, - private $stateParams, // service/obj - private $q, // TODO: get from runtime.$q - private activeTransQ: Queue, - private changeHistory: Queue + private globals: UIRouterGlobals ) { + this.$q = services.$q; this.viewHooks = new ViewHooks(transition, $view); this.enterExitHooks = new EnterExitHooks(transition); this.resolveHooks = new ResolveHooks(transition); @@ -63,45 +62,26 @@ export class TransitionManager { } runTransition(): Promise { - this.activeTransQ.clear(); // TODO: nuke this - this.activeTransQ.enqueue(this.transition); - this.$state.transition = this.transition; - let promise = this.transition.run() + this.globals.transitionHistory.enqueue(this.transition); + return this.transition.run() .then((trans: Transition) => trans.to()) // resolve to the final state (TODO: good? bad?) .catch(error => this.transRejected(error)); // if rejected, handle dynamic and redirect - - let always = () => { - this.activeTransQ.remove(this.transition); - if (this.$state.transition === this.transition) this.transition = null; - }; - - promise.then(always, always); - - return promise; } registerUpdateGlobalState() { - this.transition.onFinish({}, this.updateGlobalState.bind(this), {priority: -10000}); - } - - updateGlobalState() { - let {treeChanges, transition, $state, changeHistory} = this; - // Update globals in $state - $state.$current = transition.$to(); - $state.current = $state.$current.self; - changeHistory.enqueue(treeChanges); - this.updateStateParams(); + // After globals.current is updated at priority: 10000 + this.transition.onSuccess({}, this.updateUrl.bind(this), {priority: 9999}); } transRejected(error): (StateDeclaration|Promise) { - let {transition, $state, $stateParams, $q} = this; + let {transition, $state, $q} = this; // Handle redirect and abort if (error instanceof TransitionRejection) { if (error.type === RejectType.IGNORED) { // Update $stateParmas/$state.params/$location.url if transition ignored, but dynamic params have changed. let dynamic = $state.$current.parameters().filter(prop('dynamic')); - if (!Param.equals(dynamic, $stateParams, transition.params())) { - this.updateStateParams(); + if (!Param.equals(dynamic, $state.params, transition.params())) { + this.updateUrl(); } return $state.current; } @@ -120,22 +100,20 @@ export class TransitionManager { return $q.reject(error); } - updateStateParams() { + updateUrl() { let transition = this.transition; - let {$urlRouter, $state, $stateParams} = this; + let {$urlRouter, $state} = this; let options = transition.options(); - copy(transition.params(), $state.params); - copy($state.params, $stateParams); + var toState = transition.$to(); if (options.location && $state.$current.navigable) { - $urlRouter.push($state.$current.navigable.url, $stateParams, { replace: options.location === 'replace' }); + $urlRouter.push($state.$current.navigable.url, $state.params, { replace: options.location === 'replace' }); } - $urlRouter.update(true); } private _redirectMgr(redirect: Transition): TransitionManager { - let {$transitions, $urlRouter, $view, $state, $stateParams, $q, activeTransQ, changeHistory} = this; - return new TransitionManager(redirect, $transitions, $urlRouter, $view, $state, $stateParams, $q, activeTransQ, changeHistory); + let {$transitions, $urlRouter, $view, $state, globals} = this; + return new TransitionManager(redirect, $transitions, $urlRouter, $view, $state, globals); } } diff --git a/src/state/stateService.ts b/src/state/stateService.ts index 996a63a75..f53bb0a87 100644 --- a/src/state/stateService.ts +++ b/src/state/stateService.ts @@ -13,7 +13,7 @@ import {StateParams} from "../params/stateParams"; import {UrlRouter} from "../url/urlRouter"; -import {TransitionOptions, TreeChanges} from "../transition/interface"; +import {TransitionOptions} from "../transition/interface"; import {TransitionService, defaultTransOpts} from "../transition/transitionService"; import {RejectFactory} from "../transition/rejectFactory"; import {Transition} from "../transition/transition"; @@ -32,43 +32,37 @@ import {equalForKeys} from "../common/common"; import {HrefOptions} from "./interface"; import {StateProvider} from "./state"; import {bindFunctions} from "../common/common"; +import {UIRouterGlobals} from "../globals"; export class StateService { - private transQueue = new Queue(); - private treeChangesQueue = new Queue(); + get transition() { return this.globals.transition; } + get params() { return this.globals.params; } + get current() { return this.globals.current; } + get $current() { return this.globals.$current; } + private rejectFactory = new RejectFactory(); - public params = new StateParams(); - public current: StateDeclaration; - public $current: State; - public transition: Transition; constructor(private $view: ViewService, - private $stateParams: StateParams, private $urlRouter: UrlRouter, private $transitions: TransitionService, private stateRegistry: StateRegistry, - private stateProvider: StateProvider) { - bindFunctions(StateService.prototype, this, this); - - let root = stateRegistry.root(); - extend(this, { - params: new StateParams(), - current: root.self, - $current: root, - transition: null - }); + private stateProvider: StateProvider, + private globals: UIRouterGlobals) { + let getters = ['current', '$current', 'params', 'transition']; + let boundFns = Object.keys(StateService.prototype).filter(key => getters.indexOf(key) === -1); + bindFunctions(StateService.prototype, this, this, boundFns); } /** * Invokes the onInvalid callbacks, in natural order. Each callback's return value is checked in sequence - * until one of them returns an instance of ITargetState. The results of the callbacks are wrapped + * until one of them returns an instance of TargetState. The results of the callbacks are wrapped * in $q.when(), so the callbacks may return promises. * - * If a callback returns an ITargetState, then it is used as arguments to $state.transitionTo() and + * If a callback returns an TargetState, then it is used as arguments to $state.transitionTo() and * the result returned. */ private _handleInvalidTargetState(fromPath: Node[], $to$: TargetState) { - const latestThing = () => this.transQueue.peekTail() || this.treeChangesQueue.peekTail(); + const latestThing = () => this.globals.transitionHistory.peekTail(); let latest = latestThing(); let $from$ = PathFactory.makeTargetState(fromPath); let callbackQueue = new Queue([].concat(this.stateProvider.invalidCallbacks)); @@ -146,7 +140,7 @@ export class StateService { * {@link ui.router.state.$state#methods_go $state.go}. */ reload(reloadState: StateOrName): Promise { - return this.transitionTo(this.current, this.$stateParams, { + return this.transitionTo(this.current, this.params, { reload: isDefined(reloadState) ? reloadState : true, inherit: false, notify: false @@ -278,14 +272,14 @@ export class StateService { * {@link ui.router.state.$state#methods_go $state.go}. */ transitionTo(to: StateOrName, toParams: RawParams = {}, options: TransitionOptions = {}): Promise { - let {transQueue, treeChangesQueue} = this; + let transHistory = this.globals.transitionHistory; options = defaults(options, defaultTransOpts); - options = extend(options, { current: transQueue.peekTail.bind(transQueue)}); + options = extend(options, { current: transHistory.peekTail.bind(transHistory)}); let ref: TargetState = this.target(to, toParams, options); - let latestTreeChanges: TreeChanges = treeChangesQueue.peekTail(); + let latestSuccess: Transition = this.globals.successfulTransitions.peekTail(); const rootPath = () => PathFactory.bindTransNodesToPath([new Node(this.stateRegistry.root())]); - let currentPath: Node[] = latestTreeChanges ? latestTreeChanges.to : rootPath(); + let currentPath: Node[] = latestSuccess ? latestSuccess.treeChanges().to : rootPath(); if (!ref.exists()) return this._handleInvalidTargetState(currentPath, ref); @@ -293,7 +287,7 @@ export class StateService { return services.$q.reject(ref.error()); let transition = this.$transitions.create(currentPath, ref); - let tMgr = new TransitionManager(transition, this.$transitions, this.$urlRouter, this.$view, this, this.$stateParams, services.$q, transQueue, treeChangesQueue); + let tMgr = new TransitionManager(transition, this.$transitions, this.$urlRouter, this.$view, this, this.globals); let transitionPromise = tMgr.runTransition(); // Return a promise for the transition, which also has the transition object on it. return extend(transitionPromise, { transition }); @@ -338,7 +332,7 @@ export class StateService { let state = this.stateRegistry.matcher.find(stateOrName, options.relative); if (!isDefined(state)) return undefined; if (this.$current !== state) return false; - return isDefined(params) && params !== null ? Param.equals(state.parameters(), this.$stateParams, params) : true; + return isDefined(params) && params !== null ? Param.equals(state.parameters(), this.params, params) : true; }; /** @@ -405,7 +399,7 @@ export class StateService { if (!isDefined(state)) return undefined; if (!isDefined(include[state.name])) return false; // @TODO Replace with Param.equals() ? - return params ? equalForKeys(Param.values(state.parameters(), params), this.$stateParams, Object.keys(params)) : true; + return params ? equalForKeys(Param.values(state.parameters(), params), this.params, Object.keys(params)) : true; }; @@ -448,7 +442,7 @@ export class StateService { let state = this.stateRegistry.matcher.find(stateOrName, options.relative); if (!isDefined(state)) return null; - if (options.inherit) params = this.$stateParams.$inherit(params || {}, this.$current, state); + if (options.inherit) params = this.params.$inherit(params || {}, this.$current, state); let nav = (state && options.lossy) ? state.navigable : state; diff --git a/src/transition/transition.ts b/src/transition/transition.ts index 060a7390c..0f8a27db0 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -32,6 +32,7 @@ const stateSelf: (_state: State) => StateDeclaration = prop("self"); */ export class Transition implements IHookRegistry { $id: number; + success: boolean; private _deferred = services.$q.defer(); /** @@ -139,7 +140,7 @@ export class Transition implements IHookRegistry { } $from() { - return tail(this._treeChanges.from).state; + return tail(this._treeChanges.from).state; } $to() { @@ -395,11 +396,13 @@ export class Transition implements IHookRegistry { // When the chain is complete, then resolve or reject the deferred const resolve = () => { + this.success = true; this._deferred.resolve(this); trace.traceSuccess(this.$to(), this); }; const reject = (error) => { + this.success = false; this._deferred.reject(error); trace.traceError(error, this); return services.$q.reject(error);