From 0e10658a50862d2e8017a1b305ccc08bd5bb4e71 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sat, 19 Sep 2015 20:24:54 -0500 Subject: [PATCH 01/31] refactor(Node): rename ownParams to ownParamValues to be consistent --- src/params/paramValues.ts | 4 ++-- src/path/interface.ts | 2 +- src/path/pathFactory.ts | 16 ++++++++-------- test/resolveSpec.ts | 2 +- test/viewSpec.ts | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/params/paramValues.ts b/src/params/paramValues.ts index 3fd79035b..078254343 100644 --- a/src/params/paramValues.ts +++ b/src/params/paramValues.ts @@ -15,13 +15,13 @@ export default class ParamValues implements IRawParams { constructor($$path: IParamsPath) { Object.defineProperty(this, "$$path", { value: $$path }); - $$path.nodes().reduce((memo, node) => extend(memo, node.ownParams), this); + $$path.nodes().reduce((memo, node) => extend(memo, node.ownParamValues), this); } /** Gets the param values for a given state (by state name) */ $byState(stateName: string) { let found = find(this.$$path.nodes(), stateNameMatches(stateName)); - return found && found.ownParams; + return found && found.ownParamValues; } /** Returns a new ParamValues object which closes over a subpath of this ParamValue's Path. */ diff --git a/src/path/interface.ts b/src/path/interface.ts index d3ea04624..9cd2710e3 100644 --- a/src/path/interface.ts +++ b/src/path/interface.ts @@ -22,7 +22,7 @@ export interface IPath extends Path {} /** Contains INode base data plus raw params values for the node */ export interface IParamsNode extends INode { - ownParams: IRawParams; + ownParamValues: IRawParams; } /** A Path of IParamsNode(s) */ export interface IParamsPath extends Path {} diff --git a/src/path/pathFactory.ts b/src/path/pathFactory.ts index 4b58aae22..7826507d8 100644 --- a/src/path/pathFactory.ts +++ b/src/path/pathFactory.ts @@ -41,7 +41,7 @@ export default class PathFactory { static makeParamsNode = curry(function(params: IRawParams, state: IState) { return { state, - ownParams: state.ownParams.$$values(params) + ownParamValues: state.ownParams.$$values(params) }; }); @@ -76,7 +76,7 @@ export default class PathFactory { static inheritParams(fromPath: IParamsPath, toPath: IParamsPath, toKeys: string[] = []): IParamsPath { function nodeParamVals(path: IParamsPath, state: IState): IRawParams { let node = path.nodeForState(state); - return extend({}, node && node.ownParams); + return extend({}, node && node.ownParamValues); } /** @@ -85,14 +85,14 @@ export default class PathFactory { */ let makeInheritedParamsNode = curry(function(_fromPath: IParamsPath, _toKeys: string[], toNode: IParamsNode): IParamsNode { // All param values for the node (may include default key/vals, when key was not found in toParams) - let toParamVals = extend({}, toNode && toNode.ownParams); + let toParamVals = extend({}, toNode && toNode.ownParamValues); // limited to only those keys found in toParams let incomingParamVals = pick(toParamVals, _toKeys); toParamVals = omit(toParamVals, _toKeys); let fromParamVals = nodeParamVals(_fromPath, toNode.state) || {}; // extend toParamVals with any fromParamVals, then override any of those those with incomingParamVals - let ownParamVals: IRawParams = extend(toParamVals, fromParamVals, incomingParamVals); - return { state: toNode.state, ownParams: ownParamVals }; + let ownParamValues: IRawParams = extend(toParamVals, fromParamVals, incomingParamVals); + return { state: toNode.state, ownParamValues }; }); // The param keys specified by the incoming toParams @@ -144,7 +144,7 @@ export default class PathFactory { let keep = 0, max = Math.min(fromNodes.length, toNodes.length); const nodesMatch = (node1: IParamsNode, node2: IParamsNode) => - node1.state === node2.state && nonDynamicParams(node1.state).$$equals(node1.ownParams, node2.ownParams); + node1.state === node2.state && nonDynamicParams(node1.state).$$equals(node1.ownParamValues, node2.ownParamValues); while (keep < max && fromNodes[keep].state !== reloadState && nodesMatch(fromNodes[keep], toNodes[keep])) { keep++; @@ -152,8 +152,8 @@ export default class PathFactory { /** Given a retained node, return a new node which uses the to node's param values */ function applyToParams(retainedNode: ITransNode, idx: number): ITransNode { - let toNodeParams = toPath.nodes()[idx].ownParams; - return extend({}, retainedNode, { ownParams: toNodeParams }); + let toNodeParams = toPath.nodes()[idx].ownParamValues; + return extend({}, retainedNode, { ownParamValues: toNodeParams }); } let from: ITransPath, retained: ITransPath, exiting: ITransPath, entering: ITransPath, to: ITransPath; diff --git a/test/resolveSpec.ts b/test/resolveSpec.ts index 09b3ff68e..ab09bdb85 100644 --- a/test/resolveSpec.ts +++ b/test/resolveSpec.ts @@ -79,7 +79,7 @@ beforeEach(function () { }); function makePath(names: string[]): IResolvePath { - let nodes = map(names, name => ({ state: statesMap[name], ownParams: {} })); + let nodes = map(names, name => ({ state: statesMap[name], ownParamValues: {} })); let pPath = new Path(nodes).adapt(PathFactory.makeResolveNode); return PathFactory.bindTransNodesToPath(pPath); } diff --git a/test/viewSpec.ts b/test/viewSpec.ts index 602f6b604..80aa879ca 100644 --- a/test/viewSpec.ts +++ b/test/viewSpec.ts @@ -54,7 +54,7 @@ describe('view', function() { let ctx, state; beforeEach(() => { state = register({ name: "foo" }); - var nodes = [root, state].map(_state => ({ state: _state, ownParams: {}})); + var nodes = [root, state].map(_state => ({ state: _state, ownParamValues: {}})); var path = PathFactory.bindTransNodesToPath( new Path(nodes).adapt(PathFactory.makeResolveNode)); ctx = new ResolveContext(path); }); From d4d9e80e6b351c85c5501349d2e2e9a5b3138f10 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Tue, 22 Sep 2015 19:19:09 -0500 Subject: [PATCH 02/31] fix(Transition): reject promise in .run() if there is an error() --- src/transition/transition.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/transition/transition.ts b/src/transition/transition.ts index a6105bf0d..4ac93adb3 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -59,11 +59,14 @@ export class Transition implements IHookRegistry { getHooks: IHookGetter; constructor(fromPath: ITransPath, targetState: TargetState) { - if (targetState.error()) throw new Error(targetState.error()); + if (!targetState.valid()) { + throw new Error(targetState.error()); + } + // Makes the Transition instance a hook registry (onStart, etc) HookRegistry.mixin(new HookRegistry(), this); - // current() is assumed to come from targetState.options, but provide a naive implemention otherwise. + // 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++; let toPath = PathFactory.buildToPath(fromPath, targetState); @@ -268,7 +271,12 @@ export class Transition implements IHookRegistry { } run () { - if (this.error()) throw new Error(this.error()); + if (!this.valid()) { + let error = new Error(this.error()); + this._deferred.reject(error); + throw error; + } + trace.traceTransitionStart(this); if (this.ignored()) { From 52aeacf09efaf7894cd35ece6209273f65bf989d Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Tue, 22 Sep 2015 20:14:45 -0500 Subject: [PATCH 03/31] refactor(StateHandler): rename stateHandler to stateHooks, move $state's hook registration logic to renamed StateHooks class --- src/state/module.ts | 4 +- src/state/state.ts | 68 ++---------------- src/state/stateHandler.ts | 78 --------------------- src/state/stateHooks.ts | 142 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 141 deletions(-) delete mode 100644 src/state/stateHandler.ts create mode 100644 src/state/stateHooks.ts diff --git a/src/state/module.ts b/src/state/module.ts index 3ef39244d..787ad22f3 100644 --- a/src/state/module.ts +++ b/src/state/module.ts @@ -16,8 +16,8 @@ export {stateEvents}; import * as stateFilters from "./stateFilters"; export {stateFilters}; -import * as stateHandler from "./stateHandler"; -export {stateHandler}; +import * as stateHooks from "./stateHooks"; +export {stateHooks}; import * as stateMatcher from "./stateMatcher"; export {stateMatcher}; diff --git a/src/state/state.ts b/src/state/state.ts index a8e3e69cc..d02f06630 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -1,14 +1,13 @@ import {extend, defaults, copy, equalForKeys, forEach, ancestors, noop, isDefined, isObject, isString} from "../common/common"; import Queue from "../common/queue"; import {IServiceProviderFactory, IPromise} from "angular"; -import {annotateController} from "../common/angular1"; import {IStateService, IState, IStateDeclaration, IStateOrName, IHrefOptions} from "./interface"; import Glob from "./glob"; import StateQueueManager from "./stateQueueManager"; import StateBuilder from "./stateBuilder"; import StateMatcher from "./stateMatcher"; -import StateHandler from "./stateHandler"; +import StateHooks from "./stateHooks"; import TargetState from "./targetState"; import {ITransitionService, ITransitionOptions, ITreeChanges} from "../transition/interface"; @@ -22,8 +21,6 @@ import PathFactory from "../path/pathFactory"; import {IRawParams, IParamsOrArray} from "../params/interface"; -import {ViewConfig} from "../view/view"; - /** * @ngdoc object * @name ui.router.state.type:State @@ -694,69 +691,18 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { if (!transition.valid()) return $q.reject(transition.error()); - let stateHandler = new StateHandler($urlRouter, $view, $state, $stateParams, $q, transQueue, treeChangesQueue); - let hookBuilder = transition.hookBuilder(); - - // TODO: Move the Transition instance hook registration to its own function - // Add hooks - let enteringViews = transition.views("entering"); - let exitingViews = transition.views("exiting"); - let treeChanges = transition.treeChanges(); - - function $updateViews() { - exitingViews.forEach((viewConfig: ViewConfig) => $view.reset(viewConfig)); - enteringViews.forEach((viewConfig: ViewConfig) => $view.registerStateViewConfig(viewConfig)); - $view.sync(); - } - - function $loadAllEnteringViews() { - const loadView = (vc: ViewConfig) => { - let resolveInjector = treeChanges.to.nodeForState(vc.context.name).resolveInjector; - return $view.load(vc, resolveInjector); - }; - return $q.all(enteringViews.map(loadView)).then(noop); - } - - function $loadAllControllerLocals() { - const loadLocals = (vc: ViewConfig) => { - let deps = annotateController(vc.controller); - let resolveInjector = treeChanges.to.nodeForState(vc.context.name).resolveInjector; - function $loadControllerLocals() { } - $loadControllerLocals.$inject = deps; - return $q.all(resolveInjector.getLocals($loadControllerLocals)).then((locals) => vc.locals = locals); - }; - - let loadAllLocals = enteringViews.filter(vc => !!vc.controller).map(loadLocals); - return $q.all(loadAllLocals).then(noop); - } + let stateHooks = new StateHooks($urlRouter, $view, $state, $stateParams, $q, transQueue, treeChangesQueue); + stateHooks.registerTransitionHooks(transition); - transition.onStart({}, hookBuilder.getEagerResolvePathFn(), { priority: 1000 }); - transition.onEnter({}, hookBuilder.getLazyResolveStateFn(), { priority: 1000 }); - - - let onEnterRegistration = (state) => transition.onEnter({to: state.name}, state.onEnter); - transition.entering().filter(state => !!state.onEnter).forEach(onEnterRegistration); - - let onRetainRegistration = (state) => transition.onRetain({}, state.onRetain); - transition.entering().filter(state => !!state.onRetain).forEach(onRetainRegistration); - - let onExitRegistration = (state) => transition.onExit({from: state.name}, state.onExit); - transition.exiting().filter(state => !!state.onExit).forEach(onExitRegistration); - - if (enteringViews.length) { - transition.onStart({}, $loadAllEnteringViews); - transition.onFinish({}, $loadAllControllerLocals); - } - if (exitingViews.length || enteringViews.length) - transition.onSuccess({}, $updateViews); transition.onError({}, $transitions.defaultErrorHandler()); // Commit global state data as the last hook in the transition (using a very low priority onFinish hook) - function $commitGlobalData() { stateHandler.transitionSuccess(transition.treeChanges(), transition); } + function $commitGlobalData() { stateHooks.transitionSuccess(transition); } transition.onFinish({}, $commitGlobalData, {priority: -10000}); - function $handleError($error$) { return stateHandler.transitionFailure(transition, $error$); } - let result = stateHandler.runTransition(transition).catch($handleError); + function $handleError($error$) { return stateHooks.transitionFailure(transition, $error$); } + + let result = stateHooks.runTransition(transition).catch($handleError); result.finally(() => transQueue.remove(transition)); // Return a promise for the transition, which also has the transition object on it. diff --git a/src/state/stateHandler.ts b/src/state/stateHandler.ts deleted file mode 100644 index d2ab3e733..000000000 --- a/src/state/stateHandler.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {IPromise, IQService} from "angular"; -import {copy, prop} from "../common/common"; -import Queue from "../common/queue"; - -import {ITreeChanges} from "../transition/interface"; -import {Transition} from "../transition/transition"; -import {TransitionRejection, RejectType} from "../transition/rejectFactory"; - -import {IStateService, IStateDeclaration} from "../state/interface"; - -export default class StateHandler { - constructor(private $urlRouter, - private $view, // service - private $state: IStateService, - private $stateParams, // service/obj - private $q: IQService, - private activeTransQ: Queue, - private changeHistory: Queue - ) { } - - runTransition(transition: Transition) { - this.activeTransQ.clear(); - this.activeTransQ.enqueue(transition); - return transition.run(); - } - - transitionSuccess(treeChanges: ITreeChanges, transition: Transition) { - let {$view, $state, activeTransQ, changeHistory} = this; - $view.sync(); - - // Update globals in $state - $state.$current = transition.$to(); - $state.current = $state.$current.self; - this.updateStateParams(transition); - activeTransQ.remove(transition); - changeHistory.enqueue(treeChanges); - - return transition; - } - - transitionFailure(transition: Transition, error): (IStateDeclaration|IPromise) { - let {$state, $stateParams, $q, activeTransQ} = this; - activeTransQ.remove(transition); - // 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. - if (!$state.$current.params.$$filter(prop('dynamic')).$$equals($stateParams, transition.params())) { - this.updateStateParams(transition); - } - return $state.current; - } - - if (error.type === RejectType.SUPERSEDED) { - if (error.redirected && error.detail instanceof Transition) { - activeTransQ.enqueue(error.detail); - return this.runTransition(error.detail); - } - } - } - - return $q.reject(error); - } - - updateStateParams(transition: Transition) { - let {$urlRouter, $state, $stateParams} = this; - let options = transition.options(); - $state.params = transition.params(); - copy($state.params, $stateParams); - $stateParams.$sync().$off(); - - if (options.location && $state.$current.navigable) { - $urlRouter.push($state.$current.navigable.url, $stateParams, { replace: options.location === 'replace' }); - } - - $urlRouter.update(true); - } -} diff --git a/src/state/stateHooks.ts b/src/state/stateHooks.ts new file mode 100644 index 000000000..06626d60f --- /dev/null +++ b/src/state/stateHooks.ts @@ -0,0 +1,142 @@ +import {IPromise, IQService} from "angular"; +import {copy, prop, noop} from "../common/common"; +import Queue from "../common/queue"; +import {annotateController} from "../common/angular1"; + +import {ITreeChanges} from "../transition/interface"; +import {Transition} from "../transition/transition"; +import {TransitionRejection, RejectType} from "../transition/rejectFactory"; + +import {IStateService, IStateDeclaration} from "../state/interface"; +import {ViewConfig} from "../view/view"; + +export default class StateHooks { + constructor( + private $urlRouter, + private $view, // service + private $state: IStateService, + private $stateParams, // service/obj + private $q: IQService, + private activeTransQ: Queue, + private changeHistory: Queue + ) { } + + runTransition(transition: Transition) { + this.activeTransQ.clear(); + this.activeTransQ.enqueue(transition); + return transition.run(); + } + + transitionSuccess(transition: Transition) { + let {$view, $state, activeTransQ, changeHistory} = this; + let treeChanges = transition.treeChanges(); + $view.sync(); + + // Update globals in $state + $state.$current = transition.$to(); + $state.current = $state.$current.self; + this.updateStateParams(transition); + activeTransQ.remove(transition); + changeHistory.enqueue(treeChanges); + + return transition; + } + + transitionFailure(transition: Transition, error): (IStateDeclaration|IPromise) { + let {$state, $stateParams, $q, activeTransQ} = this; + activeTransQ.remove(transition); + // 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. + if (!$state.$current.params.$$filter(prop('dynamic')).$$equals($stateParams, transition.params())) { + this.updateStateParams(transition); + } + return $state.current; + } + + if (error.type === RejectType.SUPERSEDED) { + if (error.redirected && error.detail instanceof Transition) { + activeTransQ.enqueue(error.detail); + return this.runTransition(error.detail); + } + } + } + + return $q.reject(error); + } + + updateStateParams(transition: Transition) { + let {$urlRouter, $state, $stateParams} = this; + let options = transition.options(); + $state.params = transition.params(); + copy($state.params, $stateParams); + $stateParams.$sync().$off(); + + if (options.location && $state.$current.navigable) { + $urlRouter.push($state.$current.navigable.url, $stateParams, { replace: options.location === 'replace' }); + } + + $urlRouter.update(true); + } + + + registerTransitionHooks(transition: Transition) { + let { $view, $q } = this; + + let hookBuilder = transition.hookBuilder(); + + // TODO: Move the Transition instance hook registration to its own function + // Add hooks + let enteringViews = transition.views("entering"); + let exitingViews = transition.views("exiting"); + let treeChanges = transition.treeChanges(); + + function $updateViews() { + exitingViews.forEach((viewConfig: ViewConfig) => $view.reset(viewConfig)); + enteringViews.forEach((viewConfig: ViewConfig) => $view.registerStateViewConfig(viewConfig)); + $view.sync(); + } + + function $loadAllEnteringViews() { + const loadView = (vc: ViewConfig) => { + let resolveInjector = treeChanges.to.nodeForState(vc.context.name).resolveInjector; + return $view.load(vc, resolveInjector); + }; + return $q.all(enteringViews.map(loadView)).then(noop); + } + + function $loadAllControllerLocals() { + const loadLocals = (vc: ViewConfig) => { + let deps = annotateController(vc.controller); + let resolveInjector = treeChanges.to.nodeForState(vc.context.name).resolveInjector; + function $loadControllerLocals() { } + $loadControllerLocals.$inject = deps; + return $q.all(resolveInjector.getLocals($loadControllerLocals)).then((locals) => vc.locals = locals); + }; + + let loadAllLocals = enteringViews.filter(vc => !!vc.controller).map(loadLocals); + return $q.all(loadAllLocals).then(noop); + } + + transition.onStart({}, hookBuilder.getEagerResolvePathFn(), { priority: 1000 }); + transition.onEnter({}, hookBuilder.getLazyResolveStateFn(), { priority: 1000 }); + + + let onEnterRegistration = (state) => transition.onEnter({to: state.name}, state.onEnter); + transition.entering().filter(state => !!state.onEnter).forEach(onEnterRegistration); + + let onRetainRegistration = (state) => transition.onRetain({}, state.onRetain); + transition.entering().filter(state => !!state.onRetain).forEach(onRetainRegistration); + + let onExitRegistration = (state) => transition.onExit({from: state.name}, state.onExit); + transition.exiting().filter(state => !!state.onExit).forEach(onExitRegistration); + + if (enteringViews.length) { + transition.onStart({}, $loadAllEnteringViews); + transition.onFinish({}, $loadAllControllerLocals); + } + if (exitingViews.length || enteringViews.length) + transition.onSuccess({}, $updateViews); + } +} From 6181a08f0ab4bb0acc4c170a42598ccdd81be139 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Tue, 22 Sep 2015 20:32:01 -0500 Subject: [PATCH 04/31] refactor(StateHooks): StateHooks now has a Transition as part of its context --- src/state/state.ts | 11 +++++------ src/state/stateHooks.ts | 32 +++++++++++++++++--------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/state/state.ts b/src/state/state.ts index d02f06630..4b8adecfa 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -691,18 +691,17 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { if (!transition.valid()) return $q.reject(transition.error()); - let stateHooks = new StateHooks($urlRouter, $view, $state, $stateParams, $q, transQueue, treeChangesQueue); - stateHooks.registerTransitionHooks(transition); + let stateHooks = new StateHooks(transition, $urlRouter, $view, $state, $stateParams, $q, transQueue, treeChangesQueue); + stateHooks.registerTransitionHooks(); transition.onError({}, $transitions.defaultErrorHandler()); // Commit global state data as the last hook in the transition (using a very low priority onFinish hook) - function $commitGlobalData() { stateHooks.transitionSuccess(transition); } + function $commitGlobalData() { stateHooks.transitionSuccess(); } transition.onFinish({}, $commitGlobalData, {priority: -10000}); - function $handleError($error$) { return stateHooks.transitionFailure(transition, $error$); } - - let result = stateHooks.runTransition(transition).catch($handleError); + function $handleError($error$) { return stateHooks.transitionFailure($error$); } + let result = stateHooks.runTransition().catch($handleError); result.finally(() => transQueue.remove(transition)); // Return a promise for the transition, which also has the transition object on it. diff --git a/src/state/stateHooks.ts b/src/state/stateHooks.ts index 06626d60f..c33fde1d9 100644 --- a/src/state/stateHooks.ts +++ b/src/state/stateHooks.ts @@ -12,6 +12,7 @@ import {ViewConfig} from "../view/view"; export default class StateHooks { constructor( + private transition: Transition, private $urlRouter, private $view, // service private $state: IStateService, @@ -21,44 +22,45 @@ export default class StateHooks { private changeHistory: Queue ) { } - runTransition(transition: Transition) { + runTransition() { this.activeTransQ.clear(); - this.activeTransQ.enqueue(transition); - return transition.run(); + this.activeTransQ.enqueue(this.transition); + return this.transition.run(); } - transitionSuccess(transition: Transition) { - let {$view, $state, activeTransQ, changeHistory} = this; + transitionSuccess() { + let {transition, $view, $state, activeTransQ, changeHistory} = this; let treeChanges = transition.treeChanges(); $view.sync(); // Update globals in $state $state.$current = transition.$to(); $state.current = $state.$current.self; - this.updateStateParams(transition); + this.updateStateParams(); activeTransQ.remove(transition); changeHistory.enqueue(treeChanges); return transition; } - transitionFailure(transition: Transition, error): (IStateDeclaration|IPromise) { - let {$state, $stateParams, $q, activeTransQ} = this; + transitionFailure(error): (IStateDeclaration|IPromise) { + let {transition, $urlRouter, $view, $state, $stateParams, $q, activeTransQ, changeHistory} = this; activeTransQ.remove(transition); // 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. if (!$state.$current.params.$$filter(prop('dynamic')).$$equals($stateParams, transition.params())) { - this.updateStateParams(transition); + this.updateStateParams(); } return $state.current; } if (error.type === RejectType.SUPERSEDED) { if (error.redirected && error.detail instanceof Transition) { - activeTransQ.enqueue(error.detail); - return this.runTransition(error.detail); + let stateHooks = new StateHooks(error.detail, $urlRouter, $view, $state, $stateParams, $q, activeTransQ, changeHistory); + stateHooks.registerTransitionHooks(); + return stateHooks.runTransition(); } } } @@ -66,8 +68,8 @@ export default class StateHooks { return $q.reject(error); } - updateStateParams(transition: Transition) { - let {$urlRouter, $state, $stateParams} = this; + updateStateParams() { + let {transition, $urlRouter, $state, $stateParams} = this; let options = transition.options(); $state.params = transition.params(); copy($state.params, $stateParams); @@ -81,8 +83,8 @@ export default class StateHooks { } - registerTransitionHooks(transition: Transition) { - let { $view, $q } = this; + registerTransitionHooks() { + let { transition, $view, $q } = this; let hookBuilder = transition.hookBuilder(); From bc4c335a517ece212a88341f96a0cbbcbac65248 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Tue, 22 Sep 2015 20:14:45 -0500 Subject: [PATCH 05/31] refactor(StateHandler): rename stateHandler to stateHooks, move $state's hook registration logic to renamed StateHooks class --- src/state/state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state/state.ts b/src/state/state.ts index 4b8adecfa..f90228727 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -1029,4 +1029,4 @@ function $StateParamsProvider() { angular.module('ui.router.state') .provider('$stateParams', $StateParamsProvider) .provider('$state', $StateProvider) - .run(['$state', function($state) { /* This effectively calls $get() to init when we enter runtime */ }]); \ No newline at end of file + .run(['$state', function($state) { /* This effectively calls $get() to init when we enter runtime */ }]); From 96df71e5740994106b8a80ead11c6976d6b10a6e Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Fri, 25 Sep 2015 12:50:28 -0500 Subject: [PATCH 06/31] refactor(StateHooks): WIP: Split out $state hook management further --- src/state/hooks/enterExitHooks.ts | 30 ++++++ src/state/hooks/resolveHooks.ts | 41 ++++++++ src/state/hooks/transitionManager.ts | 134 +++++++++++++++++++++++++ src/state/hooks/viewHooks.ts | 63 ++++++++++++ src/state/module.ts | 4 +- src/state/state.ts | 21 +--- src/state/stateHooks.ts | 144 --------------------------- src/transition/hookBuilder.ts | 36 ++----- src/transition/transition.ts | 2 +- src/transition/transitionService.ts | 4 +- 10 files changed, 284 insertions(+), 195 deletions(-) create mode 100644 src/state/hooks/enterExitHooks.ts create mode 100644 src/state/hooks/resolveHooks.ts create mode 100644 src/state/hooks/transitionManager.ts create mode 100644 src/state/hooks/viewHooks.ts delete mode 100644 src/state/stateHooks.ts diff --git a/src/state/hooks/enterExitHooks.ts b/src/state/hooks/enterExitHooks.ts new file mode 100644 index 000000000..e8b5d3fbc --- /dev/null +++ b/src/state/hooks/enterExitHooks.ts @@ -0,0 +1,30 @@ +import {Transition} from "../../transition/transition"; + +export default class EnterExitHooks { + private transition: Transition; + + constructor(transition: Transition) { + this.transition = transition; + } + + registerHooks() { + this.registerOnEnterHooks(); + this.registerOnRetainHooks(); + this.registerOnExitHooks(); + } + + registerOnEnterHooks() { + let onEnterRegistration = (state) => this.transition.onEnter({to: state.name}, state.onEnter); + this.transition.entering().filter(state => !!state.onEnter).forEach(onEnterRegistration); + } + + registerOnRetainHooks() { + let onRetainRegistration = (state) => this.transition.onRetain({}, state.onRetain); + this.transition.retained().filter(state => !!state.onRetain).forEach(onRetainRegistration); + } + + registerOnExitHooks() { + let onExitRegistration = (state) => this.transition.onExit({from: state.name}, state.onExit); + this.transition.exiting().filter(state => !!state.onExit).forEach(onExitRegistration); + } +} diff --git a/src/state/hooks/resolveHooks.ts b/src/state/hooks/resolveHooks.ts new file mode 100644 index 000000000..faff64840 --- /dev/null +++ b/src/state/hooks/resolveHooks.ts @@ -0,0 +1,41 @@ +import {extend} from "../../common/common"; + +import {ResolvePolicy} from "../../resolve/interface"; + +import {Transition} from "../../transition/transition"; + + +let LAZY = ResolvePolicy[ResolvePolicy.LAZY]; +let EAGER = ResolvePolicy[ResolvePolicy.EAGER]; + +/** + * Registers Eager and Lazy (for entering states) resolve hooks + * + * * registers a hook that resolves EAGER resolves, for the To Path, onStart of the transition + * * registers a hook that resolves LAZY resolves, for each state, before it is entered + */ +export default class ResolveHooks { + constructor(private transition: Transition) { } + + registerHooks() { + let treeChanges = this.transition.treeChanges(); + + /** a function which resolves any EAGER Resolvables for a Path */ + $eagerResolvePath.$inject = ['$transition$']; + function $eagerResolvePath($transition$) { + return treeChanges.to.last().resolveContext.resolvePath(extend({transition: $transition$}, { resolvePolicy: EAGER })); + } + + /** Returns a function which pre-resolves any LAZY Resolvables for a Node in a Path */ + $lazyResolveEnteringState.$inject = ['$state$', '$transition$']; + function $lazyResolveEnteringState($state$, $transition$) { + let node = treeChanges.entering.nodeForState($state$); + return node.resolveContext.resolvePathElement(node.state, extend({transition: $transition$}, { resolvePolicy: LAZY })); + } + + // Resolve eager resolvables before when the transition starts + this.transition.onStart({}, $eagerResolvePath, { priority: 1000 }); + // Resolve lazy resolvables before each state is entered + this.transition.onEnter({}, $lazyResolveEnteringState, { priority: 1000 }); + } +} diff --git a/src/state/hooks/transitionManager.ts b/src/state/hooks/transitionManager.ts new file mode 100644 index 000000000..a99cf0796 --- /dev/null +++ b/src/state/hooks/transitionManager.ts @@ -0,0 +1,134 @@ +import {IPromise, IQService} from "angular"; +import {copy, prop} from "../../common/common"; +import Queue from "../../common/queue"; + +import {ITreeChanges} from "../../transition/interface"; +import {Transition} from "../../transition/transition"; +import {TransitionRejection, RejectType} from "../../transition/rejectFactory"; + +import {IStateService, IStateDeclaration} from "../interface"; +import ViewHooks from "./viewHooks"; +import EnterExitHooks from "./enterExitHooks"; +import ResolveHooks from "./resolveHooks"; + +/** + * This class takes a blank transition object and adds all the hooks necessary for it to behave like a state transition + * + * * Chains to the transition promise, adding state lifecycle hooks: + * * on promise resolve: update global state such as "active transitions" and "current state/params" + * * on promise rejection: handles ignored transition (as dynamic), and transition redirect (starts new transition) + * + * * Registers view hooks, which maintain the list of active view configs and sync with/update the ui-views + * + * * Registers onEnter/onRetain/onExit hooks which delegate to the state's hooks of the same name, at the appropriate time + * + * * Registers eager and lazy resolve hooks + */ +export default class TransitionManager { + private treeChanges: ITreeChanges; + private enterExitHooks: EnterExitHooks; + private viewHooks: ViewHooks; + private resolveHooks: ResolveHooks; + + constructor( + private transition: Transition, + private $transitions, + private $urlRouter, + private $view, // service + private $state: IStateService, + private $stateParams, // service/obj + private $q: IQService, // TODO: get from runtime.$q + private activeTransQ: Queue, + private changeHistory: Queue + ) { + this.viewHooks = new ViewHooks(transition, $view); + this.enterExitHooks = new EnterExitHooks(transition); + this.resolveHooks = new ResolveHooks(transition); + + this.treeChanges = transition.treeChanges(); + } + + runTransition(): IPromise { + let stateHooks = this; + this.activeTransQ.clear(); // TODO: nuke this + this.activeTransQ.enqueue(this.transition); + const $removeFromActiveQ = () => this.activeTransQ.remove(this.transition); + function $handleError($error$) { return stateHooks.transitionFailure($error$); } + + return this.transition.run().catch($handleError).finally($removeFromActiveQ); + } + + transitionSuccess() { + let {treeChanges, transition, $state, activeTransQ, changeHistory} = this; + + // Update globals in $state + $state.$current = transition.$to(); + $state.current = $state.$current.self; + this.updateStateParams(); + activeTransQ.remove(transition); + changeHistory.enqueue(treeChanges); + + return transition; + } + + transitionFailure(error): (IStateDeclaration|IPromise) { + let {transition, $transitions, $urlRouter, $view, $state, $stateParams, $q, activeTransQ, changeHistory} = this; + activeTransQ.remove(transition); + // 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. + if (!$state.$current.params.$$filter(prop('dynamic')).$$equals($stateParams, transition.params())) { + this.updateStateParams(); + } + return $state.current; + } + + if (error.type === RejectType.SUPERSEDED) { + if (error.redirected && error.detail instanceof Transition) { + let redirect = error.detail; + + let tMgr = new TransitionManager(redirect, $transitions, $urlRouter, $view, $state, $stateParams, $q, activeTransQ, changeHistory); + tMgr.registerHooks(); + return tMgr.runTransition(); + } + } + } + + return $q.reject(error); + } + + updateStateParams() { + let {transition, $urlRouter, $state, $stateParams} = this; + let options = transition.options(); + $state.params = transition.params(); + copy($state.params, $stateParams); + $stateParams.$sync().$off(); + + if (options.location && $state.$current.navigable) { + $urlRouter.push($state.$current.navigable.url, $stateParams, { replace: options.location === 'replace' }); + } + + $urlRouter.update(true); + } + + registerHooks() { + this.registerDefaultErrorHandler(); + this.registerTransitionSuccess(); + + this.viewHooks.registerHooks(); + this.enterExitHooks.registerHooks(); + this.resolveHooks.registerHooks(); + } + + registerDefaultErrorHandler() { + this.transition.onError({}, this.$transitions.defaultErrorHandler()); + } + + registerTransitionSuccess() { + let self = this; + // Commit global state data as the last hook in the transition (using a very low priority onFinish hook) + function $commitGlobalData() { self.transitionSuccess(); } + this.transition.onFinish({}, $commitGlobalData, {priority: -10000}); + } +} diff --git a/src/state/hooks/viewHooks.ts b/src/state/hooks/viewHooks.ts new file mode 100644 index 000000000..238936c27 --- /dev/null +++ b/src/state/hooks/viewHooks.ts @@ -0,0 +1,63 @@ +import {IPromise} from "angular"; +import {noop} from "../../common/common"; +import {annotateController, runtime} from "../../common/angular1"; + +import {ITreeChanges} from "../../transition/interface"; +import {Transition} from "../../transition/transition"; + +import {ViewConfig} from "../../view/view"; + +export default class ViewHooks { + private treeChanges: ITreeChanges; + private enteringViews: ViewConfig[]; + private exitingViews: ViewConfig[]; + private transition: Transition; + private $view; // service + + constructor(transition: Transition, $view) { + this.transition = transition; + this.$view = $view; + + this.treeChanges = transition.treeChanges(); + this.enteringViews = transition.views("entering"); + this.exitingViews = transition.views("exiting"); + } + + loadAllEnteringViews() { + const loadView = (vc: ViewConfig) => { + let resolveInjector = this.treeChanges.to.nodeForState(vc.context.name).resolveInjector; + return > this.$view.load(vc, resolveInjector); + }; + return runtime.$q.all(this.enteringViews.map(loadView)).then(noop); + } + + loadAllControllerLocals() { + const loadLocals = (vc: ViewConfig) => { + let deps = annotateController(vc.controller); + let resolveInjector = this.treeChanges.to.nodeForState(vc.context.name).resolveInjector; + function $loadControllerLocals() { } + $loadControllerLocals.$inject = deps; + return runtime.$q.all(resolveInjector.getLocals($loadControllerLocals)).then((locals) => vc.locals = locals); + }; + + let loadAllLocals = this.enteringViews.filter(vc => !!vc.controller).map(loadLocals); + return runtime.$q.all(loadAllLocals).then(noop); + } + + updateViews() { + let $view = this.$view; + this.exitingViews.forEach((viewConfig: ViewConfig) => $view.reset(viewConfig)); + this.enteringViews.forEach((viewConfig: ViewConfig) => $view.registerStateViewConfig(viewConfig)); + $view.sync(); + } + + registerHooks() { + if (this.enteringViews.length) { + this.transition.onStart({}, this.loadAllEnteringViews.bind(this)); + this.transition.onFinish({}, this.loadAllControllerLocals.bind(this)); + } + + if (this.exitingViews.length || this.enteringViews.length) + this.transition.onSuccess({}, this.updateViews.bind(this)); + } +} diff --git a/src/state/module.ts b/src/state/module.ts index 787ad22f3..4c5d146d3 100644 --- a/src/state/module.ts +++ b/src/state/module.ts @@ -16,8 +16,8 @@ export {stateEvents}; import * as stateFilters from "./stateFilters"; export {stateFilters}; -import * as stateHooks from "./stateHooks"; -export {stateHooks}; +import * as transitionManager from "./hooks/transitionManager"; +export {transitionManager}; import * as stateMatcher from "./stateMatcher"; export {stateMatcher}; diff --git a/src/state/state.ts b/src/state/state.ts index f90228727..5fcc67362 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -1,4 +1,4 @@ -import {extend, defaults, copy, equalForKeys, forEach, ancestors, noop, isDefined, isObject, isString} from "../common/common"; +import {extend, defaults, copy, equalForKeys, forEach, ancestors, isDefined, isObject, isString} from "../common/common"; import Queue from "../common/queue"; import {IServiceProviderFactory, IPromise} from "angular"; @@ -7,7 +7,6 @@ import Glob from "./glob"; import StateQueueManager from "./stateQueueManager"; import StateBuilder from "./stateBuilder"; import StateMatcher from "./stateMatcher"; -import StateHooks from "./stateHooks"; import TargetState from "./targetState"; import {ITransitionService, ITransitionOptions, ITreeChanges} from "../transition/interface"; @@ -20,6 +19,7 @@ import Path from "../path/path"; import PathFactory from "../path/pathFactory"; import {IRawParams, IParamsOrArray} from "../params/interface"; +import TransitionManager from "./hooks/transitionManager"; /** * @ngdoc object @@ -691,21 +691,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { if (!transition.valid()) return $q.reject(transition.error()); - let stateHooks = new StateHooks(transition, $urlRouter, $view, $state, $stateParams, $q, transQueue, treeChangesQueue); - stateHooks.registerTransitionHooks(); - - transition.onError({}, $transitions.defaultErrorHandler()); - - // Commit global state data as the last hook in the transition (using a very low priority onFinish hook) - function $commitGlobalData() { stateHooks.transitionSuccess(); } - transition.onFinish({}, $commitGlobalData, {priority: -10000}); - - function $handleError($error$) { return stateHooks.transitionFailure($error$); } - let result = stateHooks.runTransition().catch($handleError); - result.finally(() => transQueue.remove(transition)); - + let tMgr = new TransitionManager(transition, $transitions, $urlRouter, $view, $state, $stateParams, $q, transQueue, treeChangesQueue); + tMgr.registerHooks(); // Return a promise for the transition, which also has the transition object on it. - return extend(result, { transition }); + return extend(tMgr.runTransition(), { transition }); }; /** diff --git a/src/state/stateHooks.ts b/src/state/stateHooks.ts deleted file mode 100644 index c33fde1d9..000000000 --- a/src/state/stateHooks.ts +++ /dev/null @@ -1,144 +0,0 @@ -import {IPromise, IQService} from "angular"; -import {copy, prop, noop} from "../common/common"; -import Queue from "../common/queue"; -import {annotateController} from "../common/angular1"; - -import {ITreeChanges} from "../transition/interface"; -import {Transition} from "../transition/transition"; -import {TransitionRejection, RejectType} from "../transition/rejectFactory"; - -import {IStateService, IStateDeclaration} from "../state/interface"; -import {ViewConfig} from "../view/view"; - -export default class StateHooks { - constructor( - private transition: Transition, - private $urlRouter, - private $view, // service - private $state: IStateService, - private $stateParams, // service/obj - private $q: IQService, - private activeTransQ: Queue, - private changeHistory: Queue - ) { } - - runTransition() { - this.activeTransQ.clear(); - this.activeTransQ.enqueue(this.transition); - return this.transition.run(); - } - - transitionSuccess() { - let {transition, $view, $state, activeTransQ, changeHistory} = this; - let treeChanges = transition.treeChanges(); - $view.sync(); - - // Update globals in $state - $state.$current = transition.$to(); - $state.current = $state.$current.self; - this.updateStateParams(); - activeTransQ.remove(transition); - changeHistory.enqueue(treeChanges); - - return transition; - } - - transitionFailure(error): (IStateDeclaration|IPromise) { - let {transition, $urlRouter, $view, $state, $stateParams, $q, activeTransQ, changeHistory} = this; - activeTransQ.remove(transition); - // 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. - if (!$state.$current.params.$$filter(prop('dynamic')).$$equals($stateParams, transition.params())) { - this.updateStateParams(); - } - return $state.current; - } - - if (error.type === RejectType.SUPERSEDED) { - if (error.redirected && error.detail instanceof Transition) { - let stateHooks = new StateHooks(error.detail, $urlRouter, $view, $state, $stateParams, $q, activeTransQ, changeHistory); - stateHooks.registerTransitionHooks(); - return stateHooks.runTransition(); - } - } - } - - return $q.reject(error); - } - - updateStateParams() { - let {transition, $urlRouter, $state, $stateParams} = this; - let options = transition.options(); - $state.params = transition.params(); - copy($state.params, $stateParams); - $stateParams.$sync().$off(); - - if (options.location && $state.$current.navigable) { - $urlRouter.push($state.$current.navigable.url, $stateParams, { replace: options.location === 'replace' }); - } - - $urlRouter.update(true); - } - - - registerTransitionHooks() { - let { transition, $view, $q } = this; - - let hookBuilder = transition.hookBuilder(); - - // TODO: Move the Transition instance hook registration to its own function - // Add hooks - let enteringViews = transition.views("entering"); - let exitingViews = transition.views("exiting"); - let treeChanges = transition.treeChanges(); - - function $updateViews() { - exitingViews.forEach((viewConfig: ViewConfig) => $view.reset(viewConfig)); - enteringViews.forEach((viewConfig: ViewConfig) => $view.registerStateViewConfig(viewConfig)); - $view.sync(); - } - - function $loadAllEnteringViews() { - const loadView = (vc: ViewConfig) => { - let resolveInjector = treeChanges.to.nodeForState(vc.context.name).resolveInjector; - return $view.load(vc, resolveInjector); - }; - return $q.all(enteringViews.map(loadView)).then(noop); - } - - function $loadAllControllerLocals() { - const loadLocals = (vc: ViewConfig) => { - let deps = annotateController(vc.controller); - let resolveInjector = treeChanges.to.nodeForState(vc.context.name).resolveInjector; - function $loadControllerLocals() { } - $loadControllerLocals.$inject = deps; - return $q.all(resolveInjector.getLocals($loadControllerLocals)).then((locals) => vc.locals = locals); - }; - - let loadAllLocals = enteringViews.filter(vc => !!vc.controller).map(loadLocals); - return $q.all(loadAllLocals).then(noop); - } - - transition.onStart({}, hookBuilder.getEagerResolvePathFn(), { priority: 1000 }); - transition.onEnter({}, hookBuilder.getLazyResolveStateFn(), { priority: 1000 }); - - - let onEnterRegistration = (state) => transition.onEnter({to: state.name}, state.onEnter); - transition.entering().filter(state => !!state.onEnter).forEach(onEnterRegistration); - - let onRetainRegistration = (state) => transition.onRetain({}, state.onRetain); - transition.entering().filter(state => !!state.onRetain).forEach(onRetainRegistration); - - let onExitRegistration = (state) => transition.onExit({from: state.name}, state.onExit); - transition.exiting().filter(state => !!state.onExit).forEach(onExitRegistration); - - if (enteringViews.length) { - transition.onStart({}, $loadAllEnteringViews); - transition.onFinish({}, $loadAllControllerLocals); - } - if (exitingViews.length || enteringViews.length) - transition.onSuccess({}, $updateViews); - } -} diff --git a/src/transition/hookBuilder.ts b/src/transition/hookBuilder.ts index 05b02ab13..219422c16 100644 --- a/src/transition/hookBuilder.ts +++ b/src/transition/hookBuilder.ts @@ -3,7 +3,7 @@ import {IPromise} from "angular"; import {IInjectable, extend, isPromise, isArray, assertPredicate, unnestR} from "../common/common"; import {runtime} from "../common/angular1"; -import {ITransitionOptions, ITransitionHookOptions, ITreeChanges, IEventHook, ITransitionService} from "./interface"; +import {ITransitionOptions, ITransitionHookOptions, IHookRegistry, ITreeChanges, IEventHook, ITransitionService} from "./interface"; import TransitionHook from "./transitionHook"; import {Transition} from "./transition"; @@ -11,9 +11,6 @@ import {IState} from "../state/interface"; import {ITransPath, ITransNode} from "../path/interface"; -import {ResolvePolicy, IOptions1} from "../resolve/interface"; -import {IHookRegistry} from "./interface"; - interface IToFrom { to: IState; from: IState; @@ -40,6 +37,7 @@ let successErrorOptions: ITransitionHookOptions = { */ export default class HookBuilder { + treeChanges: ITreeChanges; transitionOptions: ITransitionOptions; toState: IState; @@ -47,9 +45,10 @@ export default class HookBuilder { transitionLocals: {[key: string]: any}; - constructor(private $transitions: ITransitionService, private treeChanges: ITreeChanges, private transition: Transition, private baseHookOptions: ITransitionHookOptions) { - this.toState = treeChanges.to.last().state; - this.fromState = treeChanges.from.last().state; + constructor(private $transitions: ITransitionService, private transition: Transition, private baseHookOptions: ITransitionHookOptions) { + this.treeChanges = transition.treeChanges(); + this.toState = this.treeChanges.to.last().state; + this.fromState = this.treeChanges.from.last().state; this.transitionOptions = transition.options(); this.transitionLocals = { $transition$: transition }; } @@ -161,27 +160,4 @@ export default class HookBuilder { .filter(matchFilter) // Only those satisfying matchCriteria .sort(prioritySort); // Order them by .priority field } - - /** Returns a function which resolves the LAZY Resolvables for a Node in a Path */ - getLazyResolveStateFn() { - let options = { resolvePolicy: ResolvePolicy[ResolvePolicy.LAZY] }; - let treeChanges = this.treeChanges; - $lazyResolveEnteringState.$inject = ['$state$', '$transition$']; - function $lazyResolveEnteringState($state$, $transition$) { - let node = treeChanges.entering.nodeForState($state$); - return node.resolveContext.resolvePathElement(node.state, extend({transition: $transition$}, options)); - } - return $lazyResolveEnteringState; - } - - /** Returns a function which resolves the EAGER Resolvables for a Path */ - getEagerResolvePathFn() { - let options: IOptions1 = { resolvePolicy: ResolvePolicy[ResolvePolicy.EAGER] }; - let path = this.treeChanges.to; - $eagerResolvePath.$inject = ['$transition$']; - function $eagerResolvePath($transition$) { - return path.last().resolveContext.resolvePath(extend({transition: $transition$}, options)); - } - return $eagerResolvePath; - } } diff --git a/src/transition/transition.ts b/src/transition/transition.ts index 4ac93adb3..faf6b7580 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -267,7 +267,7 @@ export class Transition implements IHookRegistry { current: this._options.current }; - return new HookBuilder($transitions, this._treeChanges, this, baseHookOptions); + return new HookBuilder($transitions, this, baseHookOptions); } run () { diff --git a/src/transition/transitionService.ts b/src/transition/transitionService.ts index 42c37f0bb..ec1060b80 100644 --- a/src/transition/transitionService.ts +++ b/src/transition/transitionService.ts @@ -28,7 +28,7 @@ export let defaultTransOpts: ITransitionOptions = { current : () => null }; -class TransitionService implements IHookRegistry { +class TransitionService implements ITransitionService, IHookRegistry { constructor() { this._reinit(); } @@ -64,7 +64,7 @@ class TransitionService implements IHookRegistry { } -let $transitions: ITransitionService = new TransitionService(); +let $transitions = new TransitionService(); $TransitionProvider.prototype = $transitions; function $TransitionProvider() { From d98b32ee65fb940c74b4f67972473ad987b33268 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sun, 27 Sep 2015 13:00:15 -0500 Subject: [PATCH 07/31] refactor(TransitionManager): Improve clarity of TransitionManager code --- src/state/hooks/transitionManager.ts | 73 +++++++++++++--------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/src/state/hooks/transitionManager.ts b/src/state/hooks/transitionManager.ts index a99cf0796..0418a0aff 100644 --- a/src/state/hooks/transitionManager.ts +++ b/src/state/hooks/transitionManager.ts @@ -12,11 +12,15 @@ import EnterExitHooks from "./enterExitHooks"; import ResolveHooks from "./resolveHooks"; /** - * This class takes a blank transition object and adds all the hooks necessary for it to behave like a state transition + * This class: * - * * Chains to the transition promise, adding state lifecycle hooks: - * * on promise resolve: update global state such as "active transitions" and "current state/params" - * * on promise rejection: handles ignored transition (as dynamic), and transition redirect (starts new transition) + * * Takes a blank transition object and adds all the hooks necessary for it to behave like a state transition. + * + * * Runs the transition, returning a chained promise which: + * * transforms the resolved Transition.promise to the final destination state. + * * manages the rejected Transition.promise, checking for Dynamic or Redirected transitions + * + * * Registers a handler to update global $state data such as "active transitions" and "current state/params" * * * Registers view hooks, which maintain the list of active view configs and sync with/update the ui-views * @@ -48,32 +52,38 @@ export default class TransitionManager { this.treeChanges = transition.treeChanges(); } + registerHooks() { + this.registerUpdateGlobalState(); + + this.viewHooks.registerHooks(); + this.enterExitHooks.registerHooks(); + this.resolveHooks.registerHooks(); + } + runTransition(): IPromise { - let stateHooks = this; this.activeTransQ.clear(); // TODO: nuke this this.activeTransQ.enqueue(this.transition); - const $removeFromActiveQ = () => this.activeTransQ.remove(this.transition); - function $handleError($error$) { return stateHooks.transitionFailure($error$); } - - return this.transition.run().catch($handleError).finally($removeFromActiveQ); + 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 + .finally(() => this.activeTransQ.remove(this.transition)); } - transitionSuccess() { - let {treeChanges, transition, $state, activeTransQ, changeHistory} = this; + 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; - this.updateStateParams(); - activeTransQ.remove(transition); changeHistory.enqueue(treeChanges); - - return transition; + this.updateStateParams(); } - transitionFailure(error): (IStateDeclaration|IPromise) { - let {transition, $transitions, $urlRouter, $view, $state, $stateParams, $q, activeTransQ, changeHistory} = this; - activeTransQ.remove(transition); + transRejected(error): (IStateDeclaration|IPromise) { + let {transition, $state, $stateParams, $q} = this; // Handle redirect and abort if (error instanceof TransitionRejection) { if (error.type === RejectType.IGNORED) { @@ -86,15 +96,15 @@ export default class TransitionManager { if (error.type === RejectType.SUPERSEDED) { if (error.redirected && error.detail instanceof Transition) { - let redirect = error.detail; - - let tMgr = new TransitionManager(redirect, $transitions, $urlRouter, $view, $state, $stateParams, $q, activeTransQ, changeHistory); + let tMgr = this._redirectMgr(error.detail); tMgr.registerHooks(); return tMgr.runTransition(); } } } + this.$transitions.defaultErrorHandler()(error); + return $q.reject(error); } @@ -112,23 +122,8 @@ export default class TransitionManager { $urlRouter.update(true); } - registerHooks() { - this.registerDefaultErrorHandler(); - this.registerTransitionSuccess(); - - this.viewHooks.registerHooks(); - this.enterExitHooks.registerHooks(); - this.resolveHooks.registerHooks(); - } - - registerDefaultErrorHandler() { - this.transition.onError({}, this.$transitions.defaultErrorHandler()); - } - - registerTransitionSuccess() { - let self = this; - // Commit global state data as the last hook in the transition (using a very low priority onFinish hook) - function $commitGlobalData() { self.transitionSuccess(); } - this.transition.onFinish({}, $commitGlobalData, {priority: -10000}); + 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); } } From 46f65bb5a4696cda07a10060657bed6181e4a57b Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Tue, 29 Sep 2015 19:10:29 -0500 Subject: [PATCH 08/31] feat(Resolve): When redirecting from a parent to a child, re-use any resolved resolvables fetched during the original transition. closes #2274 --- src/common/common.ts | 13 ++++++++++++ src/path/path.ts | 13 ++++++++++++ src/transition/transition.ts | 14 +++++++++++-- test/resolveSpec.ts | 38 +++++++++++++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index c8430ff69..7cf105147 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -306,6 +306,19 @@ export function assertPredicate(fn: Predicate, errMsg: string = "assert fa /** Like _.pairs: Given an object, returns an array of key/value pairs */ export const pairs = (object) => Object.keys(object).map(key => [ key, object[key]] ); +/** + * Given two parallel arrays, returns an array of tuples, where each tuple is composed of [ left[i], right[i] ] + * Optionally, a map function can be provided. It will be applied to each left and right element before adding it to the tuple. + * + * let foo = [ 0, 2, 4, 6 ]; + * let bar = [ 1, 3, 5, 7 ]; + * paired(foo, bar); // [ [0, 1], [2, 3], [4, 5], [6, 7] ] + * paired(foo, bar, x => x * 2); // [ [0, 2], [4, 6], [8, 10], [12, 14] ] + * + */ +export const paired = (left: any[], right: any[], mapFn: Function = identity) => + left.map((lval, idx) => [ mapFn(lval), mapFn(right[idx]) ]); + /** * Sets a key/val pair on an object, then returns the object. * diff --git a/src/path/path.ts b/src/path/path.ts index 68ad4dfd8..6fb019f18 100644 --- a/src/path/path.ts +++ b/src/path/path.ts @@ -52,6 +52,19 @@ export default class Path { return new Path(this._nodes.slice(start, end).map(shallowNodeCopy)); } + /** + * Returns a new Path which is a subpath of this Path. The new Path starts from root and contains any nodes + * that match the nodes in the otherPath. Nodes are compared using their state properties. + * @param otherPath {Path} + * @returns {Path} + */ + matching(otherPath: Path): Path { + let otherNodes = otherPath._nodes; + let matchedCount = this._nodes.reduce((prev, node, i) => + prev === i && i < otherNodes.length && node.state === otherNodes[i].state ? i + 1 : prev, 0); + return this.slice(matchedCount); + } + /** * Returns a new Path which is a copy of this Path, but with nodes in reverse order. * Each node in the reversed path is a shallow copy of the original Path's node. diff --git a/src/transition/transition.ts b/src/transition/transition.ts index faf6b7580..4ec783e45 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -236,9 +236,19 @@ export class Transition implements IHookRegistry { * @returns {Transition} Returns a new `Transition` instance. */ redirect(targetState: TargetState): Transition { - let newOptions = extend({}, this.options(), targetState.options(), { previous: this} ); + let newOptions = extend({}, this.options(), targetState.options(), { previous: this } ); targetState = new TargetState(targetState.identifier(), targetState.$state(), targetState.params(), newOptions); - return new Transition(this._treeChanges.from, targetState); + + let redirectTo = new Transition(this._treeChanges.from, targetState); + + // If the current transition has already resolved any resolvables which are also in the redirected "to path", then + // add those resolvables to the redirected transition. Allows you to define a resolve at a parent level, wait for + // the resolve, then redirect to a child state based on the result, and not have to re-fetch the resolve. + let redirectedPath = this.treeChanges().to; + let matching = redirectTo.treeChanges().to.matching(redirectedPath); + matching.nodes().forEach((node, idx) => node.ownResolvables = redirectedPath.nodes()[idx].ownResolvables); + + return redirectTo; } /** diff --git a/test/resolveSpec.ts b/test/resolveSpec.ts index ab09bdb85..00fe47df8 100644 --- a/test/resolveSpec.ts +++ b/test/resolveSpec.ts @@ -11,7 +11,7 @@ import {IParamsPath, IResolvePath} from "../src/path/interface" import Path from "../src/path/path" import PathFactory from "../src/path/pathFactory" -import {omit, map, pick, prop} from "../src/common/common" +import {omit, map, pick, prop, extend, forEach} from "../src/common/common" let module = angular.mock.module; /////////////////////////////////////////////// @@ -611,3 +611,39 @@ describe("State transitions with resolves", function() { expect(counts).toEqualData(expectCounts); })); }); + + + +// Integration tests +describe("Integration: Resolvables system", () => { + beforeEach(module(function ($stateProvider) { + let copy = {}; + forEach(statesMap, (stateDef, name) => { + copy[name] = extend({}, stateDef); + }); + + angular.forEach(copy, stateDef => { + if (stateDef.name) $stateProvider.state(stateDef); + }); + })); + + let $state, $rootScope, $transitions, $trace; + beforeEach(inject((_$state_, _$rootScope_, _$transitions_, _$trace_) => { + $state = _$state_; + $rootScope = _$rootScope_; + $transitions = _$transitions_; + $trace = _$trace_; + })); + + + it("should not re-resolve data, when redirecting to a child", () => { + $transitions.onStart({to: "J"}, ($transition$, _J) => { + expect(counts._J).toEqualData(1); + return $transition$.redirect($state.targetState("K")); + }); + $state.go("J"); + $rootScope.$digest(); + expect($state.current.name).toBe("K"); + expect(counts._J).toEqualData(1); + }); +}); \ No newline at end of file From ef46b46fb9c1d4fef08d48feae7407996c648f92 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Thu, 1 Oct 2015 19:51:18 -0500 Subject: [PATCH 09/31] refactor(HookRegistry): slightly simplify the deregistration fn --- src/transition/hookRegistry.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/transition/hookRegistry.ts b/src/transition/hookRegistry.ts index c06f7bcb8..899c3ac7f 100644 --- a/src/transition/hookRegistry.ts +++ b/src/transition/hookRegistry.ts @@ -1,4 +1,4 @@ -import {IInjectable, extend, val, isString, isFunction} from "../common/common"; +import {IInjectable, extend, val, isString, isFunction, removeFrom} from "../common/common"; import {IState} from "../state/interface"; import Glob from "../state/glob"; @@ -53,15 +53,13 @@ export class EventHook implements IEventHook { interface ITransitionEvents { [key: string]: IEventHook[]; } // Return a registration function of the requested type. -function makeHookRegistrationFn(transitionEvents: ITransitionEvents, eventType: string): IHookRegistration { +function makeHookRegistrationFn(hooks: ITransitionEvents, name: string): IHookRegistration { return function (matchObject, callback, options = {}) { let eventHook = new EventHook(matchObject, callback, options); - let hooks = transitionEvents[eventType]; - hooks.push(eventHook); + hooks[name].push(eventHook); return function deregisterEventHook() { - let idx = hooks.indexOf(eventHook); - if (idx !== -1) hooks.splice(idx, 1); + removeFrom(hooks[name])(eventHook); }; }; } From 5726cd24b9beb7febc4c48c8c19d06858e7082bd Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Thu, 1 Oct 2015 20:50:12 -0500 Subject: [PATCH 10/31] refactor(Transition): broke out transition run method into TransitionRunner class --- src/transition/hookBuilder.ts | 23 --------- src/transition/transition.ts | 43 ++-------------- src/transition/transitionRunner.ts | 79 ++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 63 deletions(-) create mode 100644 src/transition/transitionRunner.ts diff --git a/src/transition/hookBuilder.ts b/src/transition/hookBuilder.ts index 219422c16..42ddce50b 100644 --- a/src/transition/hookBuilder.ts +++ b/src/transition/hookBuilder.ts @@ -108,29 +108,6 @@ export default class HookBuilder { return path.nodes().map(hooksForNode); } - /** - * Given an array of TransitionHooks, runs each one synchronously and sequentially. - * - * Returns a promise chain composed of any promises returned from each hook.invokeStep() call - */ - runSynchronousHooks(hooks: TransitionHook[], locals = {}, swallowExceptions: boolean = false): IPromise { - let promises = []; - for (let i = 0; i < hooks.length; i++) { - try { - let hookResult = hooks[i].invokeStep(locals); - // If a hook returns a promise, that promise is added to an array to be resolved asynchronously. - if (hookResult && isPromise(hookResult)) - promises.push(hookResult); - } catch (ex) { - if (!swallowExceptions) throw ex; - console.log("Swallowed exception during synchronous hook handler: " + ex); // TODO: What to do here? - } - } - - let resolvedPromise = runtime.$q.when(undefined); - return promises.reduce((memo, val) => memo.then(() => val), resolvedPromise); - } - /** Given a node and a callback function, builds a TransitionHook */ buildHook(node: ITransNode, fn: IInjectable, moreLocals?, options: ITransitionHookOptions = {}): TransitionHook { let locals = extend({}, this.transitionLocals, moreLocals); diff --git a/src/transition/transition.ts b/src/transition/transition.ts index 4ec783e45..68ccc0f9c 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -7,6 +7,7 @@ import {ITransitionOptions, ITransitionHookOptions, ITreeChanges, IHookRegistry, import $transitions from "./transitionService"; import {HookRegistry, matchState} from "./hookRegistry"; import HookBuilder from "./hookBuilder"; +import TransitionRunner from "./transitionRunner"; import {RejectFactory} from "./rejectFactory"; import {ITransPath} from "../path/interface"; @@ -19,7 +20,7 @@ import ParamValues from "../params/paramValues"; import {ViewConfig} from "../view/view"; -import {extend, flatten, unnest, forEach, identity, omit, isObject, not, prop, toJson, val, abstractKey} from "../common/common"; +import {extend, unnest, omit, isObject, not, prop, toJson, val, abstractKey} from "../common/common"; let transitionCount = 0, REJECT = new RejectFactory(); const stateSelf: (_state: IState) => IStateDeclaration = prop("self"); @@ -296,39 +297,6 @@ export class Transition implements IHookRegistry { return this.promise; } - // ----------------------------------------------------------------------- - // Transition Steps - // ----------------------------------------------------------------------- - - let hookBuilder = this.hookBuilder(); - - let onBeforeHooks = hookBuilder.getOnBeforeHooks(); - // ---- Synchronous hooks ---- - // Run the "onBefore" hooks and save their promises - let chain = hookBuilder.runSynchronousHooks(onBeforeHooks); - - // Build the async hooks *after* running onBefore hooks. - // The synchronous onBefore hooks may register additional async hooks on-the-fly. - let onStartHooks = hookBuilder.getOnStartHooks(); - let onExitHooks = hookBuilder.getOnExitHooks(); - let onRetainHooks = hookBuilder.getOnRetainHooks(); - let onEnterHooks = hookBuilder.getOnEnterHooks(); - let onFinishHooks = hookBuilder.getOnFinishHooks(); - let onSuccessHooks = hookBuilder.getOnSuccessHooks(); - let onErrorHooks = hookBuilder.getOnErrorHooks(); - - // Set up a promise chain. Add the steps' promises in appropriate order to the promise chain. - let asyncSteps = flatten([onStartHooks, onExitHooks, onRetainHooks, onEnterHooks, onFinishHooks]).filter(identity); - - // ---- Asynchronous section ---- - // The results of the sync hooks is a promise chain (rejected or otherwise) that begins the async portion of the transition. - // Build the rest of the chain off the sync promise chain out of all the asynchronous steps - forEach(asyncSteps, function (step) { - // Don't pass prev as locals to invokeStep() - chain = chain.then((prev) => step.invokeStep()); - }); - - // When the chain is complete, then resolve or reject the deferred const resolve = () => { this._deferred.resolve(this); @@ -341,12 +309,7 @@ export class Transition implements IHookRegistry { return runtime.$q.reject(error); }; - chain = chain.then(resolve, reject); - - // When the promise has settled (i.e., the transition is complete), then invoke the registered success or error hooks - const runSuccessHooks = () => hookBuilder.runSynchronousHooks(onSuccessHooks, {}, true); - const runErrorHooks = ($error$) => hookBuilder.runSynchronousHooks(onErrorHooks, { $error$ }, true); - this.promise.then(runSuccessHooks).catch(runErrorHooks); + new TransitionRunner(this, resolve, reject).run(); return this.promise; } diff --git a/src/transition/transitionRunner.ts b/src/transition/transitionRunner.ts new file mode 100644 index 000000000..0c7478909 --- /dev/null +++ b/src/transition/transitionRunner.ts @@ -0,0 +1,79 @@ +import {val, flatten, identity, isPromise, Predicate} from "../common/common"; +import {runtime} from "../common/angular1"; +import {IPromise} from "angular"; +import HookBuilder from "./hookBuilder"; +import {Transition} from "./transition"; +import TransitionHook from "./transitionHook"; + +export default class TransitionRunner { + private hookBuilder: HookBuilder; + + constructor(private transition: Transition, private _resolve, private _reject) { + this.hookBuilder = transition.hookBuilder(); + } + + run(): IPromise { + const runSuccessHooks = () => runSynchronousHooks(this.success(), {}, true); + const runErrorHooks = ($error$) => runSynchronousHooks(this.error(), { $error$ }, true); + // Run the success/error hooks *after* the Transition promise is settled. + this.transition.promise.then(runSuccessHooks, runErrorHooks); + + // ---- Synchronous hooks ---- + // Run the "onBefore" sync hooks + // The results of the sync hooks is an async promise chain (which gets rejected or resolved) + let chain = runSynchronousHooks(this.before()); + + // ---- Asynchronous section ---- + // Chain off the promise, build the remainder of the chain using each async step. + chain = this.async().reduce((_chain, step) => _chain.then(step.invokeStep), chain); + + // Make sure to settle the Transition promise, using the supplied callbacks and return the full chain. + return chain.then(this._resolve, this._reject); + } + + before() { + return this.hookBuilder.getOnBeforeHooks(); + } + + async() { + let hookBuilder = this.hookBuilder; + // Build the async hooks *after* running onBefore hooks. + // The synchronous onBefore hooks may register additional async hooks on-the-fly. + let onStartHooks = hookBuilder.getOnStartHooks(); + let onExitHooks = hookBuilder.getOnExitHooks(); + let onRetainHooks = hookBuilder.getOnRetainHooks(); + let onEnterHooks = hookBuilder.getOnEnterHooks(); + let onFinishHooks = hookBuilder.getOnFinishHooks(); + + return flatten([onStartHooks, onExitHooks, onRetainHooks, onEnterHooks, onFinishHooks]).filter(identity); + } + + success() { + return this.hookBuilder.getOnSuccessHooks(); + } + + error() { + return this.hookBuilder.getOnErrorHooks(); + } +} + +/** + * Given an array of TransitionHooks, runs each one synchronously and sequentially. + * + * Returns a promise chain composed of any promises returned from each hook.invokeStep() call + */ +export function runSynchronousHooks(hooks: TransitionHook[], locals = {}, swallowExceptions: boolean = false): IPromise { + function invokeSwallowExceptions(hook) { + try { + return hook.invokeStep(locals); + } catch (exception) { + if (!swallowExceptions) + throw exception; + console.log("Swallowed exception during synchronous hook handler: " + exception); // TODO: What to do here? + } + } + + return hooks.map(invokeSwallowExceptions) + .filter(> isPromise) + .reduce((chain, promise) => chain.then(val(promise)), runtime.$q.when(undefined)); +} From f065353c0e0a2d6278033a15a18a696c9236db7b Mon Sep 17 00:00:00 2001 From: Nate Abele Date: Mon, 21 Sep 2015 15:46:14 -0400 Subject: [PATCH 11/31] refactor(Type): extract ArrayType pseudo-class --- src/params/type.ts | 110 +++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 58 deletions(-) diff --git a/src/params/type.ts b/src/params/type.ts index 02c54231b..6ece253e8 100644 --- a/src/params/type.ts +++ b/src/params/type.ts @@ -1,5 +1,54 @@ import {extend, isArray, isDefined, filter, map} from "../common/common"; +/** + * Wraps up a `Type` object to handle array values. + */ +function ArrayType(type, mode) { + // Wrap non-array value as array + function arrayWrap(val): any[] { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); } + + // Unwrap array value for "auto" mode. Return undefined for empty array. + function arrayUnwrap(val) { + switch(val.length) { + case 0: return undefined; + case 1: return mode === "auto" ? val[0] : val; + default: return val; + } + } + + // Wraps type (.is/.encode/.decode) functions to operate on each value of an array + function arrayHandler(callback, allTruthyMode?: boolean) { + return function handleArray(val) { + let arr = arrayWrap(val); + var result = map(arr, callback); + return (allTruthyMode === true) ? filter(result, val => !val).length === 0 : arrayUnwrap(result); + }; + } + + // Wraps type (.equals) functions to operate on each value of an array + function arrayEqualsHandler(callback) { + return function handleArray(val1, val2) { + var left = arrayWrap(val1), right = arrayWrap(val2); + if (left.length !== right.length) return false; + for (var i = 0; i < left.length; i++) { + if (!callback(left[i], right[i])) return false; + } + return true; + }; + } + + ['encode', 'decode', 'equals', '$normalize'].map(name => { + this[name] = (name === 'equals' ? arrayEqualsHandler : arrayHandler)(type[name].bind(type)); + }); + + extend(this, { + name: type.name, + pattern: type.pattern, + is: arrayHandler(type.is.bind(type), true), + $arrayMode: mode + }); +} + /** * @ngdoc object * @name ui.router.util.type:Type @@ -31,7 +80,7 @@ import {extend, isArray, isDefined, filter, map} from "../common/common"; * @returns {Object} Returns a new `Type` object. */ export default class Type { - pattern: RegExp; + pattern: RegExp = /.*/; name: string; raw: boolean; @@ -116,7 +165,7 @@ export default class Type { } toString() { - return "{Type:" + this.name + "}"; + return `{Type:${this.name}}`; } /** Given an encoded string, or a decoded object, returns a decoded object */ @@ -137,61 +186,6 @@ export default class Type { $asArray(mode, isSearch) { if (!mode) return this; if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only"); - - function ArrayType(type, mode) { - function bindTo(type, callbackName) { - return function() { - return type[callbackName].apply(type, arguments); - }; - } - - // Wrap non-array value as array - function arrayWrap(val): any[] { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); } - // Unwrap array value for "auto" mode. Return undefined for empty array. - function arrayUnwrap(val) { - switch(val.length) { - case 0: return undefined; - case 1: return mode === "auto" ? val[0] : val; - default: return val; - } - } - function falsey(val) { return !val; } - - // Wraps type (.is/.encode/.decode) functions to operate on each value of an array - function arrayHandler(callback, allTruthyMode?: boolean) { - return function handleArray(val) { - let arr = arrayWrap(val); - var result = map(arr, callback); - if (allTruthyMode === true) - return filter(result, falsey).length === 0; - return arrayUnwrap(result); - }; - } - - // Wraps type (.equals) functions to operate on each value of an array - function arrayEqualsHandler(callback) { - return function handleArray(val1, val2) { - var left = arrayWrap(val1), right = arrayWrap(val2); - if (left.length !== right.length) return false; - for (var i = 0; i < left.length; i++) { - if (!callback(left[i], right[i])) return false; - } - return true; - }; - } - - this.encode = arrayHandler(bindTo(type, 'encode')); - this.decode = arrayHandler(bindTo(type, 'decode')); - this.is = arrayHandler(bindTo(type, 'is'), true); - this.equals = arrayEqualsHandler(bindTo(type, 'equals')); - this.pattern = type.pattern; - this.$normalize = arrayHandler(bindTo(type, '$normalize')); - this.name = type.name; - this.$arrayMode = mode; - } - return new ArrayType(this, mode); } -} - -Type.prototype.pattern = /.*/; +} \ No newline at end of file From 7175a5ce526404e21c35a52ca1fc7495abc3f8e3 Mon Sep 17 00:00:00 2001 From: Nate Abele Date: Thu, 24 Sep 2015 17:14:48 -0400 Subject: [PATCH 12/31] refactor(*): simplify function definitions --- src/common/common.ts | 19 ++++++++------ src/params/param.ts | 6 ++--- src/params/paramTypes.ts | 49 ++++++++++++++++++------------------ src/params/paramValues.ts | 4 +-- src/path/path.ts | 9 +++---- src/path/pathFactory.ts | 9 +++---- src/state/stateBuilder.ts | 3 +-- src/url/urlMatcher.ts | 27 ++++++++++---------- src/url/urlMatcherFactory.ts | 29 ++++++--------------- 9 files changed, 70 insertions(+), 85 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index 7cf105147..0de6a391d 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -58,6 +58,15 @@ export function pipe(...funcs: Function[]): (obj: any) => any { */ export const prop = (name: string) => (obj: any) => obj && obj[name]; +/** + * Given a property name and a value, returns a function that returns a boolean based on whether + * the passed object has a property that matches the value + * let obj = { foo: 1, name: "blarg" }; + * let getName = propEq("name", "blarg"); + * getName(obj) === true + */ +export const propEq = curry((name: string, val: any, obj: any) => obj && obj[name] === val); + /** * Given a dotted property name, returns a function that returns a nested property from an object, or undefined * let obj = { id: 1, nestedObj: { foo: 1, name: "blarg" }, }; @@ -72,16 +81,14 @@ export const parse = (name: string) => pipe.apply(null, name.split(".").map(prop * Given a function that returns a truthy or falsey value, returns a * function that returns the opposite (falsey or truthy) value given the same inputs */ -export const not = (fn) => (function() { return !fn.apply(null, [].slice.call(arguments)); }); +export const not = (fn) => (...args) => !fn.apply(null, args); /** * Given two functions that return truthy or falsey values, returns a function that returns truthy * if both functions return truthy for the given arguments */ export function and(fn1, fn2): Function { - return function() { - return fn1.apply(null, [].slice.call(arguments)) && fn2.apply(null, [].slice.call(arguments)); - }; + return (...args) => fn1.apply(null, args) && fn2.apply(null, args); } /** @@ -89,9 +96,7 @@ export function and(fn1, fn2): Function { * if at least one of the functions returns truthy for the given arguments */ export function or(fn1, fn2): Function { - return function() { - return fn1.apply(null, [].slice.call(arguments)) || fn2.apply(null, [].slice.call(arguments)); - }; + return (...args) => fn1.apply(null, args) || fn2.apply(null, args); } /** Given a class, returns a Predicate function that returns true if the object is of that class */ diff --git a/src/params/param.ts b/src/params/param.ts index 49e22ee54..79f8858c9 100644 --- a/src/params/param.ts +++ b/src/params/param.ts @@ -1,4 +1,4 @@ -import {isInjectable, extend, isDefined, isString, isArray, filter, map, prop, curry} from "../common/common"; +import {isInjectable, extend, isDefined, isString, isArray, filter, map, pick, prop, propEq, curry} from "../common/common"; import {runtime} from "../common/angular1"; import matcherConfig from "../url/urlMatcherConfig"; import paramTypes from "./paramTypes"; @@ -97,10 +97,8 @@ export default class Param { return defaultValue; }; - const hasReplaceVal = curry((val, obj) => obj.from === val); - const $replace = (value) => { - var replacement: any = map(filter(this.replace, hasReplaceVal(value)), prop("to")); + var replacement: any = map(filter(this.replace, propEq('from', value)), prop("to")); return replacement.length ? replacement[0] : value; }; diff --git a/src/params/paramTypes.ts b/src/params/paramTypes.ts index 4f1144a9c..e1d60f43e 100644 --- a/src/params/paramTypes.ts +++ b/src/params/paramTypes.ts @@ -1,64 +1,64 @@ -import {isDefined, fromJson, toJson, isObject, identity, equals, inherit, map, extend} from "../common/common"; +import {isDefined, fromJson, toJson, is, identity, equals, inherit, map, extend, val} from "../common/common"; import Type from "./type"; import {runtime} from "../common/angular1"; -function valToString(val) { return val != null ? val.toString().replace(/\//g, "%2F") : val; } -function valFromString(val) { return val != null ? val.toString().replace(/%2F/g, "/") : val; } +const swapString = (search, replace) => val => val != null ? val.toString().replace(search, replace) : val; +const valToString = swapString(/\//g, "%2F"); +const valFromString = swapString(/%2F/g, "/"); class ParamTypes { types: any; enqueue: boolean = true; typeQueue: any[] = []; - private defaultTypes:any = { + private defaultTypes: any = { hash: { encode: valToString, decode: valFromString, - is: function(val) { return typeof val === "string"; }, + is: is(String), pattern: /.*/, - equals: function() { return true; } + equals: val(true) }, string: { encode: valToString, decode: valFromString, - is: function(val) { return typeof val === "string"; }, + is: is(String), pattern: /[^/]*/ }, int: { encode: valToString, - decode: function(val) { return parseInt(val, 10); }, - is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; }, + decode(val) { return parseInt(val, 10); }, + is(val) { return isDefined(val) && this.decode(val.toString()) === val; }, pattern: /\d+/ }, bool: { - encode: function(val) { return val ? 1 : 0; }, - decode: function(val) { return parseInt(val, 10) !== 0; }, - is: function(val) { return val === true || val === false; }, + encode: val => val && 1 || 0, + decode: val => parseInt(val, 10) !== 0, + is: is(Boolean), pattern: /0|1/ }, date: { - encode: function (val) { - if (!this.is(val)) - return undefined; - return [ val.getFullYear(), + encode(val) { + return !this.is(val) ? undefined : [ + val.getFullYear(), ('0' + (val.getMonth() + 1)).slice(-2), ('0' + val.getDate()).slice(-2) ].join("-"); }, - decode: function (val) { + decode(val) { if (this.is(val)) return val; var match = this.capture.exec(val); return match ? new Date(match[1], match[2] - 1, match[3]) : undefined; }, - is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); }, - equals: function (a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); }, + is: (val) => val instanceof Date && !isNaN(val.valueOf()), + equals(a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); }, pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/, capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/ }, json: { encode: toJson, decode: fromJson, - is: isObject, + is: is(Object), equals: equals, pattern: /[^/]*/ }, @@ -72,17 +72,18 @@ class ParamTypes { constructor() { // Register default types. Store them in the prototype of this.types. - const makeType = (definition, name) => new Type(extend({name: name}, definition)); + const makeType = (definition, name) => new Type(extend({ name }, definition)); this.types = inherit(map(this.defaultTypes, makeType), {}); } type(name, definition?: any, definitionFn?: Function) { if (!isDefined(definition)) return this.types[name]; - if (this.types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined."); + if (this.types.hasOwnProperty(name)) throw new Error(`A type named '${name}' has already been defined.`); + + this.types[name] = new Type(extend({ name }, definition)); - this.types[name] = new Type(extend({ name: name }, definition)); if (definitionFn) { - this.typeQueue.push({ name: name, def: definitionFn }); + this.typeQueue.push({ name, def: definitionFn }); if (!this.enqueue) this._flushTypeQueue(); } return this; diff --git a/src/params/paramValues.ts b/src/params/paramValues.ts index 078254343..9ff529f14 100644 --- a/src/params/paramValues.ts +++ b/src/params/paramValues.ts @@ -1,13 +1,13 @@ import {IParamsPath} from "../path/interface"; import {IRawParams} from "../params/interface"; -import {extend, find} from "../common/common"; +import {extend, find, curry} from "../common/common"; /** * This class closes over a Path and encapsulates the parameter values from the Path's Nodes. * The param values for the path are flattened and copied to the resulting ParamValues object. * Param values for a specific state are exposed with the $byState(stateName) function. */ -const stateNameMatches = (stateName: string) => (node) => node.state.name === stateName; +const stateNameMatches = curry((stateName: string, node) => node.state.name === stateName); export default class ParamValues implements IRawParams { [key: string]: any diff --git a/src/path/path.ts b/src/path/path.ts index 6fb019f18..3b7857a95 100644 --- a/src/path/path.ts +++ b/src/path/path.ts @@ -70,9 +70,7 @@ export default class Path { * Each node in the reversed path is a shallow copy of the original Path's node. */ reverse(): Path { - let copy = [].concat(this._nodes).map(shallowNodeCopy); - copy.reverse(); - return new Path(copy); + return new Path(this.nodes().map(shallowNodeCopy).reverse()); } /** Returns the "state" property of each node in this Path */ @@ -98,12 +96,11 @@ export default class Path { /** Returns a new path where each path element is mapped using the nodeMapper function */ adapt(nodeMapper: (NODE, idx?) => T): Path { - var adaptedNodes = this._nodes.map(nodeMapper); - return new Path(adaptedNodes); + return new Path(this._nodes.map(nodeMapper)); } toString() { - var elements = this._nodes.map(e => e.state.name).join(", "); + var elements = this._nodes.map(parse('state.name')).join(', '); return `Path([${elements}])`; } } \ No newline at end of file diff --git a/src/path/pathFactory.ts b/src/path/pathFactory.ts index 7826507d8..13297eccb 100644 --- a/src/path/pathFactory.ts +++ b/src/path/pathFactory.ts @@ -38,7 +38,7 @@ export default class PathFactory { } /* Given params and a state, creates an IParamsNode */ - static makeParamsNode = curry(function(params: IRawParams, state: IState) { + static makeParamsNode = curry((params: IRawParams, state: IState) => { return { state, ownParamValues: state.ownParams.$$values(params) @@ -135,16 +135,13 @@ export default class PathFactory { * Computes the tree changes (entering, exiting) between a fromPath and toPath. */ static treeChanges(fromPath: ITransPath, toPath: IParamsPath, reloadState: IState): ITreeChanges { - function nonDynamicParams(state) { - return state.params.$$filter(not(prop('dynamic'))); - } - let fromNodes = fromPath.nodes(); let toNodes = toPath.nodes(); let keep = 0, max = Math.min(fromNodes.length, toNodes.length); + const staticParams = (state) => state.params.$$filter(not(prop('dynamic'))); const nodesMatch = (node1: IParamsNode, node2: IParamsNode) => - node1.state === node2.state && nonDynamicParams(node1.state).$$equals(node1.ownParamValues, node2.ownParamValues); + node1.state === node2.state && staticParams(node1.state).$$equals(node1.ownParams, node2.ownParams); while (keep < max && fromNodes[keep].state !== reloadState && nodesMatch(fromNodes[keep], toNodes[keep])) { keep++; diff --git a/src/state/stateBuilder.ts b/src/state/stateBuilder.ts index 769e157c0..ff52f3cc6 100644 --- a/src/state/stateBuilder.ts +++ b/src/state/stateBuilder.ts @@ -68,10 +68,9 @@ export default function StateBuilder(root, matcher, $urlMatcherFactoryProvider) forEach(state.views || { "$default": pick(state, allKeys) }, function (config, name) { name = name || "$default"; // Account for views: { "": { template... } } // Allow controller settings to be defined at the state level for all views - forEach(ctrlKeys, function(key) { + forEach(ctrlKeys, (key) => { if (state[key] && !config[key]) config[key] = state[key]; }); - if (Object.keys(config).length > 0) views[name] = config; }); return views; diff --git a/src/url/urlMatcher.ts b/src/url/urlMatcher.ts index 1a9735726..9ce27a3d1 100644 --- a/src/url/urlMatcher.ts +++ b/src/url/urlMatcher.ts @@ -8,6 +8,18 @@ interface params { $$validates: (params: string) => Array; } +function quoteRegExp(string, pattern?: any, squash?: any, optional?: any) { + var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + if (!pattern) return result; + + switch (squash) { + case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break; + case true: surroundPattern = ['?(', ')?']; break; + default: surroundPattern = [`(${squash}|`, ')?']; break; + } + return result + surroundPattern[0] + pattern + surroundPattern[1]; +} + /** * @ngdoc object * @name ui.router.util.type:UrlMatcher @@ -116,17 +128,6 @@ export default class UrlMatcher { return params[id]; } - function quoteRegExp(string, pattern?: any, squash?: any, optional?: any) { - var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); - if (!pattern) return result; - switch(squash) { - case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break; - case true: surroundPattern = ['?(', ')?']; break; - default: surroundPattern = [`(${squash}|`, ')?']; break; - } - return result + surroundPattern[0] + pattern + surroundPattern[1]; - } - this.source = pattern; // Split into static segments separated by path parameter placeholders. @@ -258,8 +259,8 @@ export default class UrlMatcher { if (nPath !== m.length - 1) throw new Error(`Unbalanced capture group in route '${this.source}'`); function decodePathArray(string: string) { - function reverseString(str: string) { return str.split("").reverse().join(""); } - function unquoteDashes(str: string) { return str.replace(/\\-/g, "-"); } + const reverseString = (str: string) => str.split("").reverse().join(""); + const unquoteDashes = (str: string) => str.replace(/\\-/g, "-"); var split = reverseString(string).split(/-(?!\\)/); var allReversed = map(split, reverseString); diff --git a/src/url/urlMatcherFactory.ts b/src/url/urlMatcherFactory.ts index bb9c58ff2..1e0b5b117 100644 --- a/src/url/urlMatcherFactory.ts +++ b/src/url/urlMatcherFactory.ts @@ -37,9 +37,7 @@ function $UrlMatcherFactory() { * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`; * @returns {boolean} the current value of caseInsensitive */ - this.caseInsensitive = function(value) { - return matcherConfig.caseInsensitive(value); - }; + this.caseInsensitive = matcherConfig.caseInsensitive.bind(matcherConfig); /** * @ngdoc function @@ -52,9 +50,7 @@ function $UrlMatcherFactory() { * @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`. * @returns {boolean} the current value of strictMode */ - this.strictMode = function(value) { - return matcherConfig.strictMode(value); - }; + this.strictMode = matcherConfig.strictMode.bind(matcherConfig); /** * @ngdoc function @@ -71,9 +67,7 @@ function $UrlMatcherFactory() { * any other string, e.g. "~": When generating an href with a default parameter value, squash (remove) * the parameter value from the URL and replace it with this string. */ - this.defaultSquashPolicy = function(value) { - return matcherConfig.defaultSquashPolicy(value); - }; + this.defaultSquashPolicy = matcherConfig.defaultSquashPolicy.bind(matcherConfig); /** * @ngdoc function @@ -87,9 +81,7 @@ function $UrlMatcherFactory() { * @param {Object} config The config object hash. * @returns {UrlMatcher} The UrlMatcher. */ - this.compile = function (pattern, config) { - return new UrlMatcher(pattern, extend(getDefaultConfig(), config)); - }; + this.compile = (pattern, config) => new UrlMatcher(pattern, extend(getDefaultConfig(), config)); /** * @ngdoc function @@ -103,14 +95,12 @@ function $UrlMatcherFactory() { * @returns {Boolean} Returns `true` if the object matches the `UrlMatcher` interface, by * implementing all the same methods. */ - this.isMatcher = function (o) { + this.isMatcher = o => { if (!isObject(o)) return false; var result = true; - forEach(UrlMatcher.prototype, function(val, name) { - if (isFunction(val)) { - result = result && (isDefined(o[name]) && isFunction(o[name])); - } + forEach(UrlMatcher.prototype, (val, name) => { + if (isFunction(val)) result = result && (isDefined(o[name]) && isFunction(o[name])); }); return result; }; @@ -234,10 +224,7 @@ function $UrlMatcherFactory() { return this; }; - - this.UrlMatcher = UrlMatcher; - this.Param = Param; - this.ParamSet = ParamSet; + extend(this, { UrlMatcher, Param, ParamSet }); } // Register as a provider so it's available to other providers From c3bada12527ea0e1a579e1e9198ca864ab71a27f Mon Sep 17 00:00:00 2001 From: Nate Abele Date: Fri, 25 Sep 2015 10:19:12 -0400 Subject: [PATCH 13/31] style(*): whitespace cleanup --- src/params/param.ts | 4 ++-- src/params/paramValues.ts | 2 +- src/path/path.ts | 8 ++++---- src/path/pathFactory.ts | 19 +++++++++---------- src/state/state.ts | 14 +++++++------- src/transition/transition.ts | 8 ++++---- src/url/urlMatcher.ts | 10 ++++++---- src/url/urlRouter.ts | 15 +++++++++------ test/stateDirectivesSpec.js | 2 +- test/stateSpec.js | 2 +- test/transitionSpec.js | 2 +- test/urlMatcherFactorySpec.js | 8 ++++---- test/urlRouterSpec.js | 2 +- 13 files changed, 50 insertions(+), 46 deletions(-) diff --git a/src/params/param.ts b/src/params/param.ts index 79f8858c9..54f41c036 100644 --- a/src/params/param.ts +++ b/src/params/param.ts @@ -47,8 +47,8 @@ export default class Param { // array config: param name (param[]) overrides default settings. explicit config overrides param name. function getArrayMode() { - var arrayDefaults = {array: (location === "search" ? "auto" : false)}; - var arrayParamNomenclature = id.match(/\[\]$/) ? {array: true} : {}; + var arrayDefaults = { array: (location === "search" ? "auto" : false) }; + var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {}; return extend(arrayDefaults, arrayParamNomenclature, config).array; } diff --git a/src/params/paramValues.ts b/src/params/paramValues.ts index 9ff529f14..c884f6383 100644 --- a/src/params/paramValues.ts +++ b/src/params/paramValues.ts @@ -4,7 +4,7 @@ import {IRawParams} from "../params/interface"; import {extend, find, curry} from "../common/common"; /** * This class closes over a Path and encapsulates the parameter values from the Path's Nodes. - * The param values for the path are flattened and copied to the resulting ParamValues object. + * The param values for the path are flattened and copied to the resulting ParamValues object. * Param values for a specific state are exposed with the $byState(stateName) function. */ const stateNameMatches = curry((stateName: string, node) => node.state.name === stateName); diff --git a/src/path/path.ts b/src/path/path.ts index 3b7857a95..afbefe720 100644 --- a/src/path/path.ts +++ b/src/path/path.ts @@ -10,18 +10,18 @@ const stateNameMatches = (stateName: string) => (node) => node.state.name === st const shallowNodeCopy = node => extend({}, node); /** - * A Path Object represents a Path of nested States within the State Hierarchy. + * A Path Object represents a Path of nested States within the State Hierarchy. * Each node of the path holds the IState object, and additional data, according - * to the use case. + * to the use case. * - * A Path can be used to construct new Paths based on the current Path via the concat + * A Path can be used to construct new Paths based on the current Path via the concat * and slice helper methods. * * @param _nodes [array]: an array of INode data */ export default class Path { constructor(private _nodes: NODE[]) { } - + /** * returns a subpath of this path from the root path element up to and including the toState parameter. * Each node of the subpath is a shallow copy of the original node. diff --git a/src/path/pathFactory.ts b/src/path/pathFactory.ts index 13297eccb..9c84ee35f 100644 --- a/src/path/pathFactory.ts +++ b/src/path/pathFactory.ts @@ -78,8 +78,8 @@ export default class PathFactory { let node = path.nodeForState(state); return extend({}, node && node.ownParamValues); } - - /** + + /** * Given an IParamsNode "toNode", return a new IParamsNode with param values inherited from the * matching node in fromPath. Only inherit keys that aren't found in "toKeys" from the node in "fromPath"" */ @@ -94,7 +94,7 @@ export default class PathFactory { let ownParamValues: IRawParams = extend(toParamVals, fromParamVals, incomingParamVals); return { state: toNode.state, ownParamValues }; }); - + // The param keys specified by the incoming toParams return new Path( toPath.nodes().map(makeInheritedParamsNode(fromPath, toKeys))); } @@ -120,13 +120,12 @@ export default class PathFactory { // Attach bound resolveContext and paramValues to each node // Attach views to each node transPath.nodes().forEach((node: ITransNode) => { - node.resolveContext = resolveContext.isolateRootTo(node.state); - node.resolveInjector = new ResolveInjector(node.resolveContext, node.state); - node.paramValues = paramValues.$isolateRootTo(node.state.name); - node.ownResolvables["$stateParams"] = new Resolvable("$stateParams", () => node.paramValues, node.state); - node.views = makeViews(node); - } - ); + node.resolveContext = resolveContext.isolateRootTo(node.state); + node.resolveInjector = new ResolveInjector(node.resolveContext, node.state); + node.paramValues = paramValues.$isolateRootTo(node.state.name); + node.ownResolvables["$stateParams"] = new Resolvable("$stateParams", () => node.paramValues, node.state); + node.views = makeViews(node); + }); return transPath; } diff --git a/src/state/state.ts b/src/state/state.ts index 5fcc67362..9ce997a57 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -377,16 +377,16 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { let invalidCallbacks: Function[] = []; this.onInvalid = onInvalid; - + /** * @ngdoc function * @name ui.router.state.$stateProvider#onInvalid * @methodOf ui.router.state.$stateProvider * * @description - * Registers a function to be injected and invoked when transitionTo has been called with an invalid + * Registers a function to be injected and invoked when transitionTo has been called with an invalid * state reference parameter - * + * * This function can be injected with one some special values: * - **`$to$`**: TargetState * - **`$from$`**: TargetState @@ -396,7 +396,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { * The function may optionally return a {TargetState} or a Promise for a TargetState. If one * is returned, it is treated as a redirect. */ - + function onInvalid(callback: Function) { invalidCallbacks.push(callback); } @@ -495,7 +495,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { stateQueue.flush($state); stateQueue.autoFlush = true; // Autoflush once we are in runtime - + /** * @ngdoc function * @name ui.router.state.$state#reload @@ -621,7 +621,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { let transOpts = defaults(options, defautGoOpts, defaultTransOpts); return $state.transitionTo(to, params, transOpts); }; - + /** Factory method for creating a TargetState */ $state.targetState = function targetState(identifier: IStateOrName, params: IParamsOrArray, options: ITransitionOptions = {}): TargetState { let stateDefinition = matcher.find(identifier, options.relative); @@ -827,7 +827,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { * first parameter, then the constructed href url will be built from the first navigable ancestor (aka * ancestor with a valid url). * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. - * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), * defines which state to be relative from. * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". * diff --git a/src/transition/transition.ts b/src/transition/transition.ts index 68ccc0f9c..46c45a810 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -121,11 +121,11 @@ export class Transition implements IHookRegistry { is(compare: (Transition|{to: any, from: any})) { if (compare instanceof Transition) { // TODO: Also compare parameters - return this.is({to: compare.$to().name, from: compare.$from().name}); + return this.is({ to: compare.$to().name, from: compare.$from().name }); } return !( - (compare.to && !matchState(this.$to(), compare.to)) || - (compare.from && !matchState(this.$from(), compare.from)) + (compare.to && !matchState(this.$to(), compare.to)) || + (compare.from && !matchState(this.$from(), compare.from)) ); } @@ -237,7 +237,7 @@ export class Transition implements IHookRegistry { * @returns {Transition} Returns a new `Transition` instance. */ redirect(targetState: TargetState): Transition { - let newOptions = extend({}, this.options(), targetState.options(), { previous: this } ); + let newOptions = extend({}, this.options(), targetState.options(), { previous: this }); targetState = new TargetState(targetState.identifier(), targetState.$state(), targetState.params(), newOptions); let redirectTo = new Transition(this._treeChanges.from, targetState); diff --git a/src/url/urlMatcher.ts b/src/url/urlMatcher.ts index 9ce27a3d1..6a02fb1ca 100644 --- a/src/url/urlMatcher.ts +++ b/src/url/urlMatcher.ts @@ -138,7 +138,9 @@ export default class UrlMatcher { cfg = config.params[id]; segment = pattern.substring(last, m.index); regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); - type = paramTypes.type(regexp || "string") || inherit(paramTypes.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) }); + type = paramTypes.type(regexp || "string") || inherit(paramTypes.type("string"), { + pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) + }); return {id, regexp, segment, type, cfg}; } @@ -238,8 +240,8 @@ export default class UrlMatcher { * @example *
    * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', {
- *   x: '1', q: 'hello'
- * });
+   *   x: '1', q: 'hello'
+   * });
    * // returns { id: 'bob', q: 'hello', r: null }
    * 
* @@ -270,7 +272,7 @@ export default class UrlMatcher { for (i = 0; i < nPath; i++) { paramName = paramNames[i]; var param = this.params[paramName]; - var paramVal: (any|any[]) = m[i+1]; + var paramVal: (any|any[]) = m[i + 1]; // if the param value matches a pre-replace pair, replace the value before decoding. for (j = 0; j < param.replace; j++) { if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; diff --git a/src/url/urlRouter.ts b/src/url/urlRouter.ts index 4ce9a00b7..9a9cde864 100644 --- a/src/url/urlRouter.ts +++ b/src/url/urlRouter.ts @@ -10,9 +10,9 @@ import {IServiceProviderFactory} from "angular"; * @requires $locationProvider * * @description - * `$urlRouterProvider` has the responsibility of watching `$location`. - * When `$location` changes it runs through a list of rules one by one until a - * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify + * `$urlRouterProvider` has the responsibility of watching `$location`. + * When `$location` changes it runs through a list of rules one by one until a + * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify * a url in a state configuration. All urls are compiled into a UrlMatcher object. * * There are several methods on `$urlRouterProvider` that make it useful to use directly @@ -97,8 +97,8 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { * }); * * - * @param {string|function} rule The url path you want to redirect to or a function - * rule that returns the url path. The function version is passed two params: + * @param {string|function} rule The url path you want to redirect to or a function + * rule that returns the url path. The function version is passed two params: * `$injector` and `$location` services, and must return a url string. * * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance @@ -195,7 +195,10 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { } }; - var check = { matcher: $urlMatcherFactory.isMatcher(what), regex: what instanceof RegExp }; + var check = { + matcher: $urlMatcherFactory.isMatcher(what), + regex: what instanceof RegExp + }; for (var n in check) { if (check[n]) return this.rule(strategies[n](what, handler)); diff --git a/test/stateDirectivesSpec.js b/test/stateDirectivesSpec.js index 4ecb6d3c5..798364657 100644 --- a/test/stateDirectivesSpec.js +++ b/test/stateDirectivesSpec.js @@ -162,7 +162,7 @@ describe('uiStateRef', function() { timeoutFlush(); $q.flush(); - + expect($state.current.name).toEqual('top'); expect(obj($stateParams)).toEqualData({ }); })); diff --git a/test/stateSpec.js b/test/stateSpec.js index 6e5297b10..49a5ed881 100644 --- a/test/stateSpec.js +++ b/test/stateSpec.js @@ -901,7 +901,7 @@ describe('state', function () { it('should work for relative states', inject(function ($state, $q) { var options = { relative: $state.get('about') }; - + $state.transitionTo('about.person', { person: 'jane' }); $q.flush(); expect($state.is('.person', undefined, options)).toBe(true); diff --git a/test/transitionSpec.js b/test/transitionSpec.js index 94d89e69c..6b5fcc99c 100644 --- a/test/transitionSpec.js +++ b/test/transitionSpec.js @@ -314,7 +314,7 @@ describe('transition', function () { it("return value of type Transition should abort the transition with SUPERSEDED status", inject(function($transitions, $q) { var states = [], rejection, transition = makeTransition("A", "D"); transitionProvider.onEnter({ from: "*", to: "*" }, function($state$) { states.push($state$); }); - transitionProvider.onEnter({ from: "*", to: "C" }, function($state, $transition$) { + transitionProvider.onEnter({ from: "*", to: "C" }, function($state, $transition$) { return $transition$.redirect(targetState("B")); }); transition.promise.catch(function(err) { rejection = err; }); diff --git a/test/urlMatcherFactorySpec.js b/test/urlMatcherFactorySpec.js index dde60a947..d34938e30 100644 --- a/test/urlMatcherFactorySpec.js +++ b/test/urlMatcherFactorySpec.js @@ -267,9 +267,9 @@ describe("UrlMatcher", function () { expect(m.exec("/foo")).toEqual({ param1: undefined }); expect(m.exec("/foo", {})).toEqual({ param1: undefined }); - expect(m.exec("/foo", {param1: ""})).toEqual({ param1: undefined }); - expect(m.exec("/foo", {param1: "1"})).toEqual({ param1: "1" }); // auto unwrap single values - expect(m.exec("/foo", {param1: [ "1", "2" ]})).toEqual({ param1: [ "1", "2" ] }); + expect(m.exec("/foo", { param1: "" })).toEqual({ param1: undefined }); + expect(m.exec("/foo", { param1: "1" })).toEqual({ param1: "1" }); // auto unwrap single values + expect(m.exec("/foo", { param1: ["1", "2"]})).toEqual({ param1: ["1", "2"] }); $location.url("/foo"); expect(m.exec($location.path(), $location.search())).toEqual( { param1: undefined } ); @@ -948,7 +948,7 @@ describe("urlMatcherFactory", function () { describe(".$$filter", function() { it("should return a new ParamSet which is a subset of the current param set", function() { - var gpa = new ParamSet({grandparent: params.grandparent}); + var gpa = new ParamSet({ grandparent: params.grandparent }); var pa = gpa.$$new({ parent: params.parent }); var child = pa.$$new({ child: params.child }); diff --git a/test/urlRouterSpec.js b/test/urlRouterSpec.js index efaea230c..cc95853c2 100644 --- a/test/urlRouterSpec.js +++ b/test/urlRouterSpec.js @@ -223,7 +223,7 @@ describe("UrlRouter", function () { })); it('should handle the new html5Mode object config from Angular 1.3', inject(function($urlRouter) { - + $lp.html5Mode({ enabled: false }); From c87a0c82a608d23379b27c27f55b9f63926b2f53 Mon Sep 17 00:00:00 2001 From: Nate Abele Date: Sat, 3 Oct 2015 14:49:55 -0400 Subject: [PATCH 14/31] refactor(*): TS/ES6ification of State & UrlRouter --- src/state/state.ts | 111 +++++++++++++++++++++--------------------- src/url/urlRouter.ts | 23 ++++----- test/urlRouterSpec.js | 2 +- 3 files changed, 67 insertions(+), 69 deletions(-) diff --git a/src/state/state.ts b/src/state/state.ts index 9ce997a57..7648cc72b 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -33,69 +33,70 @@ import TransitionManager from "./hooks/transitionManager"; * * @returns {Object} Returns a new `State` object. */ -export function State(config?: IStateDeclaration) { - extend(this, config); -} +export class State { -/** - * @ngdoc function - * @name ui.router.state.type:State#is - * @methodOf ui.router.state.type:State - * - * @description - * Compares the identity of the state against the passed value, which is either an object - * reference to the actual `State` instance, the original definition object passed to - * `$stateProvider.state()`, or the fully-qualified name. - * - * @param {Object} ref Can be one of (a) a `State` instance, (b) an object that was passed - * into `$stateProvider.state()`, (c) the fully-qualified name of a state as a string. - * @returns {boolean} Returns `true` if `ref` matches the current `State` instance. - */ -State.prototype.is = function(ref) { - return this === ref || this.self === ref || this.fqn() === ref; -}; + public self: IStateDeclaration; + public parent: State; + public name: string; -/** - * @ngdoc function - * @name ui.router.state.type:State#fqn - * @methodOf ui.router.state.type:State - * - * @description - * Returns the fully-qualified name of the state, based on its current position in the tree. - * - * @returns {string} Returns a dot-separated name of the state. - */ -State.prototype.fqn = function() { - if (!this.parent || !(this.parent instanceof this.constructor)) { - return this.name; + constructor(config?: IStateDeclaration) { + extend(this, config); } - let name = this.parent.fqn(); - return name ? name + "." + this.name : this.name; -}; -/** - * @ngdoc function - * @name ui.router.state.type:State#root - * @methodOf ui.router.state.type:State - * - * @description - * Returns the root node of this state's tree. - * - * @returns {State} The root of this state's tree. - */ -State.prototype.root = function() { - let result = this; + /** + * @ngdoc function + * @name ui.router.state.type:State#is + * @methodOf ui.router.state.type:State + * + * @description + * Compares the identity of the state against the passed value, which is either an object + * reference to the actual `State` instance, the original definition object passed to + * `$stateProvider.state()`, or the fully-qualified name. + * + * @param {Object} ref Can be one of (a) a `State` instance, (b) an object that was passed + * into `$stateProvider.state()`, (c) the fully-qualified name of a state as a string. + * @returns {boolean} Returns `true` if `ref` matches the current `State` instance. + */ + is(ref: State|IStateDeclaration|string): boolean { + return this === ref || this.self === ref || this.fqn() === ref; + } - while (result.parent) { - result = result.parent; + /** + * @ngdoc function + * @name ui.router.state.type:State#fqn + * @methodOf ui.router.state.type:State + * + * @description + * Returns the fully-qualified name of the state, based on its current position in the tree. + * + * @returns {string} Returns a dot-separated name of the state. + */ + fqn(): string { + if (!this.parent || !(this.parent instanceof this.constructor)) { + return this.name; + } + let name = this.parent.fqn(); + return name ? name + "." + this.name : this.name; } - return result; -}; -State.prototype.toString = function() { - return this.fqn(); -}; + /** + * @ngdoc function + * @name ui.router.state.type:State#root + * @methodOf ui.router.state.type:State + * + * @description + * Returns the root node of this state's tree. + * + * @returns {State} The root of this state's tree. + */ + root(): State { + return this.parent && this.parent.root() || this; + } + toString() { + return this.fqn(); + } +} /** * @ngdoc object diff --git a/src/url/urlRouter.ts b/src/url/urlRouter.ts index 9a9cde864..7656bf617 100644 --- a/src/url/urlRouter.ts +++ b/src/url/urlRouter.ts @@ -1,6 +1,7 @@ /// import {isFunction, isString, isDefined, isArray, isObject, extend} from "../common/common"; import {IServiceProviderFactory} from "angular"; +import UrlMatcher from "./urlMatcher"; /** * @ngdoc object @@ -104,12 +105,8 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance */ this.otherwise = function (rule) { - if (isString(rule)) { - var redirect = rule; - rule = function () { return redirect; }; - } - else if (!isFunction(rule)) throw new Error("'rule' must be a function"); - otherwise = rule; + if (!isFunction(rule) && !isString(rule)) throw new Error("'rule' must be a string or function"); + otherwise = isString(rule) ? () => rule : rule; return this; }; @@ -172,7 +169,7 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { matcher: function (what, handler) { if (handlerIsString) { redirect = $urlMatcherFactory.compile(handler); - handler = ['$match', function ($match) { return redirect.format($match); }]; + handler = ['$match', redirect.format.bind(redirect)]; } return extend(function ($injector, $location) { return handleIfMatch($injector, handler, what.exec($location.path(), $location.search(), $location.hash())); @@ -185,7 +182,7 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { if (handlerIsString) { redirect = handler; - handler = ['$match', function ($match) { return interpolate(redirect, $match); }]; + handler = ['$match', ($match) => interpolate(redirect, $match)]; } return extend(function ($injector, $location) { return handleIfMatch($injector, handler, what.exec($location.path())); @@ -340,15 +337,15 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { * }); * */ - sync: function() { + sync() { update(); }, - listen: function() { + listen() { return listen(); }, - update: function(read) { + update(read) { if (read) { location = $location.url(); return; @@ -359,7 +356,7 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { $location.replace(); }, - push: function(urlMatcher, params, options) { + push(urlMatcher, params, options) { $location.url(urlMatcher.format(params || {})); if (options && options.replace) $location.replace(); }, @@ -389,7 +386,7 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { * * @returns {string} Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher` */ - href: function(urlMatcher, params, options) { + href(urlMatcher: UrlMatcher, params: any, options: any): string { if (!urlMatcher.validates(params)) return null; var isHtml5 = $locationProvider.html5Mode(); diff --git a/test/urlRouterSpec.js b/test/urlRouterSpec.js index cc95853c2..468a913bc 100644 --- a/test/urlRouterSpec.js +++ b/test/urlRouterSpec.js @@ -25,7 +25,7 @@ describe("UrlRouter", function () { it("should throw on non-function rules", function () { expect(function() { $urp.rule(null); }).toThrowError("'rule' must be a function"); - expect(function() { $urp.otherwise(null); }).toThrowError("'rule' must be a function"); + expect(function() { $urp.otherwise(null); }).toThrowError("'rule' must be a string or function"); }); it("should allow location changes to be deferred", inject(function ($urlRouter, $location, $rootScope) { From 33e38b188a1482c8620ad2b992d86231a71d4982 Mon Sep 17 00:00:00 2001 From: Nate Abele Date: Sat, 3 Oct 2015 15:04:51 -0400 Subject: [PATCH 15/31] refactor(*): Convert UrlMatchers to tree - UrlMatchers are now tree-based, can be picked up and re-appended - UrlMatchers are now immutable - Param objects are now factoried through static methods - Parameters in URLs now have guaranteed ordering --- src/common/common.ts | 4 + src/params/param.ts | 69 ++++++--- src/state/stateBuilder.ts | 24 +-- src/url/urlMatcher.ts | 273 +++++++++++++++++----------------- test/urlMatcherFactorySpec.js | 76 +++++----- test/urlRouterSpec.js | 4 +- 6 files changed, 243 insertions(+), 207 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index 0de6a391d..a84328dcb 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -391,6 +391,10 @@ export function padString(length: number, str: string) { return str; } +export function tail(array: any[]) { + return array.length && array[array.length - 1] || undefined; +} + /** * @ngdoc overview diff --git a/src/params/param.ts b/src/params/param.ts index 54f41c036..6ffae877d 100644 --- a/src/params/param.ts +++ b/src/params/param.ts @@ -4,10 +4,17 @@ import matcherConfig from "../url/urlMatcherConfig"; import paramTypes from "./paramTypes"; import Type from "./type"; +let hasOwn = Object.prototype.hasOwnProperty; +let isShorthand = cfg => ["value", "type", "squash", "array", "dynamic"].filter(hasOwn.bind(cfg || {})).length === 0; + +enum DefType { + PATH, SEARCH, CONFIG +} + export default class Param { id: string; type: Type; - location: string; + location: DefType; array: boolean; squash: (boolean|string); replace: any; @@ -15,39 +22,35 @@ export default class Param { dynamic: boolean; config: any; - constructor(id, type, config, location) { + constructor(id: string, type: Type, config: any, location: DefType) { config = unwrapShorthand(config); type = getType(config, type, location); var arrayMode = getArrayMode(); - type = arrayMode ? type.$asArray(arrayMode, location === "search") : type; + type = arrayMode ? type.$asArray(arrayMode, location === DefType.SEARCH) : type; var isOptional = config.value !== undefined; var dynamic = config.dynamic === true; var squash = getSquashPolicy(config, isOptional); var replace = getReplace(config, arrayMode, isOptional, squash); function unwrapShorthand(config) { - var configKeys = ["value", "type", "squash", "array", "dynamic"].filter(function (key) { - return (config || {}).hasOwnProperty(key); + config = isShorthand(config) && { value: config } || config; + + return extend(config, { + $$fn: isInjectable(config.value) ? config.value : () => config.value }); - var isShorthand = configKeys.length === 0; - if (isShorthand) config = {value: config}; - config.$$fn = isInjectable(config.value) ? config.value : function () { - return config.value; - }; - return config; } function getType(config, urlType, location) { if (config.type && urlType && urlType.name !== 'string') throw new Error(`Param '${id}' has two type configurations.`); if (config.type && urlType && urlType.name === 'string' && paramTypes.type(config.type)) return paramTypes.type(config.type); if (urlType) return urlType; - if (!config.type) return (location === "config" ? paramTypes.type("any") : paramTypes.type("string")); + if (!config.type) return (location === DefType.CONFIG ? paramTypes.type("any") : paramTypes.type("string")); return config.type instanceof Type ? config.type : paramTypes.type(config.type); } // array config: param name (param[]) overrides default settings. explicit config overrides param name. function getArrayMode() { - var arrayDefaults = { array: (location === "search" ? "auto" : false) }; + var arrayDefaults = { array: (location === DefType.SEARCH ? "auto" : false) }; var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {}; return extend(arrayDefaults, arrayParamNomenclature, config).array; } @@ -60,7 +63,7 @@ export default class Param { if (!isOptional || squash === false) return false; if (!isDefined(squash) || squash == null) return matcherConfig.defaultSquashPolicy(); if (squash === true || isString(squash)) return squash; - throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string"); + throw new Error(`Invalid squash policy: '${squash}'. Valid policies: false, true, or arbitrary string`); } function getReplace(config, arrayMode, isOptional, squash) { @@ -69,7 +72,7 @@ export default class Param { {from: null, to: (isOptional || arrayMode ? undefined : "")} ]; replace = isArray(config.replace) ? config.replace : []; - if (isString(squash)) replace.push({from: squash, to: undefined}); + if (isString(squash)) replace.push({ from: squash, to: undefined }); configuredKeys = map(replace, prop("from")); return filter(defaultPolicy, item => configuredKeys.indexOf(item.from) === -1).concat(replace); } @@ -77,7 +80,7 @@ export default class Param { extend(this, {id, type, location, squash, replace, isOptional, dynamic, config, array: arrayMode}); } - isDefaultValue(value: any) { + isDefaultValue(value: any): boolean { return this.isOptional && this.type.equals(this.value(), value); } @@ -85,7 +88,7 @@ export default class Param { * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the * default value, which may be the result of an injectable function. */ - value(value?: any) { + value(value?: any): any { /** * [Internal] Get the default value of a parameter, which may be an injectable function. */ @@ -106,8 +109,38 @@ export default class Param { return !isDefined(value) ? $$getDefaultValue() : this.type.$normalize(value); } + isSearch(): boolean { + return this.location === DefType.SEARCH; + } + + validates(value: any): boolean { + // There was no parameter value, but the param is optional + if ((!isDefined(value) || value === null) && this.isOptional) return true; + + // The value was not of the correct Type, and could not be decoded to the correct Type + const normalized = this.type.$normalize(value); + if (!this.type.is(normalized)) return false; + + // The value was of the correct type, but when encoded, did not match the Type's regexp + const encoded = this.type.encode(normalized); + if (isString(encoded) && !this.type.pattern.exec( encoded)) return false; + + return true; + } + toString() { return `{Param:${this.id} ${this.type} squash: '${this.squash}' optional: ${this.isOptional}}`; } -} + static fromConfig(id: string, type: Type, config: any): Param { + return new Param(id, type, config, DefType.CONFIG); + } + + static fromPath(id: string, type: Type, config: any): Param { + return new Param(id, type, config, DefType.PATH); + } + + static fromSearch(id: string, type: Type, config: any): Param { + return new Param(id, type, config, DefType.SEARCH); + } +} \ No newline at end of file diff --git a/src/state/stateBuilder.ts b/src/state/stateBuilder.ts index ff52f3cc6..eeff5b9b3 100644 --- a/src/state/stateBuilder.ts +++ b/src/state/stateBuilder.ts @@ -2,6 +2,12 @@ import {noop, extend, pick, isArray, isDefined, isFunction, isString, forEach} f import ParamSet from "../params/paramSet"; import Param from "../params/param"; +const parseUrl = (url: string): any => { + if (!isString(url)) return false; + var root = url.charAt(0) === '^'; + return { val: root ? url.substring(1) : url, root }; +}; + // Builds state properties from definition passed to StateQueueManager.register() export default function StateBuilder(root, matcher, $urlMatcherFactoryProvider) { @@ -20,15 +26,12 @@ export default function StateBuilder(root, matcher, $urlMatcherFactoryProvider) // Build a URLMatcher if necessary, either via a relative or absolute URL url: function(state) { - let url = state.url, config = { params: state.params || {} }; - let parent = state.parent; + const parsed = parseUrl(state.url), parent = state.parent, config = { params: state.params || {} }; + const url = parsed ? $urlMatcherFactoryProvider.compile(parsed.val, config) : state.url; - if (isString(url)) { - if (url.charAt(0) === '^') return $urlMatcherFactoryProvider.compile(url.substring(1), config); - return ((parent && parent.navigable) || root()).url.concat(url, config); - } - if (!url || $urlMatcherFactoryProvider.isMatcher(url)) return url; - throw new Error(`Invalid url '${url}' in state '${state}'`); + if (!url) return; + if (!$urlMatcherFactoryProvider.isMatcher(url)) throw new Error(`Invalid url '${url}' in state '${state}'`); + return (parsed && parsed.root) ? url : ((parent && parent.navigable) || root()).url.append(url); }, // Keep track of the closest ancestor state that has a URL (i.e. is navigable) @@ -40,10 +43,11 @@ export default function StateBuilder(root, matcher, $urlMatcherFactoryProvider) ownParams: function(state) { let params = state.url && state.url.params.$$own() || new ParamSet(); forEach(state.params || {}, function(config, id) { - if (!params[id]) params[id] = new Param(id, null, config, "config"); + if (!params[id]) params[id] = Param.fromConfig(id, null, config); }); if (state.reloadOnSearch === false) { - forEach(params, function(param) { if (param && param.location === 'search') param.dynamic = true; }); + // @TODO: Fix me + forEach(params, function(param) { if (param && param.isSearch()) param.dynamic = true; }); } return params; }, diff --git a/src/url/urlMatcher.ts b/src/url/urlMatcher.ts index 6a02fb1ca..af4be53fc 100644 --- a/src/url/urlMatcher.ts +++ b/src/url/urlMatcher.ts @@ -1,5 +1,4 @@ -import {map, extend, inherit, isDefined, isObject, isArray, isString} from "../common/common"; -import matcherConfig from "./urlMatcherConfig" +import {map, prop, propEq, defaults, extend, inherit, isDefined, isObject, isArray, isString, invoke, unnest, tail, forEach, find, curry} from "../common/common"; import paramTypes from "../params/paramTypes" import ParamSet from "../params/paramSet" import Param from "../params/param" @@ -8,18 +7,20 @@ interface params { $$validates: (params: string) => Array; } -function quoteRegExp(string, pattern?: any, squash?: any, optional?: any) { - var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); - if (!pattern) return result; +function quoteRegExp(string: any, param?: any) { + var surroundPattern = ['', ''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + if (!param) return result; - switch (squash) { - case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break; + switch (param.squash) { + case false: surroundPattern = ['(', ')' + (param.isOptional ? '?' : '')]; break; case true: surroundPattern = ['?(', ')?']; break; - default: surroundPattern = [`(${squash}|`, ')?']; break; + default: surroundPattern = [`(${param.squash}|`, ')?']; break; } - return result + surroundPattern[0] + pattern + surroundPattern[1]; + return result + surroundPattern[0] + param.type.pattern.source + surroundPattern[1]; } +const memoizeTo = (obj, prop, fn) => obj[prop] = obj[prop] || fn(); + /** * @ngdoc object * @name ui.router.util.type:UrlMatcher @@ -63,10 +64,7 @@ function quoteRegExp(string, pattern?: any, squash?: any, optional?: any) { * in the built-in `date` Type matches `2014-11-12`) and provides a Date object in $stateParams.start * * @param {string} pattern The pattern to compile into a matcher. - * @param {Object} config A configuration object hash: - * @param {Object=} parentMatcher Used to concatenate the pattern/config onto - * an existing UrlMatcher - * + * @param {Object} config A configuration object hash * * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`. * * `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`. * @@ -74,29 +72,28 @@ function quoteRegExp(string, pattern?: any, squash?: any, optional?: any) { * URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns * non-null) will start with this prefix. * - * @property {string} source The pattern that was passed into the constructor - * - * @property {string} sourcePath The path portion of the source property - * - * @property {string} sourceSearch The search portion of the source property - * - * @property {string} regex The constructed regex that will be used to match against the url when - * it is time to determine which url will match. + * @property {string} pattern The pattern that was passed into the constructor * * @returns {Object} New `UrlMatcher` object */ export default class UrlMatcher { - params: ParamSet; - prefix: string; - regexp: RegExp; - segments: Array; - source: string; - sourceSearch: string; - sourcePath: string; - $$paramNames: Array; - - constructor(pattern, config, parentMatcher?: any) { - config = extend({ params: {} }, isObject(config) ? config : {}); + + static nameValidator: RegExp = /^\w+(-+\w+)*(?:\[\])?$/; + + private _cache: { path: UrlMatcher[], pattern?: RegExp } = { path: [], pattern: null }; + private _children: UrlMatcher[] = []; + private _params: Array = []; + private _segments: Array = []; + private _compiled: Array = []; + + public prefix: string; + + constructor(public pattern: string, public config: any) { + this.config = defaults(this.config, { + params: {}, + strict: false, + caseInsensitive: false + }); // Find all placeholders and create a compiled pattern, using either classic or curly syntax: // '*' name @@ -113,35 +110,28 @@ export default class UrlMatcher { // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, searchPlaceholder = /([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, - compiled = '^', last = 0, m, - segments = this.segments = [], - parentParams = parentMatcher ? parentMatcher.params : {}, - params = this.params = parentMatcher ? parentMatcher.params.$$new() : new ParamSet(), - paramNames = []; - - function addParameter(id, type, config, location) { - paramNames.push(id); - if (parentParams[id]) return parentParams[id]; - if (!/^\w+(-+\w+)*(?:\[\])?$/.test(id)) throw new Error(`Invalid parameter name '${id}' in pattern '${pattern}'`); - if (params[id]) throw new Error(`Duplicate parameter name '${id}' in pattern '${pattern}'`); - params[id] = new Param(id, type, config, location); - return params[id]; - } + last = 0, m, patterns = []; - this.source = pattern; + const checkParamErrors = (id) => { + if (!UrlMatcher.nameValidator.test(id)) throw new Error(`Invalid parameter name '${id}' in pattern '${pattern}'`); + if (find(this._params, propEq('id', id))) throw new Error(`Duplicate parameter name '${id}' in pattern '${pattern}'`); + }; // Split into static segments separated by path parameter placeholders. // The number of segments is always 1 more than the number of parameters. - function matchDetails(m, isSearch) { - var id, regexp, segment, type, cfg, arrayMode; - id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null - cfg = config.params[id]; - segment = pattern.substring(last, m.index); - regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); - type = paramTypes.type(regexp || "string") || inherit(paramTypes.type("string"), { - pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) - }); - return {id, regexp, segment, type, cfg}; + const matchDetails = (m, isSearch) => { + // IE[78] returns '' for unmatched groups instead of null + var id = m[2] || m[3], regexp = isSearch ? m[4] : m[4] || (m[1] === '*' ? '.*' : null); + + return { + id, + regexp, + cfg: this.config.params[id], + segment: pattern.substring(last, m.index), + type: paramTypes.type(regexp || "string") || inherit(paramTypes.type("string"), { + pattern: new RegExp(regexp, this.config.caseInsensitive ? 'i' : undefined) + }) + }; } var p, param, segment; @@ -150,9 +140,10 @@ export default class UrlMatcher { p = matchDetails(m, false); if (p.segment.indexOf('?') >= 0) break; // we're into the search part - param = addParameter(p.id, p.type, p.cfg, "path"); - compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash, param.isOptional); - segments.push(p.segment); + checkParamErrors(p.id); + this._params.push(Param.fromPath(p.id, p.type, p.cfg)); + this._segments.push(p.segment); + patterns.push([p.segment, tail(this._params)]); last = placeholder.lastIndex; } segment = pattern.substring(last); @@ -161,68 +152,58 @@ export default class UrlMatcher { var i = segment.indexOf('?'); if (i >= 0) { - var search = this.sourceSearch = segment.substring(i); + var search = segment.substring(i); segment = segment.substring(0, i); - this.sourcePath = pattern.substring(0, last + i); if (search.length > 0) { last = 0; + while ((m = searchPlaceholder.exec(search))) { p = matchDetails(m, true); - param = addParameter(p.id, p.type, p.cfg, "search"); + checkParamErrors(p.id); + this._params.push(Param.fromSearch(p.id, p.type, p.cfg)); last = placeholder.lastIndex; // check if ?& } } - } else { - this.sourcePath = pattern; - this.sourceSearch = ''; } - compiled += quoteRegExp(segment) + (config.strict === false ? '\/?' : '') + '$'; - segments.push(segment); + this._segments.push(segment); + + extend(this, { + _compiled: patterns.map(pattern => quoteRegExp.apply(null, pattern)).concat(quoteRegExp(segment)), + prefix: this._segments[0] + }); - this.regexp = new RegExp(compiled, config.caseInsensitive ? 'i' : undefined); - this.prefix = segments[0]; - this.$$paramNames = paramNames; + Object.freeze(this); } /** * @ngdoc function - * @name ui.router.util.type:UrlMatcher#concat + * @name ui.router.util.type:UrlMatcher#append * @methodOf ui.router.util.type:UrlMatcher * * @description - * Returns a new matcher for a pattern constructed by appending the path part and adding the - * search parameters of the specified pattern to this pattern. The current pattern is not - * modified. This can be understood as creating a pattern for URLs that are relative to (or - * suffixes of) the current pattern. + * @TODO * * @example - * The following two matchers are equivalent: - *
-   * new UrlMatcher('/user/{id}?q').concat('/details?date');
-   * new UrlMatcher('/user/{id}/details?q&date');
-   * 
+ * @TODO * - * @param {string} pattern The pattern to append. - * @param {Object} config An object hash of the configuration for the matcher. - * @returns {UrlMatcher} A matcher for the concatenated pattern. + * @param {UrlMatcher} url A `UrlMatcher` instance to append as a child of the current `UrlMatcher`. */ - concat(pattern, config) { - // Because order of search parameters is irrelevant, we can add our own search - // parameters to the end of the new pattern. Parse the new pattern by itself - // and then join the bits together, but it's much easier to do this on a string level. - var defaultConfig = { - caseInsensitive: matcherConfig.caseInsensitive(), - strict: matcherConfig.strictMode(), - squash: matcherConfig.defaultSquashPolicy() - }; - return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, extend(defaultConfig, config), this); + append(url: UrlMatcher): UrlMatcher { + this._children.push(url); + forEach(url._cache, (val, key) => url._cache[key] = isArray(val) ? [] : null); + url._cache.path = this._cache.path.concat(this); + return url; + } + + isRoot(): boolean { + return this._cache.path.length === 0; } - toString() { - return this.source; + toString(): string { + return this.pattern; } /** @@ -234,7 +215,7 @@ export default class UrlMatcher { * Tests the specified path against this matcher, and returns an object containing the captured * parameter values, or null if the path does not match. The returned object contains the values * of any search parameters that are mentioned in the pattern, but their value may be null if - * they are not present in `searchParams`. This means that search parameters are always treated + * they are not present in `search`. This means that search parameters are always treated * as optional. * * @example @@ -246,19 +227,33 @@ export default class UrlMatcher { * * * @param {string} path The URL path to match, e.g. `$location.path()`. - * @param {Object} searchParams URL search parameters, e.g. `$location.search()`. + * @param {Object} search URL search parameters, e.g. `$location.search()`. + * @param {Object} options * @returns {Object} The captured parameter values. */ - exec(path, searchParams, hash) { - var m = this.regexp.exec(path); - if (!m) return null; - searchParams = searchParams || {}; - - var paramNames = this.parameters(), nTotal = paramNames.length, - nPath = this.segments.length - 1, - values = {}, i, j, cfg, paramName; - - if (nPath !== m.length - 1) throw new Error(`Unbalanced capture group in route '${this.source}'`); + exec(path: string, search: any = {}, options: any = {}) { + var match = memoizeTo(this._cache, 'pattern', () => { + return new RegExp([ + '^', + unnest(this._cache.path.concat(this).map(prop('_compiled'))).join(''), + this.config.strict === false ? '\/?' : '', + '$' + ].join(''), this.config.caseInsensitive ? 'i' : undefined); + }).exec(path); + + if (!match) return null; + + options = defaults(options, { isolate: false }); + const hash: string = search['#'] ? search['#'] : undefined; + delete search['#']; + + var allParams: Param[] = this.parameters().map(name => this.parameter(name)), + pathParams: Param[] = allParams.filter(param => !param.isSearch()), + searchParams: Param[] = allParams.filter(param => param.isSearch()), + nPath = this._segments.length - 1, + values = {}; + + if (nPath !== match.length - 1) throw new Error(`Unbalanced capture group in route '${this.pattern}'`); function decodePathArray(string: string) { const reverseString = (str: string) => str.split("").reverse().join(""); @@ -269,21 +264,20 @@ export default class UrlMatcher { return map(allReversed, unquoteDashes).reverse(); } - for (i = 0; i < nPath; i++) { - paramName = paramNames[i]; - var param = this.params[paramName]; - var paramVal: (any|any[]) = m[i + 1]; + for (var i = 0; i < nPath; i++) { + var param: Param = pathParams[i]; + var value: (any|any[]) = match[i + 1]; + // if the param value matches a pre-replace pair, replace the value before decoding. - for (j = 0; j < param.replace; j++) { - if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; + for (var j = 0; j < param.replace; j++) { + if (param.replace[j].from === value) value = param.replace[j].to; } - if (paramVal && param.array === true) paramVal = decodePathArray(paramVal); - values[paramName] = param.value(paramVal); - } - for (/**/; i < nTotal; i++) { - paramName = paramNames[i]; - values[paramName] = this.params[paramName].value(searchParams[paramName]); + if (value && param.array === true) value = decodePathArray(value); + values[param.id] = param.value(value); } + forEach(searchParams, param => { + values[param.id] = param.value(search[param.id]) + }); if (hash) values["#"] = hash; @@ -296,19 +290,23 @@ export default class UrlMatcher { * @methodOf ui.router.util.type:UrlMatcher * * @description - * Returns the names of all path and search parameters of this pattern in an unspecified order. + * Returns the names of all path and search parameters of this pattern in order of appearance. * * @returns {Array.} An array of parameter names. Must be treated as read-only. If the * pattern has no parameters, an empty array is returned. */ - parameters(param?: string) { - if (!isDefined(param)) return this.$$paramNames; - return this.params[param] || null; + parameters(): Array { + return unnest(this._cache.path.concat(this).map(prop('_params'))).map(prop('id')) + } + + parameter(id: string): Param { + var parent = tail(this._cache.path); + return find(this._params, propEq('id', id)) || (parent && parent.parameter(id)) || null; } /** * @ngdoc function - * @name ui.router.util.type:UrlMatcher#validate + * @name ui.router.util.type:UrlMatcher#validates * @methodOf ui.router.util.type:UrlMatcher * * @description @@ -318,8 +316,14 @@ export default class UrlMatcher { * @param {Object} params The object hash of parameters to validate. * @returns {boolean} Returns `true` if `params` validates, otherwise `false`. */ - validates(params) { - return this.params.$$validates(params); + validates(params): boolean { + var result = true; + + map(params, (val, key) => { + var param = this.parameter( key); + if (param) result = result && param.validates(val); + }); + return result; } /** @@ -342,12 +346,7 @@ export default class UrlMatcher { * @returns {string} the formatted URL (path and optionally search part). */ format(values = {}) { - const url = { - params: this.parameters(), - paramSet: this.params, - nPath: this.segments.length - 1 - }; - var i, search = false, result = this.segments[0]; + var i, search = false, result = this._segments[0]; if (!this.validates(values)) return null; @@ -355,9 +354,9 @@ export default class UrlMatcher { return encodeURIComponent(str).replace(/-/g, c => `%5C%${c.charCodeAt(0).toString(16).toUpperCase()}`); } - url.params.map((name, i) => { - var isPathParam = i < url.nPath; - var param: Param = url.paramSet[name], value = param.value(values[name]); + this.parameters().map((name, i) => { + var isPathParam = i < this._segments.length - 1; + var param: Param = this.parameter(name), value = param.value(values[name]); var isDefaultValue = param.isDefaultValue(value); var squash = isDefaultValue ? param.squash : false; var encoded = param.type.encode(value); @@ -380,7 +379,7 @@ export default class UrlMatcher { if (isArray(encoded)) return map( encoded, encodeDashes).join("-") + segment; if (param.type.raw) return encoded + segment; return encodeURIComponent( encoded) + segment; - })(this.segments[i + 1], result); + })(this._segments[i + 1], result); }); if (values["#"]) result += "#" + values["#"]; diff --git a/test/urlMatcherFactorySpec.js b/test/urlMatcherFactorySpec.js index d34938e30..14f425b03 100644 --- a/test/urlMatcherFactorySpec.js +++ b/test/urlMatcherFactorySpec.js @@ -50,7 +50,7 @@ describe("UrlMatcher", function () { var m = new UrlMatcher("/"); expect(provider.isMatcher(m)).toBe(true); - m.validates = null; + m = angular.extend({}, m, { validates: null }); expect(provider.isMatcher(m)).toBe(false); }); }); @@ -64,7 +64,7 @@ describe("UrlMatcher", function () { }); it("should match against the entire path", function () { - var matcher = new UrlMatcher('/hello/world'); + var matcher = new UrlMatcher('/hello/world', { strict: true }); expect(matcher.exec('/hello/world/')).toBeNull(); expect(matcher.exec('/hello/world/suffix')).toBeNull(); }); @@ -89,13 +89,7 @@ describe("UrlMatcher", function () { describe("snake-case parameters", function() { it("should match if properly formatted", function() { var matcher = new UrlMatcher('/users/?from&to&snake-case&snake-case-triple'); - var params = matcher.parameters(); - - expect(params.length).toBe(4); - expect(params).toContain('from'); - expect(params).toContain('to'); - expect(params).toContain('snake-case'); - expect(params).toContain('snake-case-triple'); + expect(matcher.parameters()).toEqual(['from', 'to', 'snake-case', 'snake-case-triple']); }); it("should not match if invalid", function() { @@ -109,7 +103,7 @@ describe("UrlMatcher", function () { describe(".exec()", function() { it("should capture parameter values", function () { - var m = new UrlMatcher('/users/:id/details/{type}/{repeat:[0-9]+}?from&to'); + var m = new UrlMatcher('/users/:id/details/{type}/{repeat:[0-9]+}?from&to', { strict: false }); expect(m.exec('/users/123/details//0', {})).toEqualData({ id:'123', type:'', repeat:'0'}); }); @@ -178,34 +172,28 @@ describe("UrlMatcher", function () { }); it("encodes URL parameters with hashes", function () { - var m = new UrlMatcher('/users/:id#:section'), - params = { id: 'bob', section: 'contact-details' }; - expect(m.format(params)).toEqual('/users/bob#contact-details'); + var m = new UrlMatcher('/users/:id#:section'); + expect(m.format({ id: 'bob', section: 'contact-details' })).toEqual('/users/bob#contact-details'); }); }); - describe(".concat()", function() { - it("should concatenate matchers", function () { - var matcher = new UrlMatcher('/users/:id/details/{type}?from').concat('/{repeat:[0-9]+}?to'); + describe(".append()", function() { + it("should append matchers", function () { + var matcher = new UrlMatcher('/users/:id/details/{type}?from').append(new UrlMatcher('/{repeat:[0-9]+}?to')); var params = matcher.parameters(); - expect(params.length).toBe(5); - expect(params).toContain('id'); - expect(params).toContain('type'); - expect(params).toContain('repeat'); - expect(params).toContain('from'); - expect(params).toContain('to'); + expect(params).toEqual(['id', 'type', 'from', 'repeat', 'to']); }); it("should return a new matcher", function () { var base = new UrlMatcher('/users/:id/details/{type}?from'); - var matcher = base.concat('/{repeat:[0-9]+}?to'); + var matcher = base.append(new UrlMatcher('/{repeat:[0-9]+}?to')); expect(matcher).not.toBe(base); }); it("should respect $urlMatcherFactoryProvider.strictMode", function() { var m = new UrlMatcher('/'); provider.strictMode(false); - m = m.concat("foo"); + m = m.append(provider.compile("foo")); expect(m.exec("/foo")).toEqual({}); expect(m.exec("/foo/")).toEqual({}) }); @@ -213,7 +201,7 @@ describe("UrlMatcher", function () { it("should respect $urlMatcherFactoryProvider.caseInsensitive", function() { var m = new UrlMatcher('/'); provider.caseInsensitive(true); - m = m.concat("foo"); + m = m.append(provider.compile("foo")); expect(m.exec("/foo")).toEqual({}); expect(m.exec("/FOO")).toEqual({}); }); @@ -221,34 +209,40 @@ describe("UrlMatcher", function () { it("should respect $urlMatcherFactoryProvider.caseInsensitive when validating regex params", function() { var m = new UrlMatcher('/'); provider.caseInsensitive(true); - m = m.concat("foo/{param:bar}"); - expect(m.validates({param:'BAR'})).toEqual(true); + m = m.append(provider.compile("foo/{param:bar}")); + expect(m.validates({ param: 'BAR' })).toEqual(true); }); it("should generate/match params in the proper order", function() { var m = new UrlMatcher('/foo?queryparam'); - m = m.concat("/bar/:pathparam"); - expect(m.exec("/foo/bar/pathval", { queryparam: "queryval" })).toEqual({ pathparam: "pathval", queryparam: "queryval"}); + m = m.append(new UrlMatcher("/bar/:pathparam")); + expect(m.exec("/foo/bar/pathval", { queryparam: "queryval" })).toEqual({ + pathparam: "pathval", + queryparam: "queryval" + }); }); }); describe("multivalue-query-parameters", function() { it("should handle .is() for an array of values", inject(function($location) { - var m = new UrlMatcher('/foo?{param1:int}'); - expect(m.params.param1.type.is([1, 2, 3])).toBe(true); - expect(m.params.param1.type.is([1, "2", 3])).toBe(false); + var m = new UrlMatcher('/foo?{param1:int}'), param = m.parameter('param1'); + expect(param.type.is([1, 2, 3])).toBe(true); + expect(param.type.is([1, "2", 3])).toBe(false); })); it("should handle .equals() for two arrays of values", inject(function($location) { - var m = new UrlMatcher('/foo?{param1:int}&{param2:date}'); - expect(m.params.param1.type.equals([1, 2, 3], [1, 2, 3])).toBe(true); - expect(m.params.param1.type.equals([1, 2, 3], [1, 2 ])).toBe(false); - expect(m.params.param2.type.equals( + var m = new UrlMatcher('/foo?{param1:int}&{param2:date}'), + param1 = m.parameter('param1'), + param2 = m.parameter('param2'); + + expect(param1.type.equals([1, 2, 3], [1, 2, 3])).toBe(true); + expect(param1.type.equals([1, 2, 3], [1, 2 ])).toBe(false); + expect(param2.type.equals( [new Date(2014, 11, 15), new Date(2014, 10, 15)], [new Date(2014, 11, 15), new Date(2014, 10, 15)]) ).toBe(true); - expect(m.params.param2.type.equals( + expect(param2.type.equals( [new Date(2014, 11, 15), new Date(2014, 9, 15)], [new Date(2014, 11, 15), new Date(2014, 10, 15)]) ).toBe(false); @@ -960,23 +954,23 @@ describe("urlMatcherFactory", function () { xdescribe("parameter isolation", function() { it("should allow parameters of the same name in different segments", function() { - var m = new UrlMatcher('/users/:id').concat('/photos/:id'); + var m = new UrlMatcher('/users/:id').append(new UrlMatcher('/photos/:id')); expect(m.exec('/users/11/photos/38', {}, { isolate: true })).toEqual([{ id: '11' }, { id: '38' }]); }); it("should prioritize the last child when non-isolated", function() { - var m = new UrlMatcher('/users/:id').concat('/photos/:id'); + var m = new UrlMatcher('/users/:id').append(new UrlMatcher('/photos/:id')); expect(m.exec('/users/11/photos/38')).toEqual({ id: '38' }); }); it("should copy search parameter values to all matching segments", function() { - var m = new UrlMatcher('/users/:id?from').concat('/photos/:id?from'); + var m = new UrlMatcher('/users/:id?from').append(new UrlMatcher('/photos/:id?from')); var result = m.exec('/users/11/photos/38', { from: "bob" }, { isolate: true }); expect(result).toEqual([{ from: "bob", id: "11" }, { from: "bob", id: "38" }]); }); it("should pair empty objects with static segments", function() { - var m = new UrlMatcher('/users/:id').concat('/foo').concat('/photos/:id'); + var m = new UrlMatcher('/users/:id').append(new UrlMatcher('/foo')).append(new UrlMatcher('/photos/:id')); var result = m.exec('/users/11/foo/photos/38', {}, { isolate: true }); expect(result).toEqual([{ id: '11' }, {}, { id: '38' }]); }); diff --git a/test/urlRouterSpec.js b/test/urlRouterSpec.js index 468a913bc..9f39c80f8 100644 --- a/test/urlRouterSpec.js +++ b/test/urlRouterSpec.js @@ -106,9 +106,11 @@ describe("UrlRouter", function () { var custom = { url: { exec: function() {}, + isRoot: function() {}, format: function() {}, - concat: function() {}, + append: function() {}, validates: function() {}, + parameter: function() {}, parameters: function() {} }, handler: function() {} From cc76df22b2beef3d3314f2ddf73e424b6e6bd6c5 Mon Sep 17 00:00:00 2001 From: Nate Abele Date: Fri, 16 Oct 2015 19:01:35 -0400 Subject: [PATCH 16/31] refactor(*): consolidate path handling --- src/common/common.ts | 5 +- src/params/interface.ts | 4 +- src/params/module.ts | 6 -- src/params/param.ts | 19 +++- src/params/paramSet.ts | 75 ---------------- src/params/paramValues.ts | 31 ------- src/path/interface.ts | 34 ++----- src/path/module.ts | 4 +- src/path/node.ts | 69 ++++++++++++++ src/path/path.ts | 106 ---------------------- src/path/pathFactory.ts | 129 +++++++++++---------------- src/resolve/resolvable.ts | 10 +-- src/resolve/resolveContext.ts | 75 ++++++++++------ src/resolve/resolveInjector.ts | 4 +- src/state/hooks/resolveHooks.ts | 6 +- src/state/hooks/transitionManager.ts | 3 +- src/state/hooks/viewHooks.ts | 6 +- src/state/interface.ts | 36 ++------ src/state/state.ts | 106 +++++++++++++++------- src/state/stateBuilder.ts | 38 ++++---- src/state/stateMatcher.ts | 9 +- src/state/stateQueueManager.ts | 2 + src/state/targetState.ts | 8 +- src/transition/hookBuilder.ts | 32 +++---- src/transition/hookRegistry.ts | 6 +- src/transition/interface.ts | 30 +++---- src/transition/transition.ts | 64 +++++++------ src/transition/transitionHook.ts | 5 +- src/transition/transitionService.ts | 4 +- src/url/urlMatcher.ts | 66 ++++++++------ src/url/urlMatcherFactory.ts | 5 +- test/resolveSpec.ts | 28 +++--- test/urlMatcherFactorySpec.js | 102 +-------------------- test/viewSpec.ts | 10 +-- 34 files changed, 456 insertions(+), 681 deletions(-) delete mode 100644 src/params/paramSet.ts delete mode 100644 src/params/paramValues.ts create mode 100644 src/path/node.ts delete mode 100644 src/path/path.ts diff --git a/src/common/common.ts b/src/common/common.ts index a84328dcb..83123fc0d 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -391,8 +391,9 @@ export function padString(length: number, str: string) { return str; } -export function tail(array: any[]) { - return array.length && array[array.length - 1] || undefined; +export function tail(collection: T[]): T; +export function tail(collection: any[]): any { + return collection.length && collection[collection.length - 1] || undefined; } diff --git a/src/params/interface.ts b/src/params/interface.ts index 14502226b..6049348d9 100644 --- a/src/params/interface.ts +++ b/src/params/interface.ts @@ -1,6 +1,4 @@ -import ParamValues from "./paramValues"; - export interface IRawParams { [key: string]: any } -export type IParamsOrArray = (IRawParams|IRawParams[]|ParamValues); \ No newline at end of file +export type IParamsOrArray = (IRawParams|IRawParams[]); \ No newline at end of file diff --git a/src/params/module.ts b/src/params/module.ts index ef2928dc5..2970a3631 100644 --- a/src/params/module.ts +++ b/src/params/module.ts @@ -1,14 +1,8 @@ import * as param from "./param"; export {param}; -import * as paramSet from "./paramSet"; -export {paramSet}; - import * as paramTypes from "./paramTypes"; export {paramTypes}; -import * as paramValues from "./paramValues"; -export {paramValues}; - import * as type from "./type"; export {type}; diff --git a/src/params/param.ts b/src/params/param.ts index 6ffae877d..50d560dbd 100644 --- a/src/params/param.ts +++ b/src/params/param.ts @@ -1,4 +1,5 @@ -import {isInjectable, extend, isDefined, isString, isArray, filter, map, pick, prop, propEq, curry} from "../common/common"; +import {isInjectable, extend, isDefined, isString, isArray, filter, map, pick, prop, propEq, curry, applyPairs} from "../common/common"; +import {IRawParams} from "../params/interface"; import {runtime} from "../common/angular1"; import matcherConfig from "../url/urlMatcherConfig"; import paramTypes from "./paramTypes"; @@ -143,4 +144,20 @@ export default class Param { static fromSearch(id: string, type: Type, config: any): Param { return new Param(id, type, config, DefType.SEARCH); } + + static values(params: Param[], values): IRawParams { + values = values || {}; + return params.map(param => [param.id, param.value(values[param.id])]).reduce(applyPairs, {}); + } + + static equals(params: Param[], values1, values2): boolean { + values1 = values1 || {}; + values2 = values2 || {}; + return params.map(param => param.type.equals(values1[param.id], values2[param.id])).indexOf(false) === -1; + } + + static validates(params: Param[], values): boolean { + values = values || {}; + return params.map(param => param.validates(values[param.id])).indexOf(false) === -1; + } } \ No newline at end of file diff --git a/src/params/paramSet.ts b/src/params/paramSet.ts deleted file mode 100644 index 9b6a58cf5..000000000 --- a/src/params/paramSet.ts +++ /dev/null @@ -1,75 +0,0 @@ -import {extend, inherit, forEach, isString} from "../common/common"; - -import {IRawParams} from "../params/interface" - -export default class ParamSet { - constructor(params?: any) { - extend(this, params || {}); - } - - $$new(params) { - return inherit(inherit(this, { $$parent: () => this }), params); - } - - $$own() { - return extend(new ParamSet(), this); - } - - $$keys() { - var keys = [], chain = [], parent = this, ignore = Object.keys(ParamSet.prototype); - while (parent) { chain.push(parent); parent = parent.$$parent(); } - chain.reverse(); - forEach(chain, function(paramset) { - forEach(Object.keys(paramset), function(key) { - if (keys.indexOf(key) === -1 && ignore.indexOf(key) === -1) keys.push(key); - }); - }); - return keys; - } - - $$values(paramValues): IRawParams { - var values = {}; - forEach(this.$$keys(), (key: string) => { - values[key] = this[key].value(paramValues && paramValues[key]); - }); - return values; - } - - $$equals(paramValues1, paramValues2) { - var equal = true, self = this; - forEach(self.$$keys(), function(key) { - var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key]; - if (!self[key].type.equals(left, right)) equal = false; - }); - return equal; - } - - $$validates(paramValues) { - var keys = this.$$keys(), i, param, rawVal, normalized, encoded; - paramValues = paramValues || {}; - for (i = 0; i < keys.length; i++) { - param = this[keys[i]]; - rawVal = paramValues[keys[i]]; - if ((rawVal === undefined || rawVal === null) && param.isOptional) - continue; // There was no parameter value, but the param is optional - normalized = param.type.$normalize(rawVal); - if (!param.type.is(normalized)) - return false; // The value was not of the correct Type, and could not be decoded to the correct Type - encoded = param.type.encode(normalized); - if (isString(encoded) && !param.type.pattern.exec(encoded)) - return false; // The value was of the correct type, but when encoded, did not match the Type's regexp - } - return true; - } - - $$filter(filterFn) { - var self = this, subset = new ParamSet(); - forEach(this.$$keys(), function(key) { - if (filterFn(self[key])) - subset[key] = self[key]; - }); - return subset; - } - - $$parent() { return null; } -} diff --git a/src/params/paramValues.ts b/src/params/paramValues.ts deleted file mode 100644 index c884f6383..000000000 --- a/src/params/paramValues.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {IParamsPath} from "../path/interface"; -import {IRawParams} from "../params/interface"; - -import {extend, find, curry} from "../common/common"; -/** - * This class closes over a Path and encapsulates the parameter values from the Path's Nodes. - * The param values for the path are flattened and copied to the resulting ParamValues object. - * Param values for a specific state are exposed with the $byState(stateName) function. - */ -const stateNameMatches = curry((stateName: string, node) => node.state.name === stateName); - -export default class ParamValues implements IRawParams { - [key: string]: any - private $$path: IParamsPath; - - constructor($$path: IParamsPath) { - Object.defineProperty(this, "$$path", { value: $$path }); - $$path.nodes().reduce((memo, node) => extend(memo, node.ownParamValues), this); - } - - /** Gets the param values for a given state (by state name) */ - $byState(stateName: string) { - let found = find(this.$$path.nodes(), stateNameMatches(stateName)); - return found && found.ownParamValues; - } - - /** Returns a new ParamValues object which closes over a subpath of this ParamValue's Path. */ - $isolateRootTo(stateName: string): ParamValues { - return new ParamValues(this.$$path.pathFromRootTo(stateName)); - } -} diff --git a/src/path/interface.ts b/src/path/interface.ts index 9cd2710e3..9f6f92133 100644 --- a/src/path/interface.ts +++ b/src/path/interface.ts @@ -1,49 +1,29 @@ - -import Path from "./../path/path"; - -import {IState} from "../state/interface"; +import {State} from "../state/state"; +import Node from "../path/node"; import {ViewConfig} from "../view/view"; import {IRawParams} from "../params/interface"; -import ParamValues from "../params/paramValues"; import {IResolvables} from "../resolve/interface"; import ResolveContext from "../resolve/resolveContext"; import ResolveInjector from "../resolve/resolveInjector"; -/** Base data (contains a state) for a node in a Path */ -export interface INode { - state: IState; -} -/** A basic Path. Each node contains an IState */ -export interface IPath extends Path {} - -/** Contains INode base data plus raw params values for the node */ -export interface IParamsNode extends INode { - ownParamValues: IRawParams; +/** Contains Node base data plus raw params values for the node */ +export interface IParamsNode extends Node { + values: IRawParams; } -/** A Path of IParamsNode(s) */ -export interface IParamsPath extends Path {} - /** Contains IParamsNode data, plus Resolvables for the node */ export interface IResolveNode extends IParamsNode { - ownResolvables: IResolvables; + resolves: IResolvables; } -/** A Path of IResolveNode(s) */ -export interface IResolvePath extends Path {} /** Contains IResolveNode data, plus a ResolveContext and ParamsValues (bound to a full path) for the node, */ export interface ITransNode extends IResolveNode { resolveContext: ResolveContext; resolveInjector: ResolveInjector; views: ViewConfig[]; - paramValues: ParamValues; + // paramValues: ParamValues; } -/** - * A Path of ITransNode(s). Each node contains raw param values, Resolvables and also a ResolveContext - * and ParamValues which are bound to the overall path, but isolated to the node. - */ -export interface ITransPath extends Path {} diff --git a/src/path/module.ts b/src/path/module.ts index fc0c970a1..739d6d068 100644 --- a/src/path/module.ts +++ b/src/path/module.ts @@ -1,5 +1,5 @@ -import * as path from "./path"; -export {path}; +import * as Node from "./node"; +export {Node}; import * as pathFactory from "./pathFactory"; export {pathFactory}; diff --git a/src/path/node.ts b/src/path/node.ts new file mode 100644 index 000000000..609010fff --- /dev/null +++ b/src/path/node.ts @@ -0,0 +1,69 @@ +/// +import {extend, pick, prop, propEq, pairs, map, find} from "../common/common"; +import {State} from "../state/state"; +import Param from "../params/param"; +import Type from "../params/type"; +import {IRawParams} from "../params/interface"; +import Resolvable from "../resolve/resolvable"; +import ResolveContext from "../resolve/resolveContext"; +import ResolveInjector from "../resolve/resolveInjector"; +import {ViewConfig} from "../view/view"; + +export default class Node { + + public schema: Param[]; + public values: { [key: string]: any }; + public resolves: any; + public views: ViewConfig[]; + public resolveContext: ResolveContext; + public resolveInjector: ResolveInjector; + + // Possibly extract this logic into an intermediary object that maps states to nodes + constructor(public state: State, params: IRawParams, resolves: any = {}) { + const schema: Param[] = state.parameters({ inherit: false }); + // schema = keys.map(key => [key, state.parameter(key)]).reduce(applyPairs, {}); + + // Object.freeze(extend(this, { ... })) + extend(this, { + state, + schema, + values: pick(params, schema.map(prop('id'))), + resolves: map( + extend(state.resolve || {}, resolves), + (fn: Function, name: string) => new Resolvable(name, fn, state) + ), + views: pairs(state.views || {}).map(([rawViewName, viewDeclarationObj]): ViewConfig => { + return new ViewConfig({ + rawViewName, viewDeclarationObj, context: state, params + }); + }) + }); + } + + parameter(name: string): Param { + return find(this.schema, prop('id')); + } + + equals(node: Node, keys?: string[]): boolean { + return this.state === node.state && (keys || Object.keys(this.values)).map(key => { + return this.parameter(key).type.equals(this.values[key], node.values[key]); + }).reduce((previous, current) => previous && current, true); + } + + static clone(node: Node, update: any = {}) { + return new Node(node.state, update.params || node.values, update.resolves || map(node.resolves, prop('resolveFn'))); + } + + /** + * Returns a new path which is a subpath of this path. The new path starts from root and contains any nodes + * that match the nodes in the second path. Nodes are compared using their state properties. + * @param first {Node[]} + * @param second {Node[]} + * @returns {Node[]} + */ + static matching(first: Node[], second: Node[]): Node[] { + let matchedCount = first.reduce((prev, node, i) => + prev === i && i < second.length && node.state === second[i].state ? i + 1 : prev, 0); + return first.slice(matchedCount); + } +} \ No newline at end of file diff --git a/src/path/path.ts b/src/path/path.ts deleted file mode 100644 index afbefe720..000000000 --- a/src/path/path.ts +++ /dev/null @@ -1,106 +0,0 @@ -/// - -import {extend, isString, find, pipe, parse, prop, eq} from "../common/common"; -import {INode} from "./interface"; - -import {IState, IStateDeclaration, IStateOrName} from "../state/interface"; - -const stateMatches = (state: IState|IStateDeclaration) => (node) => node.state === state || node.state.self === state; -const stateNameMatches = (stateName: string) => (node) => node.state.name === stateName; -const shallowNodeCopy = node => extend({}, node); - -/** - * A Path Object represents a Path of nested States within the State Hierarchy. - * Each node of the path holds the IState object, and additional data, according - * to the use case. - * - * A Path can be used to construct new Paths based on the current Path via the concat - * and slice helper methods. - * - * @param _nodes [array]: an array of INode data - */ -export default class Path { - constructor(private _nodes: NODE[]) { } - - /** - * returns a subpath of this path from the root path element up to and including the toState parameter. - * Each node of the subpath is a shallow copy of the original node. - * - * @param toState A state or name of a state - */ - pathFromRootTo(toState: IStateOrName): Path { - let predicate = isString(toState) ? stateNameMatches( toState) : stateMatches( toState); - let node = find(this._nodes, predicate); - let elementIdx = this._nodes.indexOf(node); - if (elementIdx === -1) throw new Error("This Path does not contain the toPathElement"); - return this.slice(0, elementIdx + 1); - } - - /** - * Returns a new Path which contains this Path's nodes, concatenated with another Path's nodes. - * Each node of the concatenated Path is a shallow copy of the original nodes. - */ - concat(path: Path): Path { - return new Path(this._nodes.concat(path._nodes).map(shallowNodeCopy)); - } - - /** - * Returns a new Path which is a subpath of this Path. The new Path contains nodes starting from "start" and - * ending at "end". Each node of the subpath is a shallow copy of the original Path's node. - */ - slice(start: number, end?: number): Path { - return new Path(this._nodes.slice(start, end).map(shallowNodeCopy)); - } - - /** - * Returns a new Path which is a subpath of this Path. The new Path starts from root and contains any nodes - * that match the nodes in the otherPath. Nodes are compared using their state properties. - * @param otherPath {Path} - * @returns {Path} - */ - matching(otherPath: Path): Path { - let otherNodes = otherPath._nodes; - let matchedCount = this._nodes.reduce((prev, node, i) => - prev === i && i < otherNodes.length && node.state === otherNodes[i].state ? i + 1 : prev, 0); - return this.slice(matchedCount); - } - - /** - * Returns a new Path which is a copy of this Path, but with nodes in reverse order. - * Each node in the reversed path is a shallow copy of the original Path's node. - */ - reverse(): Path { - return new Path(this.nodes().map(shallowNodeCopy).reverse()); - } - - /** Returns the "state" property of each node in this Path */ - states(): IState[] { - return this._nodes.map(prop("state")); - } - - /** Gets the first node that exactly matches the given state */ - nodeForState(state: IStateOrName): NODE { - let propName = (isString(state) ? "state.name" : "state"); - return find(this._nodes, pipe(parse(propName), eq(state))); - } - - /** Returns the Path's nodes wrapped in a new array */ - nodes(): NODE[] { - return [].concat(this._nodes); - } - - /** Returns the last node in the Path */ - last(): NODE { - return this._nodes.length ? this._nodes[this._nodes.length - 1] : null; - } - - /** Returns a new path where each path element is mapped using the nodeMapper function */ - adapt(nodeMapper: (NODE, idx?) => T): Path { - return new Path(this._nodes.map(nodeMapper)); - } - - toString() { - var elements = this._nodes.map(parse('state.name')).join(', '); - return `Path([${elements}])`; - } -} \ No newline at end of file diff --git a/src/path/pathFactory.ts b/src/path/pathFactory.ts index 9c84ee35f..78e57f2fe 100644 --- a/src/path/pathFactory.ts +++ b/src/path/pathFactory.ts @@ -1,15 +1,13 @@ -import {map, extend, pairs, prop, pick, omit, not, curry} from "../common/common"; +import {map, extend, find, pairs, prop, propEq, pick, omit, not, curry, tail, applyPairs} from "../common/common"; import {IRawParams} from "../params/interface"; -import ParamValues from "../params/paramValues"; import {ITreeChanges} from "../transition/interface"; -import {IState} from "../state/interface"; +import {State} from "../state/state"; import TargetState from "../state/targetState"; -import {INode, IParamsNode, IResolveNode, ITransNode, IParamsPath, IResolvePath, ITransPath} from "../path/interface"; -import Path from "../path/path"; +import Node from "../path/node"; import Resolvable from "../resolve/resolvable"; import ResolveContext from "../resolve/resolveContext"; @@ -18,47 +16,36 @@ import {ViewConfig} from "../view/view"; import ResolveInjector from "../resolve/resolveInjector"; /** - * This class contains functions which convert TargetStates, Nodes and Paths from one type to another. + * This class contains functions which convert TargetStates, Nodes and paths from one type to another. */ export default class PathFactory { + constructor() { } - /** Given a TargetState, create an IParamsPath */ - static makeParamsPath(ref: TargetState): IParamsPath { + /** Given a TargetState, create a Node[] */ + static makeParamsPath(ref: TargetState): Node[] { let states = ref ? ref.$state().path : []; let params = ref ? ref.params() : {}; - const toParamsNodeFn: (IState) => IParamsNode = PathFactory.makeParamsNode(params); - return new Path(states.map(toParamsNodeFn)); + const toParamsNodeFn: (State) => Node = PathFactory.makeParamsNode(params); + return states.map(toParamsNodeFn); } - /** Given a IParamsPath, create an TargetState */ - static makeTargetState(path: IParamsPath): TargetState { - let state = path.last().state; - return new TargetState(state, state, new ParamValues(path)); + /** Given a Node[], create an TargetState */ + static makeTargetState(path: Node[]): TargetState { + let state = tail(path).state; + return new TargetState(state, state, /*new ParamValues(*/[path]/*)*/); } - /* Given params and a state, creates an IParamsNode */ - static makeParamsNode = curry((params: IRawParams, state: IState) => { - return { - state, - ownParamValues: state.ownParams.$$values(params) - }; - }); - - /** Given an IParamsNode, make an IResolveNode by creating resolvables for resolves on the state's declaration */ - static makeResolveNode(node: IParamsNode): IResolveNode { - const makeResolvable = (_node: INode) => (resolveFn: Function, name: string) => new Resolvable(name, resolveFn, _node.state); - let ownResolvables = map(node.state.resolve || {}, makeResolvable(node)); - return extend({}, node, {ownResolvables}); - } + /* Given params and a state, creates an Node */ + static makeParamsNode = curry((params: IRawParams, state: State) => new Node(state, params)); - /** Given a fromPath: ITransPath and a TargetState, builds a toPath: IParamsPath */ - static buildToPath(fromPath: ITransPath, targetState: TargetState): IParamsPath { + /** Given a fromPath: Node[] and a TargetState, builds a toPath: Node[] */ + static buildToPath(fromPath: Node[], targetState: TargetState): Node[] { let toParams = targetState.params(); - const toParamsNodeFn: (IState) => IParamsNode = PathFactory.makeParamsNode(toParams); - let toPath: IParamsPath = new Path(targetState.$state().path.map(toParamsNodeFn)); - if (targetState.options().inherit) - toPath = PathFactory.inheritParams(fromPath, toPath, Object.keys(toParams)); + const toParamsNodeFn: (State) => Node = PathFactory.makeParamsNode(toParams); + let toPath: Node[] = targetState.$state().path.map(toParamsNodeFn); + + if (targetState.options().inherit) toPath = PathFactory.inheritParams(fromPath, toPath, Object.keys(toParams)); return toPath; } @@ -73,98 +60,83 @@ export default class PathFactory { * caller, for instance, $state.transitionTo(..., toParams). If a key was found in toParams, * it is not inherited from the fromPath. */ - static inheritParams(fromPath: IParamsPath, toPath: IParamsPath, toKeys: string[] = []): IParamsPath { - function nodeParamVals(path: IParamsPath, state: IState): IRawParams { - let node = path.nodeForState(state); - return extend({}, node && node.ownParamValues); + static inheritParams(fromPath: Node[], toPath: Node[], toKeys: string[] = []): Node[] { + function nodeParamVals(path: Node[], state: State): IRawParams { + let node = find(path, propEq('state', state)); + return extend({}, node && node.values); } /** - * Given an IParamsNode "toNode", return a new IParamsNode with param values inherited from the + * Given an Node "toNode", return a new Node with param values inherited from the * matching node in fromPath. Only inherit keys that aren't found in "toKeys" from the node in "fromPath"" */ - let makeInheritedParamsNode = curry(function(_fromPath: IParamsPath, _toKeys: string[], toNode: IParamsNode): IParamsNode { + let makeInheritedParamsNode = curry(function(_fromPath: Node[], _toKeys: string[], toNode: Node): Node { // All param values for the node (may include default key/vals, when key was not found in toParams) - let toParamVals = extend({}, toNode && toNode.ownParamValues); + let toParamVals = extend({}, toNode && toNode.values); // limited to only those keys found in toParams let incomingParamVals = pick(toParamVals, _toKeys); toParamVals = omit(toParamVals, _toKeys); let fromParamVals = nodeParamVals(_fromPath, toNode.state) || {}; // extend toParamVals with any fromParamVals, then override any of those those with incomingParamVals - let ownParamValues: IRawParams = extend(toParamVals, fromParamVals, incomingParamVals); - return { state: toNode.state, ownParamValues }; + let ownParamVals: IRawParams = extend(toParamVals, fromParamVals, incomingParamVals); + return new Node(toNode.state, ownParamVals); }); // The param keys specified by the incoming toParams - return new Path( toPath.nodes().map(makeInheritedParamsNode(fromPath, toKeys))); + return toPath.map(makeInheritedParamsNode(fromPath, toKeys)); } /** - * Given an IResolvePath, upgrades the path to an ITransPath. Each node is assigned a ResolveContext + * Given a path, upgrades the path to a Node[]. Each node is assigned a ResolveContext * and ParamValues object which is bound to the whole path, but closes over the subpath from root to the node. * The views are also added to the node. */ - static bindTransNodesToPath(resolvePath: IResolvePath): ITransPath { + static bindTransNodesToPath(resolvePath: Node[]): Node[] { let resolveContext = new ResolveContext(resolvePath); - let paramValues = new ParamValues(resolvePath); - let transPath = resolvePath; - - // TODO: this doesn't belong here. - function makeViews(node: ITransNode) { - let context = node.state, params = node.paramValues; - const makeViewConfig = ([rawViewName, viewDeclarationObj]) => - new ViewConfig({rawViewName, viewDeclarationObj, context, params}); - return pairs(node.state.views || {}).map(makeViewConfig); - } + // let paramValues = new ParamValues(resolvePath); // Attach bound resolveContext and paramValues to each node // Attach views to each node - transPath.nodes().forEach((node: ITransNode) => { + resolvePath.forEach((node: Node) => { node.resolveContext = resolveContext.isolateRootTo(node.state); node.resolveInjector = new ResolveInjector(node.resolveContext, node.state); - node.paramValues = paramValues.$isolateRootTo(node.state.name); - node.ownResolvables["$stateParams"] = new Resolvable("$stateParams", () => node.paramValues, node.state); - node.views = makeViews(node); + // node.paramValues = paramValues.$isolateRootTo(node.state.name); + node.resolves["$stateParams"] = new Resolvable("$stateParams", () => node.values, node.state); }); - return transPath; + return resolvePath; } /** * Computes the tree changes (entering, exiting) between a fromPath and toPath. */ - static treeChanges(fromPath: ITransPath, toPath: IParamsPath, reloadState: IState): ITreeChanges { - let fromNodes = fromPath.nodes(); - let toNodes = toPath.nodes(); - let keep = 0, max = Math.min(fromNodes.length, toNodes.length); - - const staticParams = (state) => state.params.$$filter(not(prop('dynamic'))); - const nodesMatch = (node1: IParamsNode, node2: IParamsNode) => - node1.state === node2.state && staticParams(node1.state).$$equals(node1.ownParams, node2.ownParams); + static treeChanges(fromPath: Node[], toPath: Node[], reloadState: State): ITreeChanges { + let keep = 0, max = Math.min(fromPath.length, toPath.length); + const staticParams = (state) => state.parameters({ inherit: false }).filter(not(prop('dynamic'))).map(prop('id')); + const nodesMatch = (node1: Node, node2: Node) => node1.equals(node2, staticParams(node1.state)); - while (keep < max && fromNodes[keep].state !== reloadState && nodesMatch(fromNodes[keep], toNodes[keep])) { + while (keep < max && fromPath[keep].state !== reloadState && nodesMatch(fromPath[keep], toPath[keep])) { keep++; } /** Given a retained node, return a new node which uses the to node's param values */ - function applyToParams(retainedNode: ITransNode, idx: number): ITransNode { - let toNodeParams = toPath.nodes()[idx].ownParamValues; - return extend({}, retainedNode, { ownParamValues: toNodeParams }); + function applyToParams(retainedNode: Node, idx: number): Node { + return Node.clone(retainedNode, { params: toPath[idx].values }); } - let from: ITransPath, retained: ITransPath, exiting: ITransPath, entering: ITransPath, to: ITransPath; + let from: Node[], retained: Node[], exiting: Node[], entering: Node[], to: Node[]; // intermediate vars - let retainedWithToParams: ITransPath, enteringResolvePath: IResolvePath, toResolvePath: IResolvePath; + let retainedWithToParams: Node[], enteringResolvePath: Node[], toResolvePath: Node[]; from = fromPath; retained = from.slice(0, keep); exiting = from.slice(keep); // Create a new retained path (with shallow copies of nodes) which have the params of the toPath mapped - retainedWithToParams = retained.adapt(applyToParams); - enteringResolvePath = toPath.slice(keep).adapt(PathFactory.makeResolveNode); + retainedWithToParams = retained.map(applyToParams); + enteringResolvePath = toPath.slice(keep); // "toResolvePath" is "retainedWithToParams" concat "enteringResolvePath". - toResolvePath = ( retainedWithToParams).concat(enteringResolvePath); + toResolvePath = (retainedWithToParams).concat(enteringResolvePath); // "to: is "toResolvePath" with ParamValues/ResolveContext added to each node and bound to the path context to = PathFactory.bindTransNodesToPath(toResolvePath); @@ -174,5 +146,4 @@ export default class PathFactory { return { from, to, retained, exiting, entering }; } - } diff --git a/src/resolve/resolvable.ts b/src/resolve/resolvable.ts index ea3839ba9..a0d69c377 100644 --- a/src/resolve/resolvable.ts +++ b/src/resolve/resolvable.ts @@ -4,13 +4,11 @@ import trace from "../common/trace"; import {runtime} from "../common/angular1" import {IPromise} from "angular"; -import {IState} from "../state/interface"; +import {State} from "../state/state"; import {IResolvables, IOptions1} from "./interface" import ResolveContext from "./resolveContext" -import {IResolvePath} from "../path/interface" - /** * The basic building block for the resolve system. * @@ -20,11 +18,11 @@ import {IResolvePath} from "../path/interface" * Resolvable.get() either retrieves the Resolvable's existing promise, or else invokes resolve() (which invokes the * resolveFn) and returns the resulting promise. * - * Resolvable.get() and Resolvable.resolve() both execute within a context Path, which is passed as the first + * Resolvable.get() and Resolvable.resolve() both execute within a context path, which is passed as the first * parameter to those fns. */ export default class Resolvable { - constructor(name: string, resolveFn: Function, state: IState) { + constructor(name: string, resolveFn: Function, state: State) { this.name = name; this.resolveFn = resolveFn; this.state = state; @@ -33,7 +31,7 @@ export default class Resolvable { name: string; resolveFn: Function; - state: IState; + state: State; deps: string[]; promise: IPromise = undefined; diff --git a/src/resolve/resolveContext.ts b/src/resolve/resolveContext.ts index 966627170..f89e5d4b9 100644 --- a/src/resolve/resolveContext.ts +++ b/src/resolve/resolveContext.ts @@ -1,14 +1,14 @@ /// -import {IInjectable, filter, map, noop, defaults, extend, prop, pick, omit, isString, isObject} from "../common/common"; +import {IInjectable, find, filter, map, noop, tail, defaults, extend, prop, propEq, pick, omit, isString, isObject} from "../common/common"; import trace from "../common/trace"; import {runtime} from "../common/angular1"; import {IPromise} from "angular"; -import {IResolvePath, IResolveNode} from "../path/interface"; +import Node from "../path/node"; import {IPromises, IResolvables, ResolvePolicy, IOptions1} from "./interface"; import Resolvable from "./resolvable"; -import {IState} from "../state/interface"; +import {State} from "../state/state"; // TODO: make this configurable let defaultResolvePolicy = ResolvePolicy[ResolvePolicy.LAZY]; @@ -16,8 +16,24 @@ let defaultResolvePolicy = ResolvePolicy[ResolvePolicy.LAZY]; interface IPolicies { [key: string]: string; } export default class ResolveContext { - constructor(private _path: IResolvePath) { } - + + private _nodeFor: Function; + private _pathTo: Function; + + constructor(private _path: Node[]) { + extend(this, { + _nodeFor(state: State): Node { + return find(this._path, propEq('state', state)); + }, + _pathTo(state: State): Node[] { + let node = this._nodeFor(state); + let elementIdx = this._path.indexOf(node); + if (elementIdx === -1) throw new Error("This path does not contain the state"); + return this._path.slice(0, elementIdx + 1); + } + }); + } + /** * Gets the available Resolvables for the last element of this path. * @@ -39,50 +55,51 @@ export default class ResolveContext { * state({ name: 'G.G2', resolve: { _G: function(_G) { return _G + "G2"; } } }); * where injecting _G into a controller will yield "GG2" */ - getResolvables(state?: IState, options?: any): IResolvables { + getResolvables(state?: State, options?: any): IResolvables { options = defaults(options, { omitOwnLocals: [] }); - let path: IResolvePath = (state ? this._path.pathFromRootTo(state) : this._path); - let last = path.last(); - - return path.nodes().reduce((memo, node) => { + + const offset = find(this._path, propEq('')); + const path = (state ? this._pathTo(state) : this._path); + const last = tail(path); + + return path.reduce((memo, node) => { let omitProps = (node === last) ? options.omitOwnLocals : []; - let filteredResolvables = omit(node.ownResolvables, omitProps); + let filteredResolvables = omit(node.resolves, omitProps); return extend(memo, filteredResolvables); }, {}); } /** Inspects a function `fn` for its dependencies. Returns an object containing any matching Resolvables */ - getResolvablesForFn(fn: IInjectable, resolveContext: ResolveContext = this): {[key: string]: Resolvable} { + getResolvablesForFn(fn: IInjectable, resolveContext: ResolveContext = this): { [key: string]: Resolvable } { let deps = runtime.$injector.annotate( fn); return pick(resolveContext.getResolvables(), deps); } - isolateRootTo(state: IState): ResolveContext { - return new ResolveContext(this._path.pathFromRootTo(state)); + isolateRootTo(state: State): ResolveContext { + return new ResolveContext(this._pathTo(state)); } - addResolvables(resolvables: IResolvables, state: IState) { - let node = this._path.nodeForState(state); - extend(node.ownResolvables, resolvables); + addResolvables(resolvables: IResolvables, state: State) { + extend(this._nodeFor(state).resolves, resolvables); } /** Gets the resolvables declared on a particular state */ - getOwnResolvables(state: IState): IResolvables { - return extend({}, this._path.nodeForState(state).ownResolvables); + getOwnResolvables(state: State): IResolvables { + return extend({}, this._nodeFor(state).resolves); } - // Returns a promise for an array of resolved Path Element promises + // Returns a promise for an array of resolved path Element promises resolvePath(options: IOptions1 = {}): IPromise { trace.traceResolvePath(this._path, options); - const promiseForNode = (node: IResolveNode) => this.resolvePathElement(node.state, options); - return runtime.$q.all( map(this._path.nodes(), promiseForNode)).then(noop); + const promiseForNode = (node: Node) => this.resolvePathElement(node.state, options); + return runtime.$q.all( map(this._path, promiseForNode)).then(noop); } // returns a promise for all the resolvables on this PathElement // options.resolvePolicy: only return promises for those Resolvables which are at // the specified policy, or above. i.e., options.resolvePolicy === 'lazy' will // resolve both 'lazy' and 'eager' resolves. - resolvePathElement(state: IState, options: IOptions1 = {}): IPromise { + resolvePathElement(state: State, options: IOptions1 = {}): IPromise { // The caller can request the path be resolved for a given policy and "below" let policy: string = options && options.resolvePolicy; let policyOrdinal: number = ResolvePolicy[policy || defaultResolvePolicy]; @@ -102,7 +119,7 @@ export default class ResolveContext { /** - * Injects a function given the Resolvables available in the IResolvePath, from the first node + * Injects a function given the Resolvables available in the path, from the first node * up to the node for the given state. * * First it resolves all the resolvable depencies. When they are done resolving, it invokes @@ -110,12 +127,12 @@ export default class ResolveContext { * * @return a promise for the return value of the function. * - * @param state: The state context object (within the Path) + * @param state: The state context object (within the path) * @param fn: the function to inject (i.e., onEnter, onExit, controller) * @param locals: are the angular $injector-style locals to inject * @param options: options (TODO: document) */ - invokeLater(state: IState, fn: IInjectable, locals: any = {}, options: IOptions1 = {}): IPromise { + invokeLater(state: State, fn: IInjectable, locals: any = {}, options: IOptions1 = {}): IPromise { let isolateCtx = this.isolateRootTo(state); let resolvables = this.getResolvablesForFn(fn, isolateCtx); trace.tracePathElementInvoke(state, fn, Object.keys(resolvables), extend({when: "Later"}, options)); @@ -132,21 +149,21 @@ export default class ResolveContext { } /** - * Immediately injects a function with the dependent Resolvables available in the IResolvePath, from + * Immediately injects a function with the dependent Resolvables available in the path, from * the first node up to the node for the given state. * * If a Resolvable is not yet resolved, then null is injected in place of the resolvable. * * @return the return value of the function. * - * @param state: The state context object (within the Path) + * @param state: The state context object (within the path) * @param fn: the function to inject (i.e., onEnter, onExit, controller) * @param locals: are the angular $injector-style locals to inject * @param options: options (TODO: document) */ // Injects a function at this PathElement level with available Resolvables // Does not wait until all Resolvables have been resolved; you must call PathElement.resolve() (or manually resolve each dep) first - invokeNow(state: IState, fn: IInjectable, locals: any, options: any = {}) { + invokeNow(state: State, fn: IInjectable, locals: any, options: any = {}) { let isolateCtx = this.isolateRootTo(state); let resolvables = this.getResolvablesForFn(fn, isolateCtx); trace.tracePathElementInvoke(state, fn, Object.keys(resolvables), extend({when: "Now "}, options)); diff --git a/src/resolve/resolveInjector.ts b/src/resolve/resolveInjector.ts index 67da206dd..1cd183eee 100644 --- a/src/resolve/resolveInjector.ts +++ b/src/resolve/resolveInjector.ts @@ -1,10 +1,10 @@ import {map} from "../common/common"; import ResolveContext from "../resolve/resolveContext"; -import {IState} from "../state/interface"; +import {State} from "../state/state"; import Resolvable from "./resolvable"; export default class ResolveInjector { - constructor(private _resolveContext: ResolveContext, private _state: IState) { } + constructor(private _resolveContext: ResolveContext, private _state: State) { } /** Returns a promise to invoke an annotated function in the resolve context */ invokeLater(injectedFn, locals) { diff --git a/src/state/hooks/resolveHooks.ts b/src/state/hooks/resolveHooks.ts index faff64840..d774bd341 100644 --- a/src/state/hooks/resolveHooks.ts +++ b/src/state/hooks/resolveHooks.ts @@ -1,4 +1,4 @@ -import {extend} from "../../common/common"; +import {extend, find, propEq, tail} from "../../common/common"; import {ResolvePolicy} from "../../resolve/interface"; @@ -23,13 +23,13 @@ export default class ResolveHooks { /** a function which resolves any EAGER Resolvables for a Path */ $eagerResolvePath.$inject = ['$transition$']; function $eagerResolvePath($transition$) { - return treeChanges.to.last().resolveContext.resolvePath(extend({transition: $transition$}, { resolvePolicy: EAGER })); + return tail( treeChanges.to).resolveContext.resolvePath(extend({ transition: $transition$ }, { resolvePolicy: EAGER })); } /** Returns a function which pre-resolves any LAZY Resolvables for a Node in a Path */ $lazyResolveEnteringState.$inject = ['$state$', '$transition$']; function $lazyResolveEnteringState($state$, $transition$) { - let node = treeChanges.entering.nodeForState($state$); + let node = find( treeChanges.entering, propEq('state', $state$)); return node.resolveContext.resolvePathElement(node.state, extend({transition: $transition$}, { resolvePolicy: LAZY })); } diff --git a/src/state/hooks/transitionManager.ts b/src/state/hooks/transitionManager.ts index 0418a0aff..b171c0cce 100644 --- a/src/state/hooks/transitionManager.ts +++ b/src/state/hooks/transitionManager.ts @@ -1,6 +1,7 @@ import {IPromise, IQService} from "angular"; import {copy, prop} from "../../common/common"; import Queue from "../../common/queue"; +import Param from "../../params/param"; import {ITreeChanges} from "../../transition/interface"; import {Transition} from "../../transition/transition"; @@ -88,7 +89,7 @@ export default class TransitionManager { if (error instanceof TransitionRejection) { if (error.type === RejectType.IGNORED) { // Update $stateParmas/$state.params/$location.url if transition ignored, but dynamic params have changed. - if (!$state.$current.params.$$filter(prop('dynamic')).$$equals($stateParams, transition.params())) { + if (!Param.equals($state.$current.parameters().filter(prop('dynamic')), $stateParams, transition.params())) { this.updateStateParams(); } return $state.current; diff --git a/src/state/hooks/viewHooks.ts b/src/state/hooks/viewHooks.ts index 238936c27..0915b2c6a 100644 --- a/src/state/hooks/viewHooks.ts +++ b/src/state/hooks/viewHooks.ts @@ -1,5 +1,5 @@ import {IPromise} from "angular"; -import {noop} from "../../common/common"; +import {find, propEq, noop} from "../../common/common"; import {annotateController, runtime} from "../../common/angular1"; import {ITreeChanges} from "../../transition/interface"; @@ -25,7 +25,7 @@ export default class ViewHooks { loadAllEnteringViews() { const loadView = (vc: ViewConfig) => { - let resolveInjector = this.treeChanges.to.nodeForState(vc.context.name).resolveInjector; + let resolveInjector = find(this.treeChanges.to, propEq('state', vc.context)).resolveInjector; return > this.$view.load(vc, resolveInjector); }; return runtime.$q.all(this.enteringViews.map(loadView)).then(noop); @@ -34,7 +34,7 @@ export default class ViewHooks { loadAllControllerLocals() { const loadLocals = (vc: ViewConfig) => { let deps = annotateController(vc.controller); - let resolveInjector = this.treeChanges.to.nodeForState(vc.context.name).resolveInjector; + let resolveInjector = find(this.treeChanges.to, propEq('state', vc.context)).resolveInjector; function $loadControllerLocals() { } $loadControllerLocals.$inject = deps; return runtime.$q.all(resolveInjector.getLocals($loadControllerLocals)).then((locals) => vc.locals = locals); diff --git a/src/state/interface.ts b/src/state/interface.ts index 9d7399ba5..a2dcb1237 100644 --- a/src/state/interface.ts +++ b/src/state/interface.ts @@ -3,16 +3,17 @@ import {IPromise} from "angular"; import UrlMatcher from "../url/urlMatcher"; import {IRawParams, IParamsOrArray} from "../params/interface"; -import ParamSet from "../params/paramSet"; +import Param from "../params/param"; import {IContextRef} from "../view/interface"; import TargetState from "./targetState"; +import {State} from "./state"; import {ITransitionOptions} from "../transition/interface"; import {Transition} from "../transition/transition"; -export type IStateOrName = (string|IStateDeclaration|IState); +export type IStateOrName = (string|IStateDeclaration|State); /** Context obj, State-view definition, transition params */ export interface IStateViewConfig { @@ -34,7 +35,7 @@ export interface IViewDeclaration { } /** hash of strings->views */ -interface IViewDeclarations { [key: string]: IViewDeclaration; } +export interface IViewDeclarations { [key: string]: IViewDeclaration; } /** hash of strings->resolve fns */ export interface IResolveDeclarations { [key: string]: Function; } /** hash of strings->param declarations */ @@ -63,28 +64,9 @@ export interface IStateDeclaration extends IViewDeclaration { // TODO: finish defining state definition API. Maybe start with what's on Definitely Typed. } -/** internal state API */ -export interface IState { - name: string; - abstract: boolean; - parent: IState; - resolve: IResolveDeclarations; // name->Function - resolvePolicy: (string|Object); - url: UrlMatcher; - params: ParamSet; - ownParams: ParamSet; - views: IViewDeclarations; - self: IStateDeclaration; - root: () => IState; - navigable: IState; - path: IState[]; - data: any; - includes: (name: string) => boolean; -} - export interface IStateParams { $digest: () => void; - $inherit: (newParams, $current: IState, $to: IState) => IStateParams; + $inherit: (newParams, $current: State, $to: State) => IStateParams; $set: (params, url) => boolean; $sync: () => IStateParams; $off: () => IStateParams; @@ -110,12 +92,12 @@ export interface IStateProvider { export interface IStateService { params: any; // TODO: StateParams current: IStateDeclaration; - $current: IState; + $current: State; transition: Transition; - reload (stateOrName: IStateOrName): IPromise; + reload (stateOrName: IStateOrName): IPromise; targetState (identifier: IStateOrName, params: IParamsOrArray, options: ITransitionOptions): TargetState; - go (to: IStateOrName, params: IRawParams, options: ITransitionOptions): IPromise; - transitionTo (to: IStateOrName, toParams: IParamsOrArray, options: ITransitionOptions): IPromise; + go (to: IStateOrName, params: IRawParams, options: ITransitionOptions): IPromise; + transitionTo (to: IStateOrName, toParams: IParamsOrArray, options: ITransitionOptions): IPromise; is (stateOrName: IStateOrName, params?: IRawParams, options?: ITransitionOptions): boolean; includes (stateOrName: IStateOrName, params?: IRawParams, options?: ITransitionOptions): boolean; href (stateOrName: IStateOrName, params?: IRawParams, options?: IHrefOptions): string; diff --git a/src/state/state.ts b/src/state/state.ts index 7648cc72b..824a314c1 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -1,8 +1,14 @@ -import {extend, defaults, copy, equalForKeys, forEach, ancestors, isDefined, isObject, isString} from "../common/common"; +import { + extend, defaults, copy, equalForKeys, forEach, find, prop, + propEq, ancestors, noop, isDefined, isObject, isString +} from "../common/common"; import Queue from "../common/queue"; import {IServiceProviderFactory, IPromise} from "angular"; -import {IStateService, IState, IStateDeclaration, IStateOrName, IHrefOptions} from "./interface"; +import { + IStateService, IStateDeclaration, IStateOrName, IHrefOptions, + IViewDeclarations, IResolveDeclarations +} from "./interface"; import Glob from "./glob"; import StateQueueManager from "./stateQueueManager"; import StateBuilder from "./stateBuilder"; @@ -14,13 +20,19 @@ import {Transition} from "../transition/transition"; import {RejectFactory} from "../transition/rejectFactory"; import {defaultTransOpts} from "../transition/transitionService"; -import {IParamsPath, ITransPath} from "../path/interface"; -import Path from "../path/path"; +import Node from "../path/node"; import PathFactory from "../path/pathFactory"; import {IRawParams, IParamsOrArray} from "../params/interface"; import TransitionManager from "./hooks/transitionManager"; +import paramTypes from "../params/paramTypes"; +import Param from "../params/param"; +import Type from "../params/type"; + +import UrlMatcher from "../url/urlMatcher"; +import {ViewConfig} from "../view/view"; + /** * @ngdoc object * @name ui.router.state.type:State @@ -35,12 +47,24 @@ import TransitionManager from "./hooks/transitionManager"; */ export class State { - public self: IStateDeclaration; public parent: State; public name: string; + public abstract: boolean; + public resolve: IResolveDeclarations; + public resolvePolicy: any; + public url: UrlMatcher; + public views: IViewDeclarations; + public self: IStateDeclaration; + public navigable: State; + public path: State[]; + public data: any; + public includes: (name: string) => boolean; + + private _params: Param[] = []; constructor(config?: IStateDeclaration) { extend(this, config); + // Object.freeze(this); } /** @@ -72,9 +96,7 @@ export class State { * @returns {string} Returns a dot-separated name of the state. */ fqn(): string { - if (!this.parent || !(this.parent instanceof this.constructor)) { - return this.name; - } + if (!this.parent || !(this.parent instanceof this.constructor)) return this.name; let name = this.parent.fqn(); return name ? name + "." + this.name : this.name; } @@ -93,6 +115,22 @@ export class State { return this.parent && this.parent.root() || this; } + parameters(opts: any = {}): Param[] { + opts = defaults(opts, { inherit: true }); + + return (this.parent && opts.inherit && this.parent.parameters() || []) + .concat(this.url && this.url.parameters({ inherit: false }) || []) + .concat(this._params); + } + + parameter(id: string, opts: any = {}): Param { + return ( + this.url && this.url.parameter(id, opts) || + find(this._params, propEq('id', id)) || + opts.inherit && this.parent && this.parent.parameter(id) + ); + } + toString() { return this.fqn(); } @@ -122,7 +160,7 @@ export class State { $StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider']; function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { - let root: IState, states: {[key: string]: IState} = {}; + let root: State, states: { [key: string]: State } = {}; let $state: IStateService = function $state() {}; let matcher = new StateMatcher(states); @@ -439,25 +477,26 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { * If a callback returns an ITargetState, then it is used as arguments to $state.transitionTo() and * the result returned. */ - function handleInvalidTargetState(fromPath: IParamsPath, $to$: TargetState) { + function handleInvalidTargetState(fromPath: Node[], $to$: TargetState) { const latestThing = () => transQueue.peekTail() || treeChangesQueue.peekTail(); let latest = latestThing(); let $from$ = PathFactory.makeTargetState(fromPath); let callbackQueue = new Queue([].concat(invalidCallbacks)); - const invokeCallback = (callback: Function) => - $q.when($injector.invoke(callback, null, { $to$, $from$ })); + const invokeCallback = (callback: Function) => $q.when($injector.invoke(callback, null, { $to$, $from$ })); function checkForRedirect(result) { - if (result instanceof TargetState) { - let target = result; - // Recreate the TargetState, in case the state is now defined. - target = $state.targetState(target.identifier(), target.params(), target.options()); - if (!target.valid()) return rejectFactory.invalid(target.error()); - if (latestThing() !== latest) return rejectFactory.superseded(); - - return $state.transitionTo(target.identifier(), target.params(), target.options()); + if (!(result instanceof TargetState)) { + return; } + let target = result; + // Recreate the TargetState, in case the state is now defined. + target = $state.targetState(target.identifier(), target.params(), target.options()); + + if (!target.valid()) return rejectFactory.invalid(target.error()); + if (latestThing() !== latest) return rejectFactory.superseded(); + + return $state.transitionTo(target.identifier(), target.params(), target.options()); } function invokeNextCallback() { @@ -476,14 +515,16 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { url: '^', views: null, params: { - '#': { value: null, type: 'hash'} // Param to hold the "hash" at the end of the URL + '#': { value: null, type: 'hash' } }, + // params: [Param.fromPath('#', paramTypes.types.hash, { value: null })], + path: [], 'abstract': true }; root = stateQueue.register(rootStateDef, true); root.navigable = null; - const rootPath = () => PathFactory.bindTransNodesToPath(new Path([PathFactory.makeResolveNode(PathFactory.makeParamsNode({}, root))])); + const rootPath = () => PathFactory.bindTransNodesToPath([new Node(root, {})]); $view.rootContext(root); @@ -542,10 +583,9 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { * @returns {promise} A promise representing the state of the new transition. See * {@link ui.router.state.$state#methods_go $state.go}. */ - $state.reload = function reload(reloadState: IStateOrName): IPromise { - let reloadOpt = isDefined(reloadState) ? reloadState : true; + $state.reload = function reload(reloadState: IStateOrName): IPromise { return $state.transitionTo($state.current, $stateParams, { - reload: reloadOpt, + reload: isDefined(reloadState) ? reloadState : true, inherit: false, notify: false }); @@ -617,7 +657,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { * - *resolve error* - when an error has occurred with a `resolve` * */ - $state.go = function go(to: IStateOrName, params: IRawParams, options: ITransitionOptions): IPromise { + $state.go = function go(to: IStateOrName, params: IRawParams, options: ITransitionOptions): IPromise { let defautGoOpts = { relative: $state.$current, inherit: true }; let transOpts = defaults(options, defautGoOpts, defaultTransOpts); return $state.transitionTo(to, params, transOpts); @@ -667,8 +707,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { * @returns {promise} A promise representing the state of the new transition. See * {@link ui.router.state.$state#methods_go $state.go}. */ - $state.transitionTo = function transitionTo(to: IStateOrName, toParams: IRawParams, options: ITransitionOptions = {}): IPromise { - toParams = toParams || {}; + $state.transitionTo = function transitionTo(to: IStateOrName, toParams: IRawParams = {}, options: ITransitionOptions = {}): IPromise { options = defaults(options, defaultTransOpts); options = extend(options, { current: transQueue.peekTail.bind(transQueue)}); @@ -681,7 +720,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { let ref: TargetState = $state.targetState(to, toParams, options); let latestTreeChanges: ITreeChanges = treeChangesQueue.peekTail(); - let currentPath: ITransPath = latestTreeChanges ? latestTreeChanges.to : rootPath(); + let currentPath: Node[] = latestTreeChanges ? latestTreeChanges.to : rootPath(); if (!ref.exists()) return handleInvalidTargetState(currentPath, ref); @@ -737,7 +776,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { let state = matcher.find(stateOrName, options.relative); if (!isDefined(state)) return undefined; if ($state.$current !== state) return false; - return isDefined(params) && params !== null ? state.params.$$equals($stateParams, params) : true; + return isDefined(params) && params !== null ? Param.equals(state.parameters(), $stateParams, params) : true; }; /** @@ -803,7 +842,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { if (!isDefined(state)) return undefined; if (!isDefined(include[state.name])) return false; - return params ? equalForKeys(state.params.$$values(params), $stateParams, Object.keys(params)) : true; + // @TODO Replace with Param.equals() ? + return params ? equalForKeys(Param.values(state.parameters(), params), $stateParams, Object.keys(params)) : true; }; @@ -853,7 +893,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) { if (!nav || nav.url === undefined || nav.url === null) { return null; } - return $urlRouter.href(nav.url, state.params.$$values(params), { + return $urlRouter.href(nav.url, Param.values(state.parameters(), params), { absolute: options.absolute }); }; @@ -945,7 +985,7 @@ function $StateParamsProvider() { if (url) { forEach(params, function(val, key) { - if ((url.parameters(key) || {}).dynamic !== true) abort = true; + if ((url.parameter(key) || {}).dynamic !== true) abort = true; }); } if (abort) return false; diff --git a/src/state/stateBuilder.ts b/src/state/stateBuilder.ts index eeff5b9b3..abb421735 100644 --- a/src/state/stateBuilder.ts +++ b/src/state/stateBuilder.ts @@ -1,5 +1,4 @@ -import {noop, extend, pick, isArray, isDefined, isFunction, isString, forEach} from "../common/common"; -import ParamSet from "../params/paramSet"; +import {map, noop, extend, pick, prop, omit, isArray, isDefined, isFunction, isString, forEach} from "../common/common"; import Param from "../params/param"; const parseUrl = (url: string): any => { @@ -14,7 +13,8 @@ export default function StateBuilder(root, matcher, $urlMatcherFactoryProvider) let self = this, builders = { parent: function(state) { - return matcher.find(self.parentName(state)); + if (state === root()) return null; + return matcher.find(self.parentName(state)) || root(); }, data: function(state) { @@ -26,36 +26,28 @@ export default function StateBuilder(root, matcher, $urlMatcherFactoryProvider) // Build a URLMatcher if necessary, either via a relative or absolute URL url: function(state) { - const parsed = parseUrl(state.url), parent = state.parent, config = { params: state.params || {} }; - const url = parsed ? $urlMatcherFactoryProvider.compile(parsed.val, config) : state.url; + const parsed = parseUrl(state.url), parent = state.parent; + const url = !parsed ? state.url : $urlMatcherFactoryProvider.compile(parsed.val, { + params: state.params, + paramMap: function(paramConfig, isSearch) { + if (state.reloadOnSearch === false && isSearch) paramConfig = extend(paramConfig || {}, { dynamic: true }); + return paramConfig; + } + }); - if (!url) return; + if (!url) return null; if (!$urlMatcherFactoryProvider.isMatcher(url)) throw new Error(`Invalid url '${url}' in state '${state}'`); return (parsed && parsed.root) ? url : ((parent && parent.navigable) || root()).url.append(url); }, // Keep track of the closest ancestor state that has a URL (i.e. is navigable) navigable: function(state) { - return (state !== root()) && state.url ? state : (state.parent ? state.parent.navigable : null); - }, - - // Own parameters for this state. state.url.params is already built at this point. Create and add non-url params - ownParams: function(state) { - let params = state.url && state.url.params.$$own() || new ParamSet(); - forEach(state.params || {}, function(config, id) { - if (!params[id]) params[id] = Param.fromConfig(id, null, config); - }); - if (state.reloadOnSearch === false) { - // @TODO: Fix me - forEach(params, function(param) { if (param && param.isSearch()) param.dynamic = true; }); - } - return params; + return (state !== root()) && state.url ? state : (state.parent ? state.parent.navigable : null); }, - // Derive parameters for this state and ensure they're a super-set of parent's parameters params: function(state) { - let base = state.parent && state.parent.params ? state.parent.params.$$new() : new ParamSet(); - return extend(base, state.ownParams); + const keys = state.url && state.url.parameters({ inherit: false }).map(prop('id')) || []; + return map(omit(state.params || {}, keys), (config: any, id: string) => Param.fromConfig(id, null, config)); }, // If there is no explicit multi-view configuration, make one up so we don't have diff --git a/src/state/stateMatcher.ts b/src/state/stateMatcher.ts index 46886ee8e..95e8bcdd7 100644 --- a/src/state/stateMatcher.ts +++ b/src/state/stateMatcher.ts @@ -1,8 +1,9 @@ import {isString} from "../common/common"; -import {IState, IStateOrName} from "./interface"; +import {IStateOrName} from "./interface"; +import {State} from "./state"; export default class StateMatcher { - constructor (private _states: {[key: string]: IState}) { } + constructor (private _states: { [key: string]: State }) { } isRelative(stateName: string) { stateName = stateName || ""; @@ -10,7 +11,7 @@ export default class StateMatcher { } - find(stateOrName: IStateOrName, base?: IStateOrName): IState { + find(stateOrName: IStateOrName, base?: IStateOrName): State { if (!stateOrName && stateOrName !== "") return undefined; let isStr = isString(stateOrName); let name: string = isStr ? stateOrName : (stateOrName).name; @@ -27,7 +28,7 @@ export default class StateMatcher { resolvePath(name: string, base: IStateOrName) { if (!base) throw new Error(`No reference point given for path '${name}'`); - let baseState: IState = this.find(base); + let baseState: State = this.find(base); let splitName = name.split("."), i = 0, pathLength = splitName.length, current = baseState; diff --git a/src/state/stateQueueManager.ts b/src/state/stateQueueManager.ts index ed5a13c3f..0dd843d1d 100644 --- a/src/state/stateQueueManager.ts +++ b/src/state/stateQueueManager.ts @@ -8,6 +8,7 @@ export default function StateQueueManager(states, builder, $urlRouterProvider, $ let queueManager = extend(this, { register: function(config: IStateDeclaration, pre?: boolean) { // Wrap a new object around the state so we can store our private details easily. + // @TODO: state = new State(extend({}, config, { ... })) let state = inherit(new State(), extend({}, config, { self: config, resolve: config.resolve || {}, @@ -19,6 +20,7 @@ export default function StateQueueManager(states, builder, $urlRouterProvider, $ throw new Error(`State '${state.name}' is already defined`); queue[pre ? "unshift" : "push"](state); + if (queueManager.autoFlush) { queueManager.flush($state); } diff --git a/src/state/targetState.ts b/src/state/targetState.ts index 57c79576c..fe63df2ab 100644 --- a/src/state/targetState.ts +++ b/src/state/targetState.ts @@ -1,4 +1,6 @@ -import {IState, IStateDeclaration, IStateOrName} from "./interface"; +import {State} from "./state"; + +import {IStateDeclaration, IStateOrName} from "./interface"; import {IParamsOrArray} from "../params/interface"; @@ -21,7 +23,7 @@ import {ITransitionOptions} from "../transition/interface"; export default class TargetState { constructor( private _identifier: IStateOrName, - private _definition?: IState, + private _definition?: State, private _params: IParamsOrArray = {}, private _options: ITransitionOptions = {}) { } @@ -38,7 +40,7 @@ export default class TargetState { return this._params; } - $state(): IState { + $state(): State { return this._definition; } diff --git a/src/transition/hookBuilder.ts b/src/transition/hookBuilder.ts index 42ddce50b..4e80f3e62 100644 --- a/src/transition/hookBuilder.ts +++ b/src/transition/hookBuilder.ts @@ -1,19 +1,19 @@ import {IPromise} from "angular"; -import {IInjectable, extend, isPromise, isArray, assertPredicate, unnestR} from "../common/common"; +import {IInjectable, extend, tail, isPromise, isArray, assertPredicate, unnestR} from "../common/common"; import {runtime} from "../common/angular1"; import {ITransitionOptions, ITransitionHookOptions, IHookRegistry, ITreeChanges, IEventHook, ITransitionService} from "./interface"; import TransitionHook from "./transitionHook"; import {Transition} from "./transition"; -import {IState} from "../state/interface"; +import {State} from "../state/state"; -import {ITransPath, ITransNode} from "../path/interface"; +import Node from "../path/node"; interface IToFrom { - to: IState; - from: IState; + to: State; + from: State; } let successErrorOptions: ITransitionHookOptions = { @@ -40,15 +40,15 @@ export default class HookBuilder { treeChanges: ITreeChanges; transitionOptions: ITransitionOptions; - toState: IState; - fromState: IState; + toState: State; + fromState: State; - transitionLocals: {[key: string]: any}; + transitionLocals: { [key: string]: any }; constructor(private $transitions: ITransitionService, private transition: Transition, private baseHookOptions: ITransitionHookOptions) { this.treeChanges = transition.treeChanges(); - this.toState = this.treeChanges.to.last().state; - this.fromState = this.treeChanges.from.last().state; + this.toState = tail(this.treeChanges.to).state; + this.fromState = tail(this.treeChanges.from).state; this.transitionOptions = transition.options(); this.transitionLocals = { $transition$: transition }; } @@ -77,8 +77,8 @@ export default class HookBuilder { * Finds all registered IEventHooks which matched the hookType and toFrom criteria. * A TransitionHook is then built from each IEventHook with the context, locals, and options provided. */ - private _getTransitionHooks(hookType: string, context: (ITransPath|IState), locals = {}, options: ITransitionHookOptions = {}) { - let node = this.treeChanges.to.last(); + private _getTransitionHooks(hookType: string, context: (Node[]|State), locals = {}, options: ITransitionHookOptions = {}) { + let node = tail(this.treeChanges.to); let toFrom: IToFrom = this._toFrom(); options.traceData = { hookType, context }; @@ -95,8 +95,8 @@ export default class HookBuilder { * Finds all registered IEventHooks which matched the hookType and toFrom criteria. * A TransitionHook is then built from each IEventHook with the context, locals, and options provided. */ - private _getNodeHooks(hookType: string, path: ITransPath, toFromFn: (node: ITransNode) => IToFrom, locals: any = {}, options: ITransitionHookOptions = {}) { - const hooksForNode = (node: ITransNode) => { + private _getNodeHooks(hookType: string, path: Node[], toFromFn: (node: Node) => IToFrom, locals: any = {}, options: ITransitionHookOptions = {}) { + const hooksForNode = (node: Node) => { let toFrom = toFromFn(node); options.traceData = { hookType, context: node }; locals.$state$ = node.state; @@ -105,11 +105,11 @@ export default class HookBuilder { return this._matchingHooks(hookType, toFrom).map(transitionHook); }; - return path.nodes().map(hooksForNode); + return path.map(hooksForNode); } /** Given a node and a callback function, builds a TransitionHook */ - buildHook(node: ITransNode, fn: IInjectable, moreLocals?, options: ITransitionHookOptions = {}): TransitionHook { + buildHook(node: Node, fn: IInjectable, moreLocals?, options: ITransitionHookOptions = {}): TransitionHook { let locals = extend({}, this.transitionLocals, moreLocals); let _options = extend({}, this.baseHookOptions, options); diff --git a/src/transition/hookRegistry.ts b/src/transition/hookRegistry.ts index 899c3ac7f..f74c976aa 100644 --- a/src/transition/hookRegistry.ts +++ b/src/transition/hookRegistry.ts @@ -1,6 +1,6 @@ import {IInjectable, extend, val, isString, isFunction, removeFrom} from "../common/common"; -import {IState} from "../state/interface"; +import {State} from "../state/state"; import Glob from "../state/glob"; import {IMatchCriteria, IStateMatch, IEventHook, IHookRegistry, IHookRegistration} from "./interface"; @@ -15,7 +15,7 @@ import {IMatchCriteria, IStateMatch, IEventHook, IHookRegistry, IHookRegistratio * - If a function, matchState calls the function with the state and returns true if the function's result is truthy. * @returns {boolean} */ -export function matchState(state: IState, matchCriteria: (string|IStateMatch)) { +export function matchState(state: State, matchCriteria: (string|IStateMatch)) { let toMatch = isString(matchCriteria) ? [matchCriteria] : matchCriteria; function matchGlobs(_state) { @@ -45,7 +45,7 @@ export class EventHook implements IEventHook { this.priority = options.priority || 0; } - matches(to: IState, from: IState) { + matches(to: State, from: State) { return matchState(to, this.matchCriteria.to) && matchState(from, this.matchCriteria.from); } } diff --git a/src/transition/interface.ts b/src/transition/interface.ts index 1adb379cd..b5871d03b 100644 --- a/src/transition/interface.ts +++ b/src/transition/interface.ts @@ -1,7 +1,7 @@ -import {IStateDeclaration, IState} from "../state/interface"; +import {State} from "../state/state"; +import {IStateDeclaration} from "../state/interface"; import TargetState from "../state/targetState"; - -import {ITransPath} from "../path/interface"; +import Node from "../path/node"; import {IInjectable, Predicate} from "../common/common"; @@ -9,11 +9,11 @@ import {Transition} from "./transition"; export interface ITransitionOptions { location ?: (boolean|string); - relative ?: (string|IStateDeclaration|IState); + relative ?: (string|IStateDeclaration|State); inherit ?: boolean; notify ?: boolean; - reload ?: (boolean|string|IStateDeclaration|IState); - reloadState ?: (IState); + reload ?: (boolean|string|IStateDeclaration|State); + reloadState ?: (State); custom ?: any; previous ?: Transition; current ?: () => Transition; @@ -30,18 +30,18 @@ export interface ITransitionHookOptions { } export interface ITreeChanges { - [key: string]: ITransPath; - from: ITransPath; - to: ITransPath; - retained: ITransPath; - entering: ITransPath; - exiting: ITransPath; + [key: string]: Node[]; + from: Node[]; + to: Node[]; + retained: Node[]; + entering: Node[]; + exiting: Node[]; } export type IErrorHandler = (error: Error) => void; export interface ITransitionService extends IHookRegistry { - create: (fromPath: ITransPath, targetState: TargetState) => Transition; + create: (fromPath: Node[], targetState: TargetState) => Transition; defaultErrorHandler: (handler?: IErrorHandler) => IErrorHandler; } @@ -60,7 +60,7 @@ export interface IHookRegistry { getHooks: IHookGetter; } -export type IStateMatch = Predicate +export type IStateMatch = Predicate export interface IMatchCriteria { to?: (string|IStateMatch); from?: (string|IStateMatch); @@ -69,5 +69,5 @@ export interface IMatchCriteria { export interface IEventHook { callback: IInjectable; priority: number; - matches: (a: IState, b: IState) => boolean; + matches: (a: State, b: State) => boolean; } \ No newline at end of file diff --git a/src/transition/transition.ts b/src/transition/transition.ts index 46c45a810..21c0acd26 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -10,20 +10,24 @@ import HookBuilder from "./hookBuilder"; import TransitionRunner from "./transitionRunner"; import {RejectFactory} from "./rejectFactory"; -import {ITransPath} from "../path/interface"; +import Node from "../path/node"; import PathFactory from "../path/pathFactory"; +import {State} from "../state/state"; import TargetState from "../state/targetState"; -import {IState, IStateDeclaration} from "../state/interface"; +import {IStateDeclaration} from "../state/interface"; -import ParamValues from "../params/paramValues"; +import Param from "../params/param"; import {ViewConfig} from "../view/view"; -import {extend, unnest, omit, isObject, not, prop, toJson, val, abstractKey} from "../common/common"; +import { + map, find, extend, flatten, unnest, tail, forEach, identity, + omit, isObject, not, prop, propEq, toJson, val, abstractKey +} from "../common/common"; let transitionCount = 0, REJECT = new RejectFactory(); -const stateSelf: (_state: IState) => IStateDeclaration = prop("self"); +const stateSelf: (_state: State) => IStateDeclaration = prop("self"); /** * @ngdoc object @@ -59,7 +63,7 @@ export class Transition implements IHookRegistry { onError: IHookRegistration; getHooks: IHookGetter; - constructor(fromPath: ITransPath, targetState: TargetState) { + constructor(fromPath: Node[], targetState: TargetState) { if (!targetState.valid()) { throw new Error(targetState.error()); } @@ -75,11 +79,11 @@ export class Transition implements IHookRegistry { } $from() { - return this._treeChanges.from.last().state; + return tail(this._treeChanges.from).state; } $to() { - return this._treeChanges.to.last().state; + return tail(this._treeChanges.to).state; } /** @@ -140,8 +144,8 @@ export class Transition implements IHookRegistry { * @returns {StateParams} the StateParams object for the transition. */ // TODO - params(pathname: string = "to"): ParamValues { - return this._treeChanges[pathname].last().paramValues; + params(pathname: string = "to"): { [key: string]: any } { + return tail(this._treeChanges[pathname]).values; } /** @@ -181,7 +185,7 @@ export class Transition implements IHookRegistry { * @returns {Array} Returns an array of states that will be entered in this transition. */ entering(): IStateDeclaration[] { - return this._treeChanges.entering.states().map(stateSelf); + return map(this._treeChanges.entering, prop('state')).map(stateSelf); } /** @@ -195,7 +199,7 @@ export class Transition implements IHookRegistry { * @returns {Array} Returns an array of states that will be exited in this transition. */ exiting(): IStateDeclaration[] { - return this._treeChanges.exiting.states().map(stateSelf).reverse(); + return map(this._treeChanges.exiting, prop('state')).map(stateSelf).reverse(); } /** @@ -210,16 +214,16 @@ export class Transition implements IHookRegistry { * will not be exited. */ retained(): IStateDeclaration[] { - return this._treeChanges.retained.states().map(stateSelf); + return map(this._treeChanges.retained, prop('state')).map(stateSelf); } /** * Returns a list of ViewConfig objects for a given path. Returns one ViewConfig for each view in * each state in a named path of the transition's tree changes. Optionally limited to a given state in that path. */ - views(pathname: string = "entering", state?: IState): ViewConfig[] { + views(pathname: string = "entering", state?: State): ViewConfig[] { let path = this._treeChanges[pathname]; - return state ? path.nodeForState(state).views : unnest(path.nodes().map(prop("views"))); + return state ? find(path, propEq('state', state)).views : unnest(path.map(prop("views"))); } treeChanges = () => this._treeChanges; @@ -246,8 +250,8 @@ export class Transition implements IHookRegistry { // add those resolvables to the redirected transition. Allows you to define a resolve at a parent level, wait for // the resolve, then redirect to a child state based on the result, and not have to re-fetch the resolve. let redirectedPath = this.treeChanges().to; - let matching = redirectTo.treeChanges().to.matching(redirectedPath); - matching.nodes().forEach((node, idx) => node.ownResolvables = redirectedPath.nodes()[idx].ownResolvables); + let matching = Node.matching(redirectTo.treeChanges().to, redirectedPath); + matching.forEach((node, idx) => node.resolves = redirectedPath[idx].resolves); return redirectTo; } @@ -265,20 +269,21 @@ export class Transition implements IHookRegistry { */ ignored() { let {to, from} = this._treeChanges; - let [toState, fromState] = [to, from].map((path) => path.last().state); - let [toParams, fromParams] = [to, from].map((path) => path.last().paramValues); - return !this._options.reload && - toState === fromState && - toState.params.$$filter(not(prop('dynamic'))).$$equals(toParams, fromParams); + let [toState, fromState] = [to, from].map(path => tail(path).state); + let [toParams, fromParams] = [to, from].map(path => tail(path).values); + + return ( + !this._options.reload && + toState === fromState && + Param.equals(toState.parameters().filter(not(prop('dynamic'))), toParams, fromParams) + ); } hookBuilder(): HookBuilder { - let baseHookOptions: ITransitionHookOptions = { + return new HookBuilder($transitions, this, { transition: this, current: this._options.current - }; - - return new HookBuilder($transitions, this, baseHookOptions); + }); } run () { @@ -321,10 +326,11 @@ export class Transition implements IHookRegistry { } error() { - let state = this._treeChanges.to.last().state; + let state = this.$to(); + if (state.self[abstractKey]) return `Cannot transition to abstract state '${state.name}'`; - if (!state.params.$$validates(this.params())) + if (!Param.validates(state.parameters(), this.params())) return `Param values not valid for state '${state.name}'`; } @@ -338,7 +344,7 @@ export class Transition implements IHookRegistry { // (X) means the to state is invalid. let id = this.$id, from = isObject(fromStateOrName) ? fromStateOrName.name : fromStateOrName, - fromParams = toJson(avoidEmptyHash(this._treeChanges.from.last().paramValues)), + fromParams = toJson(avoidEmptyHash(tail(this._treeChanges.from).values)), toValid = this.valid() ? "" : "(X) ", to = isObject(toStateOrName) ? toStateOrName.name : toStateOrName, toParams = toJson(avoidEmptyHash(this.params())); diff --git a/src/transition/transitionHook.ts b/src/transition/transitionHook.ts index 62742f907..53e6a0757 100644 --- a/src/transition/transitionHook.ts +++ b/src/transition/transitionHook.ts @@ -3,7 +3,8 @@ import {IInjectable, defaults, extend, noop, filter, not, isFunction, isDefined, import trace from "../common/trace"; import {RejectFactory} from "./rejectFactory"; import {Transition} from "./transition"; -import {IState, IResolveDeclarations} from "../state/interface"; +import {State} from "../state/state"; +import {IResolveDeclarations} from "../state/interface"; import Resolvable from "../resolve/resolvable"; import ResolveContext from "../resolve/resolveContext"; import {ITransitionHookOptions} from "./interface"; @@ -19,7 +20,7 @@ let defaultOptions = { }; export default class TransitionHook { - constructor(private state: IState, + constructor(private state: State, private fn: IInjectable, private locals: any, private resolveContext: ResolveContext, diff --git a/src/transition/transitionService.ts b/src/transition/transitionService.ts index ec1060b80..f82a43e81 100644 --- a/src/transition/transitionService.ts +++ b/src/transition/transitionService.ts @@ -6,7 +6,7 @@ import {Transition} from "./transition"; import TargetState from "../state/targetState"; -import {ITransPath} from "../path/interface"; +import Node from "../path/node"; import {IHookRegistry, ITransitionService, ITransitionOptions, IHookRegistration, IHookGetter} from "./interface"; @@ -58,7 +58,7 @@ class TransitionService implements ITransitionService, IHookRegistry { HookRegistry.mixin(new HookRegistry(), this); } - create(fromPath: ITransPath, targetState: TargetState) { + create(fromPath: Node[], targetState: TargetState) { return new Transition(fromPath, targetState); } } diff --git a/src/url/urlMatcher.ts b/src/url/urlMatcher.ts index af4be53fc..1360e35b5 100644 --- a/src/url/urlMatcher.ts +++ b/src/url/urlMatcher.ts @@ -1,7 +1,9 @@ -import {map, prop, propEq, defaults, extend, inherit, isDefined, isObject, isArray, isString, invoke, unnest, tail, forEach, find, curry} from "../common/common"; -import paramTypes from "../params/paramTypes" -import ParamSet from "../params/paramSet" -import Param from "../params/param" +import { + map, prop, propEq, defaults, extend, inherit, identity, isDefined, isObject, isArray, isString, + invoke, unnest, tail, forEach, find, curry, omit +} from "../common/common"; +import paramTypes from "../params/paramTypes"; +import Param from "../params/param"; interface params { $$validates: (params: string) => Array; @@ -81,10 +83,10 @@ export default class UrlMatcher { static nameValidator: RegExp = /^\w+(-+\w+)*(?:\[\])?$/; private _cache: { path: UrlMatcher[], pattern?: RegExp } = { path: [], pattern: null }; - private _children: UrlMatcher[] = []; - private _params: Array = []; - private _segments: Array = []; - private _compiled: Array = []; + private _children: UrlMatcher[] = []; + private _params: Param[] = []; + private _segments: string[] = []; + private _compiled: string[] = []; public prefix: string; @@ -92,7 +94,8 @@ export default class UrlMatcher { this.config = defaults(this.config, { params: {}, strict: false, - caseInsensitive: false + caseInsensitive: false, + paramMap: identity }); // Find all placeholders and create a compiled pattern, using either classic or curly syntax: @@ -141,7 +144,7 @@ export default class UrlMatcher { if (p.segment.indexOf('?') >= 0) break; // we're into the search part checkParamErrors(p.id); - this._params.push(Param.fromPath(p.id, p.type, p.cfg)); + this._params.push(Param.fromPath(p.id, p.type, this.config.paramMap(p.cfg, false))); this._segments.push(p.segment); patterns.push([p.segment, tail(this._params)]); last = placeholder.lastIndex; @@ -161,7 +164,7 @@ export default class UrlMatcher { while ((m = searchPlaceholder.exec(search))) { p = matchDetails(m, true); checkParamErrors(p.id); - this._params.push(Param.fromSearch(p.id, p.type, p.cfg)); + this._params.push(Param.fromSearch(p.id, p.type, this.config.paramMap(p.cfg, true))); last = placeholder.lastIndex; // check if ?& } @@ -247,7 +250,7 @@ export default class UrlMatcher { const hash: string = search['#'] ? search['#'] : undefined; delete search['#']; - var allParams: Param[] = this.parameters().map(name => this.parameter(name)), + var allParams: Param[] = this.parameters(), pathParams: Param[] = allParams.filter(param => !param.isSearch()), searchParams: Param[] = allParams.filter(param => param.isSearch()), nPath = this._segments.length - 1, @@ -292,16 +295,22 @@ export default class UrlMatcher { * @description * Returns the names of all path and search parameters of this pattern in order of appearance. * - * @returns {Array.} An array of parameter names. Must be treated as read-only. If the + * @returns {Array.} An array of parameter names. Must be treated as read-only. If the * pattern has no parameters, an empty array is returned. */ - parameters(): Array { - return unnest(this._cache.path.concat(this).map(prop('_params'))).map(prop('id')) + parameters(opts: any = {}): Param[] { + if (opts.inherit === false) return this._params; + return unnest(this._cache.path.concat(this).map(prop('_params'))); } - parameter(id: string): Param { - var parent = tail(this._cache.path); - return find(this._params, propEq('id', id)) || (parent && parent.parameter(id)) || null; + parameter(id: string, opts: any = {}): Param { + const parent = tail(this._cache.path); + + return ( + find(this._params, propEq('id', id)) || + (opts.inherit !== false && parent && parent.parameter(id)) || + null + ); } /** @@ -346,7 +355,11 @@ export default class UrlMatcher { * @returns {string} the formatted URL (path and optionally search part). */ format(values = {}) { - var i, search = false, result = this._segments[0]; + var segments: string[] = this._segments, + result: string = segments[0], + search: boolean = false, + params: Param[] = this.parameters(), + parent: UrlMatcher = tail(this._cache.path); if (!this.validates(values)) return null; @@ -354,9 +367,10 @@ export default class UrlMatcher { return encodeURIComponent(str).replace(/-/g, c => `%5C%${c.charCodeAt(0).toString(16).toUpperCase()}`); } - this.parameters().map((name, i) => { - var isPathParam = i < this._segments.length - 1; - var param: Param = this.parameter(name), value = param.value(values[name]); + // TODO: rewrite as reduce over params with result as initial + params.map((param: Param, i) => { + var isPathParam = i < segments.length - 1; + var value = param.value(values[param.id]); var isDefaultValue = param.isDefaultValue(value); var squash = isDefaultValue ? param.squash : false; var encoded = param.type.encode(value); @@ -365,8 +379,8 @@ export default class UrlMatcher { if (encoded == null || (isDefaultValue && squash !== false)) return; if (!isArray(encoded)) encoded = [ encoded]; - encoded = map( encoded, encodeURIComponent).join(`&${name}=`); - result += (search ? '&' : '?') + (`${name}=${encoded}`); + encoded = map( encoded, encodeURIComponent).join(`&${param.id}=`); + result += (search ? '&' : '?') + (`${param.id}=${encoded}`); search = true; return; } @@ -379,11 +393,11 @@ export default class UrlMatcher { if (isArray(encoded)) return map( encoded, encodeDashes).join("-") + segment; if (param.type.raw) return encoded + segment; return encodeURIComponent( encoded) + segment; - })(this._segments[i + 1], result); + })(segments[i + 1], result); }); if (values["#"]) result += "#" + values["#"]; - return result; + return (parent && parent.format(omit(values, params.map(prop('id')))) || '') + result; } } diff --git a/src/url/urlMatcherFactory.ts b/src/url/urlMatcherFactory.ts index 1e0b5b117..3a74e165a 100644 --- a/src/url/urlMatcherFactory.ts +++ b/src/url/urlMatcherFactory.ts @@ -6,7 +6,6 @@ import {forEach, extend, inherit, map, filter, isObject, isDefined, isArray, isS import matcherConfig from "./urlMatcherConfig"; import UrlMatcher from "./urlMatcher"; import Param from "../params/param"; -import ParamSet from "../params/paramSet"; import paramTypes from "../params/paramTypes"; import Type from "../params/type"; @@ -81,7 +80,7 @@ function $UrlMatcherFactory() { * @param {Object} config The config object hash. * @returns {UrlMatcher} The UrlMatcher. */ - this.compile = (pattern, config) => new UrlMatcher(pattern, extend(getDefaultConfig(), config)); + this.compile = (pattern: string, config: { [key: string]: any }) => new UrlMatcher(pattern, extend(getDefaultConfig(), config)); /** * @ngdoc function @@ -224,7 +223,7 @@ function $UrlMatcherFactory() { return this; }; - extend(this, { UrlMatcher, Param, ParamSet }); + extend(this, { UrlMatcher, Param }); } // Register as a provider so it's available to other providers diff --git a/test/resolveSpec.ts b/test/resolveSpec.ts index 00fe47df8..8f0e4b306 100644 --- a/test/resolveSpec.ts +++ b/test/resolveSpec.ts @@ -6,17 +6,16 @@ import * as uiRouter from "../src/ui-router"; import ResolveContext from "../src/resolve/resolveContext" import Resolvable from "../src/resolve/resolvable" -import {IState} from "../src/state/interface" -import {IParamsPath, IResolvePath} from "../src/path/interface" -import Path from "../src/path/path" -import PathFactory from "../src/path/pathFactory" +import {State} from "../src/state/state"; +import Node from "../src/path/node"; +import PathFactory from "../src/path/pathFactory"; import {omit, map, pick, prop, extend, forEach} from "../src/common/common" let module = angular.mock.module; /////////////////////////////////////////////// -var states, statesTree, statesMap: {[key:string]: IState} = {}; +var states, statesTree, statesMap: { [key:string]: State } = {}; var emptyPath; var vals, counts, expectCounts; var asyncCount; @@ -72,16 +71,15 @@ beforeEach(function () { angular.forEach(substates, function (value, key) { thisState.data.children.push(loadStates(thisState, value, key)); }); + thisState = new State(thisState); statesMap[name] = thisState; return thisState; } -// console.log(map(makePath([ "A", "B", "C" ]), function(s) { return s.name; })); }); -function makePath(names: string[]): IResolvePath { - let nodes = map(names, name => ({ state: statesMap[name], ownParamValues: {} })); - let pPath = new Path(nodes).adapt(PathFactory.makeResolveNode); - return PathFactory.bindTransNodesToPath(pPath); +function makePath(names: string[]): Node[] { + let nodes = map(names, name => new Node(statesMap[name], {})); + return PathFactory.bindTransNodesToPath(nodes); } function getResolvedData(pathContext: ResolveContext) { @@ -92,7 +90,7 @@ function getResolvedData(pathContext: ResolveContext) { describe('Resolvables system:', function () { beforeEach(inject(function ($transitions, $injector) { uiRouter.common.angular1.runtime.setRuntimeInjector($injector); - emptyPath = new Path([]); + emptyPath = []; asyncCount = 0; })); @@ -493,9 +491,9 @@ describe('Resolvables system:', function () { expect(asyncCount).toBe(1); let slicedPath = path.slice(0, 2); - expect(slicedPath.nodes().length).toBe(2); - expect(slicedPath.nodes()[0].state).toBe(path.nodes()[0].state); - expect(slicedPath.nodes()[1].state).toBe(path.nodes()[1].state); + expect(slicedPath.length).toBe(2); + expect(slicedPath[0].state).toBe(path[0].state); + expect(slicedPath[1].state).toBe(path[1].state); let path2 = path.concat(makePath([ "L", "M" ])); let ctx2 = new ResolveContext(path2); ctx2.resolvePath({resolvePolicy: "JIT"}).then(function () { @@ -546,7 +544,7 @@ describe("State transitions with resolves", function() { $timeout = _$timeout_; $scope = $rootScope.$new(); uiRouter.common.angular1.runtime.setRuntimeInjector($injector); - emptyPath = new Path([]); + emptyPath = []; asyncCount = 0; $compile(angular.element("
"))($scope); })); diff --git a/test/urlMatcherFactorySpec.js b/test/urlMatcherFactorySpec.js index 14f425b03..06f1efd2f 100644 --- a/test/urlMatcherFactorySpec.js +++ b/test/urlMatcherFactorySpec.js @@ -1,6 +1,5 @@ var module = angular.mock.module; var uiRouter = require("ui-router"); -var ParamSet = uiRouter.params.paramSet.default; var Param = uiRouter.params.param.default; var provide, UrlMatcher; @@ -9,7 +8,6 @@ beforeEach(function() { app.config(function ($urlMatcherFactoryProvider) { provider = $urlMatcherFactoryProvider; UrlMatcher = provider.UrlMatcher; - ParamSet = provider.ParamSet; //Param = provider.Param; }); }); @@ -488,9 +486,11 @@ describe("urlMatcherFactory", function () { var custom = { format: angular.noop, exec: angular.noop, - concat: angular.noop, + append: angular.noop, + isRoot: angular.noop, validates: angular.noop, - parameters: angular.noop + parameters: angular.noop, + parameter: angular.noop }; expect($umf.isMatcher(custom)).toBe(true); }); @@ -858,100 +858,6 @@ describe("urlMatcherFactory", function () { }); }); - describe("ParamSet", function() { - - var params = {}; - beforeEach(function() { - var types = { int: $umf.type("int"), string: $umf.type("string"), any: $umf.type("any") } - params.grandparent = new Param("grandparent", types.int, {}, "path"); - params.parent = new Param("parent", types.string, {}, "path"); - params.child = new Param("child", types.string, {}, "path"); - params.param4 = new Param("param4", types.any, {}, "path"); - }); - - describe(".$$new", function() { - it("should return a new ParamSet, which returns the previous paramset as $$parent()", function() { - var parent = new ParamSet(); - var child = parent.$$new(); - expect(child.$$parent()).toBe(parent); - }); - - it("should return a new ParamSet, which exposes parent params", function() { - var parent = new ParamSet({ parent: params.parent }); - var child = parent.$$new(); - expect(child.parent).toBe(params.parent); - }); - - it("should return a new ParamSet, which exposes ancestor params", function() { - var grandparent = new ParamSet({ grandparent: params.grandparent }); - var parent = grandparent.$$new({ parent: params.parent }); - var child = parent.$$new({ child: params.child }); - - expect(child.grandparent).toBe(params.grandparent); - expect(child.parent).toBe(params.parent); - expect(child.child).toBe(params.child); - }); - }); - - describe(".$$own", function() { - it("should return a new ParamSet which does not expose ancestor Params (only exposes own Params)", function() { - var grandparent = new ParamSet({ grandparent: params.grandparent }); - var parent = grandparent.$$new({ parent: params.parent }); - var child = parent.$$new({ child: params.child }); - - expect(child.grandparent).toBe(params.grandparent); - var own = child.$$own(); - - expect(own.$$keys()).toEqual(["child"]); - expect(own.child).toBe(params.child); - expect(own.parent).toBeUndefined(); - expect(own.grandparent).toBeUndefined(); - }); - }); - - describe(".$$keys", function() { - it("should return keys for current param set", function() { - var ps = new ParamSet(); - expect(ps.$$keys()).toEqual([]); - - ps = new ParamSet({ foo: {}, bar: {}}); - expect(ps.$$keys()).toEqual(['foo', 'bar']); - }); - - it("should return keys for current and ancestor paramset(s)", function () { - var gpa = new ParamSet({grandparent: params.grandparent}); - expect(gpa.$$keys()).toEqual(['grandparent']); - - var pa = gpa.$$new({ parent: params.parent }); - expect(pa.$$keys()).toEqual(['grandparent', 'parent']); - - var child = pa.$$new({ child: params.child }); - expect(child.$$keys()).toEqual(['grandparent', 'parent', 'child']); - }); - }); - - describe(".$$values", function() { - it("should return typed param values for current param set, from a set of input values", function() { - var gpa = new ParamSet({grandparent: params.grandparent}); - var pa = gpa.$$new({ parent: params.parent }); - var child = pa.$$new({ child: params.child }); - var values = { grandparent: "1", parent: 2, child: "3" }; - expect(child.$$values(values)).toEqual({ grandparent: 1, parent: "2", child: "3" }); - }); - }); - - describe(".$$filter", function() { - it("should return a new ParamSet which is a subset of the current param set", function() { - var gpa = new ParamSet({ grandparent: params.grandparent }); - var pa = gpa.$$new({ parent: params.parent }); - var child = pa.$$new({ child: params.child }); - - var subset = child.$$filter(function(param) { return ['parent', 'grandparent'].indexOf(param.id) !== -1; }); - expect(subset.$$keys()).toEqual(['grandparent', 'parent']) - }); - }); - }); - xdescribe("parameter isolation", function() { it("should allow parameters of the same name in different segments", function() { var m = new UrlMatcher('/users/:id').append(new UrlMatcher('/photos/:id')); diff --git a/test/viewSpec.ts b/test/viewSpec.ts index 80aa879ca..8f297d2c9 100644 --- a/test/viewSpec.ts +++ b/test/viewSpec.ts @@ -5,19 +5,18 @@ var module = angular.mock.module; import {inherit, extend, curry} from "../src/common/common"; -import Path from "../src/path/path"; +import Node from "../src/path/node"; import ResolveContext from "../src/resolve/resolveContext"; import PathFactory from "../src/path/pathFactory"; import {ViewConfig} from "../src/view/view"; import StateBuilder from "../src/state/stateBuilder"; import StateMatcher from "../src/state/stateMatcher"; -import {IState} from "../src/state/interface"; import {State} from "../src/state/state"; describe('view', function() { var scope, $compile, $injector, elem, $controllerProvider, $urlMatcherFactoryProvider; - let root: IState, states: {[key: string]: IState}; + let root: State, states: {[key: string]: State}; beforeEach(module('ui.router', function(_$provide_, _$controllerProvider_, _$urlMatcherFactoryProvider_) { _$provide_.factory('foo', function() { @@ -33,7 +32,7 @@ describe('view', function() { self: config, resolve: config.resolve || {} })); - let built: IState = stateBuilder.build(state); + let built: State = stateBuilder.build(state); return _states[built.name] = built; }); @@ -54,8 +53,7 @@ describe('view', function() { let ctx, state; beforeEach(() => { state = register({ name: "foo" }); - var nodes = [root, state].map(_state => ({ state: _state, ownParamValues: {}})); - var path = PathFactory.bindTransNodesToPath( new Path(nodes).adapt(PathFactory.makeResolveNode)); + var path = PathFactory.bindTransNodesToPath([root, state].map(_state => new Node(_state, {}))); ctx = new ResolveContext(path); }); From 7a4a1161ec90d3f8f91355c1680699d0ed12bc05 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sat, 24 Oct 2015 11:45:03 -0500 Subject: [PATCH 17/31] chore(Params): fixing tests: remove private state._params and add public params: { [key: string]: Param } - fetch Params[] dynamic using `values(this.params)` - fix node.parameter() - fix StateBuilder.url (do not expect state.params to be set, b/c 'params' is built after) --- src/common/common.ts | 8 ++++++++ src/path/node.ts | 11 +++++------ src/state/state.ts | 9 ++++----- src/state/stateBuilder.ts | 2 +- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index 83123fc0d..0e04af981 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -287,6 +287,14 @@ export function map(collection: any, callback: any): any { return result; } +/** Given an object, return its enumerable property values */ +export const values: ( (obj: TypedMap) => T[]) = (obj) => Object.keys(obj).map(key => obj[key]); + + /** Reduce function that returns true if all of the values are truthy. */ +export const allTrueR = (memo: boolean, elem) => memo && elem; +/** Reduce function that returns true if any of the values are truthy. */ +export const anyTrueR = (memo: boolean, elem) => memo || elem; + /** Push an object to an array, return the array */ export const push = (arr: any[], obj) => { arr.push(obj); return arr; }; /** Reduce function which un-nests a single level of arrays */ diff --git a/src/path/node.ts b/src/path/node.ts index 609010fff..be391f688 100644 --- a/src/path/node.ts +++ b/src/path/node.ts @@ -1,5 +1,5 @@ /// -import {extend, pick, prop, propEq, pairs, map, find} from "../common/common"; +import {extend, pick, prop, propEq, pairs, map, find, allTrueR} from "../common/common"; import {State} from "../state/state"; import Param from "../params/param"; import Type from "../params/type"; @@ -41,13 +41,12 @@ export default class Node { } parameter(name: string): Param { - return find(this.schema, prop('id')); + return find(this.schema, propEq("id", name)); } - equals(node: Node, keys?: string[]): boolean { - return this.state === node.state && (keys || Object.keys(this.values)).map(key => { - return this.parameter(key).type.equals(this.values[key], node.values[key]); - }).reduce((previous, current) => previous && current, true); + equals(node: Node, keys = this.schema.map(prop('id'))): boolean { + const paramValsEq = key => this.parameter(key).type.equals(this.values[key], node.values[key]); + return this.state === node.state && keys.map(paramValsEq).reduce(allTrueR, true); } static clone(node: Node, update: any = {}) { diff --git a/src/state/state.ts b/src/state/state.ts index 824a314c1..f4556f5df 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -1,6 +1,6 @@ import { extend, defaults, copy, equalForKeys, forEach, find, prop, - propEq, ancestors, noop, isDefined, isObject, isString + propEq, ancestors, noop, isDefined, isObject, isString, values } from "../common/common"; import Queue from "../common/queue"; import {IServiceProviderFactory, IPromise} from "angular"; @@ -53,6 +53,7 @@ export class State { public resolve: IResolveDeclarations; public resolvePolicy: any; public url: UrlMatcher; + public params: { [key: string]: Param }; public views: IViewDeclarations; public self: IStateDeclaration; public navigable: State; @@ -60,8 +61,6 @@ export class State { public data: any; public includes: (name: string) => boolean; - private _params: Param[] = []; - constructor(config?: IStateDeclaration) { extend(this, config); // Object.freeze(this); @@ -120,13 +119,13 @@ export class State { return (this.parent && opts.inherit && this.parent.parameters() || []) .concat(this.url && this.url.parameters({ inherit: false }) || []) - .concat(this._params); + .concat(values(this.params)); } parameter(id: string, opts: any = {}): Param { return ( this.url && this.url.parameter(id, opts) || - find(this._params, propEq('id', id)) || + find(values(this.params), propEq('id', id)) || opts.inherit && this.parent && this.parent.parameter(id) ); } diff --git a/src/state/stateBuilder.ts b/src/state/stateBuilder.ts index abb421735..aa4cab402 100644 --- a/src/state/stateBuilder.ts +++ b/src/state/stateBuilder.ts @@ -28,7 +28,7 @@ export default function StateBuilder(root, matcher, $urlMatcherFactoryProvider) url: function(state) { const parsed = parseUrl(state.url), parent = state.parent; const url = !parsed ? state.url : $urlMatcherFactoryProvider.compile(parsed.val, { - params: state.params, + params: state.params || {}, paramMap: function(paramConfig, isSearch) { if (state.reloadOnSearch === false && isSearch) paramConfig = extend(paramConfig || {}, { dynamic: true }); return paramConfig; From 83bd52a389dfee07e0d6977d7c70c7201be5ee05 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sat, 24 Oct 2015 13:35:08 -0500 Subject: [PATCH 18/31] test(Transition): Switch to Typescript and fix parts of the transition test harness --- test/{transitionSpec.js => transitionSpec.ts} | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) rename test/{transitionSpec.js => transitionSpec.ts} (94%) diff --git a/test/transitionSpec.js b/test/transitionSpec.ts similarity index 94% rename from test/transitionSpec.js rename to test/transitionSpec.ts index 6b5fcc99c..9af744d2b 100644 --- a/test/transitionSpec.js +++ b/test/transitionSpec.ts @@ -1,26 +1,20 @@ +import Node from "../src/path/node"; var module = angular.mock.module; -var uiRouter = require("ui-router"); -var common = uiRouter.common.common; -var RejectType = uiRouter.transition.rejectFactory.RejectType; -var extend = common.extend, - forEach = common.forEach, - map = common.map, - omit = common.omit, - pick = common.pick, - pluck = common.pluck; -var PathFactory = uiRouter.path.pathFactory.default; -var state = uiRouter.state; -var StateMatcher = state.stateMatcher.default; -var StateBuilder = state.stateBuilder.default; -var TargetState = state.targetState.default; -var StateQueueManager = state.stateQueueManager.default; -var TransitionRejection = uiRouter.transition.rejectFactory.TransitionRejection; +import uiRouter from "../src/ui-router"; +import { RejectType } from "../src/transition/rejectFactory"; +import { extend, forEach, map, omit, pick, pluck } from "../src/common/common"; +import PathFactory from "../src/path/pathFactory"; +import StateMatcher from "../src/state/stateMatcher"; +import StateBuilder from "../src/state/stateBuilder"; +import TargetState from "../src/state/targetState"; +import StateQueueManager from "../src/state/stateQueueManager"; +import {TransitionRejection} from "../src/transition/rejectFactory"; describe('transition', function () { var transitionProvider, matcher, pathFactory, statesMap, queue; - var targetState = function(identifier, params, options) { + var targetState = function(identifier, params = {}, options?) { options = options || {}; var stateDefinition = matcher.find(identifier, options.relative); return new TargetState(identifier, stateDefinition, params, options); @@ -52,7 +46,7 @@ describe('transition', function () { matcher = new StateMatcher(statesMap = {}); pathFactory = new PathFactory(function() { return root; }); var builder = new StateBuilder(function() { return root; }, matcher, $urlMatcherFactoryProvider); - queue = new StateQueueManager(statesMap, builder, { when: function() {} }); + queue = new StateQueueManager(statesMap, builder, { when: function() {} }, null); var root = queue.register({ name: '', url: '^', views: null, 'abstract': true}); root.navigable = null; @@ -79,15 +73,15 @@ describe('transition', function () { matcher = new StateMatcher(statesMap); queue.flush($state); makeTransition = function makeTransition(from, to, options) { - var paramsPath = PathFactory.makeParamsPath(targetState(from)); - var fromPath = PathFactory.bindTransNodesToPath(paramsPath.adapt(PathFactory.makeResolveNode)); + let fromState = targetState(from).$state(); + let fromPath = fromState.path.map(state => new Node(state)); return $transitions.create(fromPath, targetState(to, null, options)); }; })); describe('provider', function() { describe('async event hooks:', function() { - function PromiseResult(promise) { + function PromiseResult(promise?) { var self = this, _promise; var resolve, reject, complete; @@ -280,7 +274,7 @@ describe('transition', function () { })); it('should be called if any part of the transition fails.', inject(function($transitions, $q) { - transitionProvider.onEnter({ from: "A", to: "C" }, function($transition$) { throw new Erorr("oops!"); }); + transitionProvider.onEnter({ from: "A", to: "C" }, function($transition$) { throw new Error("oops!"); }); transitionProvider.onError({ from: "*", to: "*" }, function($transition$) { states.push($transition$.to().name); }); var states = []; @@ -289,7 +283,7 @@ describe('transition', function () { })); it('should be called for only handlers matching the transition.', inject(function($transitions, $q) { - transitionProvider.onEnter({ from: "A", to: "C" }, function($transition$) { throw new Erorr("oops!"); }); + transitionProvider.onEnter({ from: "A", to: "C" }, function($transition$) { throw new Error("oops!"); }); transitionProvider.onError({ from: "*", to: "*" }, function($transition$) { hooks.push("splatsplat"); }); transitionProvider.onError({ from: "A", to: "C" }, function($transition$) { hooks.push("AC"); }); transitionProvider.onError({ from: "A", to: "D" }, function($transition$) { hooks.push("AD"); }); @@ -440,7 +434,7 @@ describe('transition', function () { })); it('should not include already entered elements', inject(function($transitions) { - t = makeTransition("B", "D"); + let t = makeTransition("B", "D"); expect(pluck(t.entering(), 'name')).toEqual([ "C", "D" ]); })); }); From 701303552a5f258143375cca3cc2c42fbb9f7238 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sat, 24 Oct 2015 14:02:30 -0500 Subject: [PATCH 19/31] test(Resolve): fix resolve test harness --- test/resolveSpec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/resolveSpec.ts b/test/resolveSpec.ts index 8f0e4b306..19a3b6cec 100644 --- a/test/resolveSpec.ts +++ b/test/resolveSpec.ts @@ -11,6 +11,7 @@ import Node from "../src/path/node"; import PathFactory from "../src/path/pathFactory"; import {omit, map, pick, prop, extend, forEach} from "../src/common/common" +import {IStateDeclaration} from "../src/state/interface"; let module = angular.mock.module; /////////////////////////////////////////////// @@ -66,6 +67,7 @@ beforeEach(function () { thisState.template = thisState.template || "empty"; thisState.name = name; thisState.parent = parent.name; + thisState.params = {}; thisState.data = { children: [] }; angular.forEach(substates, function (value, key) { @@ -78,7 +80,7 @@ beforeEach(function () { }); function makePath(names: string[]): Node[] { - let nodes = map(names, name => new Node(statesMap[name], {})); + let nodes = map(names, name => new Node(statesMap[name])); return PathFactory.bindTransNodesToPath(nodes); } @@ -526,7 +528,7 @@ describe("State transitions with resolves", function() { } } - angular.forEach(stateDefs, function(state, key) { + angular.forEach(stateDefs, function(state: IStateDeclaration, key) { if (!key) return; state.template = "
state"+key; state.controllerProvider = controllerProvider(state); From eaa24362b1c5650c6317f1bc9311a487fc7f503c Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sat, 24 Oct 2015 19:13:54 -0500 Subject: [PATCH 20/31] fix(Transition): Transition.params() should reduce the values from each node --- src/common/common.ts | 3 +++ src/common/trace.ts | 20 ++++++++++---------- src/transition/transition.ts | 4 ++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index 0e04af981..3cd3d5b58 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -187,6 +187,9 @@ export function merge(dst, ...objs: Object[]) { return dst; } +/** Reduce function that merges each element of the list into a single object, using angular.extend */ +export const mergeR = (memo, item) => extend(memo, item); + /** * Finds the common ancestor path between two states. * diff --git a/src/common/trace.ts b/src/common/trace.ts index 5b89be7d8..6f5da6add 100644 --- a/src/common/trace.ts +++ b/src/common/trace.ts @@ -26,17 +26,17 @@ function normalizedCat(input: Category): string { return isNumber(input) ? Category[input] : Category[Category[input]]; } -let format = pattern([ - [not(isDefined), val("undefined")], - [isNull, val("null")], - [isPromise, promiseToString], - [is(Transition), invoke("toString")], - [is(Resolvable), invoke("toString")], - [isInjectable, functionToString], - [val(true), identity] -]); - function stringify(o) { + let format = pattern([ + [not(isDefined), val("undefined")], + [isNull, val("null")], + [isPromise, promiseToString], + [is(Transition), invoke("toString")], + [is(Resolvable), invoke("toString")], + [isInjectable, functionToString], + [val(true), identity] + ]); + return JSON.stringify(o, (key, val) => format(val)).replace(/\\"/g, '"'); } diff --git a/src/transition/transition.ts b/src/transition/transition.ts index 21c0acd26..5a7b9cce5 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -22,7 +22,7 @@ import Param from "../params/param"; import {ViewConfig} from "../view/view"; import { - map, find, extend, flatten, unnest, tail, forEach, identity, + map, find, extend, mergeR, flatten, unnest, tail, forEach, identity, omit, isObject, not, prop, propEq, toJson, val, abstractKey } from "../common/common"; @@ -145,7 +145,7 @@ export class Transition implements IHookRegistry { */ // TODO params(pathname: string = "to"): { [key: string]: any } { - return tail(this._treeChanges[pathname]).values; + return this._treeChanges[pathname].map(prop("values")).reduce(mergeR, {}); } /** From 6935551afd28bf0e046e4638a223ff2547036072 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sat, 24 Oct 2015 20:14:02 -0500 Subject: [PATCH 21/31] fix(TargetState): default 'null' params to empty obj {} fix(PathFactory): reduce all path params to obj when creating TargetState fix(Node): normalize param values using Param.type when creating Node --- src/path/node.ts | 30 ++++++++++++------------------ src/path/pathFactory.ts | 12 ++---------- src/state/targetState.ts | 8 ++++++-- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/src/path/node.ts b/src/path/node.ts index be391f688..6a1c3f552 100644 --- a/src/path/node.ts +++ b/src/path/node.ts @@ -1,5 +1,5 @@ /// -import {extend, pick, prop, propEq, pairs, map, find, allTrueR} from "../common/common"; +import {extend, pick, prop, propEq, pairs, applyPairs, map, find, allTrueR, values} from "../common/common"; import {State} from "../state/state"; import Param from "../params/param"; import Type from "../params/type"; @@ -20,24 +20,18 @@ export default class Node { // Possibly extract this logic into an intermediary object that maps states to nodes constructor(public state: State, params: IRawParams, resolves: any = {}) { - const schema: Param[] = state.parameters({ inherit: false }); - // schema = keys.map(key => [key, state.parameter(key)]).reduce(applyPairs, {}); - // Object.freeze(extend(this, { ... })) - extend(this, { - state, - schema, - values: pick(params, schema.map(prop('id'))), - resolves: map( - extend(state.resolve || {}, resolves), - (fn: Function, name: string) => new Resolvable(name, fn, state) - ), - views: pairs(state.views || {}).map(([rawViewName, viewDeclarationObj]): ViewConfig => { - return new ViewConfig({ - rawViewName, viewDeclarationObj, context: state, params - }); - }) - }); + this.schema = state.parameters({ inherit: false }); + + const normalizeParamVal = (paramCfg: Param) => [ paramCfg.id, paramCfg.type.$normalize(params[paramCfg.id]) ]; + this.values = this.schema.reduce((memo, pCfg) => applyPairs(memo, normalizeParamVal(pCfg)), {}); + + let resolveCfg = extend({}, state.resolve, resolves); + this.resolves = map(resolveCfg, (fn: Function, name: string) => new Resolvable(name, fn, state)); + + const makeViewConfig = (viewDeclarationObj, rawViewName) => + new ViewConfig({ rawViewName, viewDeclarationObj, context: state, params}); + this.views = values(map(state.views, makeViewConfig)); } parameter(name: string): Param { diff --git a/src/path/pathFactory.ts b/src/path/pathFactory.ts index 78e57f2fe..e171e345f 100644 --- a/src/path/pathFactory.ts +++ b/src/path/pathFactory.ts @@ -1,4 +1,4 @@ -import {map, extend, find, pairs, prop, propEq, pick, omit, not, curry, tail, applyPairs} from "../common/common"; +import {map, extend, find, pairs, prop, propEq, pick, omit, not, curry, tail, applyPairs, mergeR} from "../common/common"; import {IRawParams} from "../params/interface"; @@ -22,18 +22,10 @@ export default class PathFactory { constructor() { } - /** Given a TargetState, create a Node[] */ - static makeParamsPath(ref: TargetState): Node[] { - let states = ref ? ref.$state().path : []; - let params = ref ? ref.params() : {}; - const toParamsNodeFn: (State) => Node = PathFactory.makeParamsNode(params); - return states.map(toParamsNodeFn); - } - /** Given a Node[], create an TargetState */ static makeTargetState(path: Node[]): TargetState { let state = tail(path).state; - return new TargetState(state, state, /*new ParamValues(*/[path]/*)*/); + return new TargetState(state, state, path.map(prop("values")).reduce(mergeR, {})); } /* Given params and a state, creates an Node */ diff --git a/src/state/targetState.ts b/src/state/targetState.ts index fe63df2ab..38a77f81f 100644 --- a/src/state/targetState.ts +++ b/src/state/targetState.ts @@ -21,11 +21,15 @@ import {ITransitionOptions} from "../transition/interface"; * @param {ITransitionOptions} _options Transition options. */ export default class TargetState { + private _params: IParamsOrArray; + constructor( private _identifier: IStateOrName, private _definition?: State, - private _params: IParamsOrArray = {}, - private _options: ITransitionOptions = {}) { + _params: IParamsOrArray = {}, + private _options: ITransitionOptions = {} + ) { + this._params = _params || {}; } name() { From 5487fa4c1def48d2f8567a1e3ecf9a3ad1a584f5 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sat, 24 Oct 2015 20:37:18 -0500 Subject: [PATCH 22/31] fix(Node): use Param.value to apply default values (not Param.type.$normalize) --- src/path/node.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path/node.ts b/src/path/node.ts index 6a1c3f552..5b3fcc49a 100644 --- a/src/path/node.ts +++ b/src/path/node.ts @@ -23,8 +23,8 @@ export default class Node { // Object.freeze(extend(this, { ... })) this.schema = state.parameters({ inherit: false }); - const normalizeParamVal = (paramCfg: Param) => [ paramCfg.id, paramCfg.type.$normalize(params[paramCfg.id]) ]; - this.values = this.schema.reduce((memo, pCfg) => applyPairs(memo, normalizeParamVal(pCfg)), {}); + const getParamVal = (paramDef: Param) => [ paramDef.id, paramDef.value(params[paramDef.id]) ]; + this.values = this.schema.reduce((memo, pDef) => applyPairs(memo, getParamVal(pDef)), {}); let resolveCfg = extend({}, state.resolve, resolves); this.resolves = map(resolveCfg, (fn: Function, name: string) => new Resolvable(name, fn, state)); From 1cd871aff4597a8399730e8d7a1a47bd2d95b1d6 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sun, 25 Oct 2015 11:56:12 -0500 Subject: [PATCH 23/31] fix(UrlMatcher): Use entire UrlMatcher[] path to match URLs, but map over UrlMatcher[] and concat to format a URL --- src/url/urlMatcher.ts | 20 ++++++++------------ test/urlMatcherFactorySpec.js | 8 +++++--- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/url/urlMatcher.ts b/src/url/urlMatcher.ts index 1360e35b5..3ce2d2349 100644 --- a/src/url/urlMatcher.ts +++ b/src/url/urlMatcher.ts @@ -1,6 +1,6 @@ import { map, prop, propEq, defaults, extend, inherit, identity, isDefined, isObject, isArray, isString, - invoke, unnest, tail, forEach, find, curry, omit + invoke, unnest, tail, forEach, find, curry, omit, pairs, allTrueR } from "../common/common"; import paramTypes from "../params/paramTypes"; import Param from "../params/param"; @@ -253,10 +253,11 @@ export default class UrlMatcher { var allParams: Param[] = this.parameters(), pathParams: Param[] = allParams.filter(param => !param.isSearch()), searchParams: Param[] = allParams.filter(param => param.isSearch()), - nPath = this._segments.length - 1, + nPathSegments = this._cache.path.concat(this).map(urlm => urlm._segments.length - 1).reduce((a, x) => a + x), values = {}; - if (nPath !== match.length - 1) throw new Error(`Unbalanced capture group in route '${this.pattern}'`); + if (nPathSegments !== match.length - 1) + throw new Error(`Unbalanced capture group in route '${this.pattern}'`); function decodePathArray(string: string) { const reverseString = (str: string) => str.split("").reverse().join(""); @@ -267,7 +268,7 @@ export default class UrlMatcher { return map(allReversed, unquoteDashes).reverse(); } - for (var i = 0; i < nPath; i++) { + for (var i = 0; i < nPathSegments; i++) { var param: Param = pathParams[i]; var value: (any|any[]) = match[i + 1]; @@ -326,13 +327,8 @@ export default class UrlMatcher { * @returns {boolean} Returns `true` if `params` validates, otherwise `false`. */ validates(params): boolean { - var result = true; - - map(params, (val, key) => { - var param = this.parameter( key); - if (param) result = result && param.validates(val); - }); - return result; + const validParamVal = (param: Param, val) => !param || param.validates(val); + return pairs(params).map(([key, val]) => validParamVal(this.parameter(key), val)).reduce(allTrueR, true); } /** @@ -358,7 +354,7 @@ export default class UrlMatcher { var segments: string[] = this._segments, result: string = segments[0], search: boolean = false, - params: Param[] = this.parameters(), + params: Param[] = this.parameters({inherit: false}), parent: UrlMatcher = tail(this._cache.path); if (!this.validates(values)) return null; diff --git a/test/urlMatcherFactorySpec.js b/test/urlMatcherFactorySpec.js index 06f1efd2f..20878c955 100644 --- a/test/urlMatcherFactorySpec.js +++ b/test/urlMatcherFactorySpec.js @@ -1,6 +1,8 @@ var module = angular.mock.module; var uiRouter = require("ui-router"); var Param = uiRouter.params.param.default; +var common = uiRouter.common.common; +var prop = common.prop; var provide, UrlMatcher; beforeEach(function() { @@ -69,7 +71,7 @@ describe("UrlMatcher", function () { it("should parse parameter placeholders", function () { var matcher = new UrlMatcher('/users/:id/details/{type}/{repeat:[0-9]+}?from&to'); - expect(matcher.parameters()).toEqual(['id', 'type', 'repeat', 'from', 'to']); + expect(matcher.parameters().map(prop('id'))).toEqual(['id', 'type', 'repeat', 'from', 'to']); }); it("should encode and decode duplicate query string values as array", function () { @@ -87,7 +89,7 @@ describe("UrlMatcher", function () { describe("snake-case parameters", function() { it("should match if properly formatted", function() { var matcher = new UrlMatcher('/users/?from&to&snake-case&snake-case-triple'); - expect(matcher.parameters()).toEqual(['from', 'to', 'snake-case', 'snake-case-triple']); + expect(matcher.parameters().map(prop('id'))).toEqual(['from', 'to', 'snake-case', 'snake-case-triple']); }); it("should not match if invalid", function() { @@ -179,7 +181,7 @@ describe("UrlMatcher", function () { it("should append matchers", function () { var matcher = new UrlMatcher('/users/:id/details/{type}?from').append(new UrlMatcher('/{repeat:[0-9]+}?to')); var params = matcher.parameters(); - expect(params).toEqual(['id', 'type', 'from', 'repeat', 'to']); + expect(params.map(prop('id'))).toEqual(['id', 'type', 'from', 'repeat', 'to']); }); it("should return a new matcher", function () { From d5ff3a88be70a73e5a86661cf365c8b09329fa44 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sun, 25 Oct 2015 12:18:32 -0500 Subject: [PATCH 24/31] fix(UrlMatcher): Fixed handling of url fragment (hash) --- src/url/urlMatcher.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/url/urlMatcher.ts b/src/url/urlMatcher.ts index 3ce2d2349..f6193e857 100644 --- a/src/url/urlMatcher.ts +++ b/src/url/urlMatcher.ts @@ -231,10 +231,11 @@ export default class UrlMatcher { * * @param {string} path The URL path to match, e.g. `$location.path()`. * @param {Object} search URL search parameters, e.g. `$location.search()`. + * @param {string} hash URL hash e.g. `$location.hash()`. * @param {Object} options * @returns {Object} The captured parameter values. */ - exec(path: string, search: any = {}, options: any = {}) { + exec(path: string, search: any = {}, hash?: string, options: any = {}) { var match = memoizeTo(this._cache, 'pattern', () => { return new RegExp([ '^', @@ -246,9 +247,7 @@ export default class UrlMatcher { if (!match) return null; - options = defaults(options, { isolate: false }); - const hash: string = search['#'] ? search['#'] : undefined; - delete search['#']; + //options = defaults(options, { isolate: false }); var allParams: Param[] = this.parameters(), pathParams: Param[] = allParams.filter(param => !param.isSearch()), @@ -394,6 +393,7 @@ export default class UrlMatcher { if (values["#"]) result += "#" + values["#"]; - return (parent && parent.format(omit(values, params.map(prop('id')))) || '') + result; + var processedParams = ['#'].concat(params.map(prop('id'))); + return (parent && parent.format(omit(values, processedParams)) || '') + result; } } From 165aaed7fb82e66be1a4a029d4b287dd3bbfe691 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sun, 25 Oct 2015 13:44:05 -0500 Subject: [PATCH 25/31] fix(StateBuilder): include url and non-url params in state.params - Remove overload of applyPairs (only allow [key, value] tuple) --- src/common/common.ts | 29 ++++++++--------------------- src/state/stateBuilder.ts | 10 ++++++---- src/state/stateEvents.ts | 2 +- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index 3cd3d5b58..3e03a9c51 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -336,21 +336,10 @@ export const paired = (left: any[], right: any[], mapFn: Function = identity) => left.map((lval, idx) => [ mapFn(lval), mapFn(right[idx]) ]); /** - * Sets a key/val pair on an object, then returns the object. + * Reduce function which builds an object from an array of [key, value] pairs. + * Each iteration sets the key/val pair on the memo object, then returns the memo for the next iteration. * - * Use as a reduce function for an array of key/val pairs - * - * Given: - * var keys = [ "fookey", "barkey" ] - * var pairsToObj = keys.reduce((memo, key) => applyPairs(memo, key, true), {}) - * Then: - * true === angular.equals(pairsToObj, { fookey: true, barkey: true }) - */ -export function applyPairs(obj: TypedMap, arrayOrKey: string, val: any); -/** - * Sets a key/val pair on an object, then returns the object. - * - * Use as a reduce function for an array of key/val pairs + * Each keyValueTuple should be an array with values [ key: string, value: any ] * * Given: * var pairs = [ ["fookey", "fooval"], ["barkey","barval"] ] @@ -359,14 +348,12 @@ export function applyPairs(obj: TypedMap, arrayOrKey: string, val: any); * Then: * true === angular.equals(pairsToObj, { fookey: "fooval", barkey: "barval" }) */ -export function applyPairs(obj: TypedMap, arrayOrKey: any[]); -export function applyPairs(obj: TypedMap, arrayOrKey: (string|any[]), val?: any) { - let key; - if (isDefined(val)) key = arrayOrKey; - if (isArray(arrayOrKey)) [key, val] = arrayOrKey; +export function applyPairs(memo: TypedMap, keyValTuple: any[]) { + let key, value; + if (isArray(keyValTuple)) [key, value] = keyValTuple; if (!isString(key)) throw new Error("invalid parameters to applyPairs"); - obj[key] = val; - return obj; + memo[key] = value; + return memo; } // Checks if a value is injectable diff --git a/src/state/stateBuilder.ts b/src/state/stateBuilder.ts index aa4cab402..58de5295a 100644 --- a/src/state/stateBuilder.ts +++ b/src/state/stateBuilder.ts @@ -1,4 +1,4 @@ -import {map, noop, extend, pick, prop, omit, isArray, isDefined, isFunction, isString, forEach} from "../common/common"; +import {map, noop, extend, pick, omit, values, applyPairs, prop, isArray, isDefined, isFunction, isString, forEach} from "../common/common"; import Param from "../params/param"; const parseUrl = (url: string): any => { @@ -45,9 +45,11 @@ export default function StateBuilder(root, matcher, $urlMatcherFactoryProvider) return (state !== root()) && state.url ? state : (state.parent ? state.parent.navigable : null); }, - params: function(state) { - const keys = state.url && state.url.parameters({ inherit: false }).map(prop('id')) || []; - return map(omit(state.params || {}, keys), (config: any, id: string) => Param.fromConfig(id, null, config)); + params: function(state): { [key: string]: Param } { + const makeConfigParam = (config: any, id: string) => Param.fromConfig(id, null, config); + let urlParams: Param[] = (state.url && state.url.parameters({ inherit: false })) || []; + let nonUrlParams: Param[] = values(map(omit(state.params || {}, urlParams.map(prop('id'))), makeConfigParam)); + return urlParams.concat(nonUrlParams).map(p => [p.id, p]).reduce(applyPairs, {}); }, // If there is no explicit multi-view configuration, make one up so we don't have diff --git a/src/state/stateEvents.ts b/src/state/stateEvents.ts index 5c9378978..a9e04c6ef 100644 --- a/src/state/stateEvents.ts +++ b/src/state/stateEvents.ts @@ -179,7 +179,7 @@ function $StateEventsProvider($stateProvider: IStateProvider) { let runtime = false; let allEvents = [ '$stateChangeStart', '$stateNotFound', '$stateChangeSuccess', '$stateChangeError' ]; - let enabledStateEvents: IEventsToggle = allEvents.reduce((memo, key) => applyPairs(memo, key, false), {}); + let enabledStateEvents: IEventsToggle = allEvents.map(e => [e, false]).reduce(applyPairs, {}); function assertNotRuntime() { if (runtime) throw new Error("Cannot enable events at runtime (use $stateEventsProvider"); From 735571d7d6c281c733c09590dcda6cd408e21daa Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sun, 25 Oct 2015 14:35:20 -0500 Subject: [PATCH 26/31] test(transition): fix makeTransition to bind resolveContext to the fromPath --- test/transitionSpec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/transitionSpec.ts b/test/transitionSpec.ts index 9af744d2b..e0ec668f5 100644 --- a/test/transitionSpec.ts +++ b/test/transitionSpec.ts @@ -74,7 +74,7 @@ describe('transition', function () { queue.flush($state); makeTransition = function makeTransition(from, to, options) { let fromState = targetState(from).$state(); - let fromPath = fromState.path.map(state => new Node(state)); + let fromPath = PathFactory.bindTransNodesToPath(fromState.path.map(state => new Node(state))); return $transitions.create(fromPath, targetState(to, null, options)); }; })); From 470e7e6bdbe78917612c4be28455d467553478b3 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sun, 25 Oct 2015 14:38:46 -0500 Subject: [PATCH 27/31] fix(UrlMatcher): allow validates() to take empty params obj --- src/url/urlMatcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/url/urlMatcher.ts b/src/url/urlMatcher.ts index f6193e857..528be2410 100644 --- a/src/url/urlMatcher.ts +++ b/src/url/urlMatcher.ts @@ -327,7 +327,7 @@ export default class UrlMatcher { */ validates(params): boolean { const validParamVal = (param: Param, val) => !param || param.validates(val); - return pairs(params).map(([key, val]) => validParamVal(this.parameter(key), val)).reduce(allTrueR, true); + return pairs(params || {}).map(([key, val]) => validParamVal(this.parameter(key), val)).reduce(allTrueR, true); } /** From eba85e27ae6732e7817be33f4d4530ddad62b4e0 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sun, 25 Oct 2015 15:28:14 -0500 Subject: [PATCH 28/31] fix(Node): Node.clone() retains the original node's resolvable state --- src/path/node.ts | 5 ++--- src/path/pathFactory.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/path/node.ts b/src/path/node.ts index 5b3fcc49a..88fd87002 100644 --- a/src/path/node.ts +++ b/src/path/node.ts @@ -26,8 +26,7 @@ export default class Node { const getParamVal = (paramDef: Param) => [ paramDef.id, paramDef.value(params[paramDef.id]) ]; this.values = this.schema.reduce((memo, pDef) => applyPairs(memo, getParamVal(pDef)), {}); - let resolveCfg = extend({}, state.resolve, resolves); - this.resolves = map(resolveCfg, (fn: Function, name: string) => new Resolvable(name, fn, state)); + this.resolves = extend(map(state.resolve, (fn: Function, name: string) => new Resolvable(name, fn, state)), resolves); const makeViewConfig = (viewDeclarationObj, rawViewName) => new ViewConfig({ rawViewName, viewDeclarationObj, context: state, params}); @@ -44,7 +43,7 @@ export default class Node { } static clone(node: Node, update: any = {}) { - return new Node(node.state, update.params || node.values, update.resolves || map(node.resolves, prop('resolveFn'))); + return new Node(node.state, (update.values || node.values), (update.resolves || node.resolves)); } /** diff --git a/src/path/pathFactory.ts b/src/path/pathFactory.ts index e171e345f..5e4d737a8 100644 --- a/src/path/pathFactory.ts +++ b/src/path/pathFactory.ts @@ -113,7 +113,7 @@ export default class PathFactory { /** Given a retained node, return a new node which uses the to node's param values */ function applyToParams(retainedNode: Node, idx: number): Node { - return Node.clone(retainedNode, { params: toPath[idx].values }); + return Node.clone(retainedNode, { values: toPath[idx].values }); } let from: Node[], retained: Node[], exiting: Node[], entering: Node[], to: Node[]; From d2e7ffb2a9fbbf5283edae94c164ea2f37d2bdd8 Mon Sep 17 00:00:00 2001 From: Nate Abele Date: Mon, 26 Oct 2015 01:40:08 -0400 Subject: [PATCH 29/31] test(StateBuilder): match tests to new interfaces --- test/stateSpec.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/test/stateSpec.js b/test/stateSpec.js index 49a5ed881..4c08f1b2f 100644 --- a/test/stateSpec.js +++ b/test/stateSpec.js @@ -131,19 +131,26 @@ describe('state helpers', function() { }); it('should compile a UrlMatcher for ^ URLs', function() { - var url = {}; + var url = new UrlMatcher('/'); spyOn(urlMatcherFactoryProvider, 'compile').and.returnValue(url); + spyOn(urlMatcherFactoryProvider, 'isMatcher').and.returnValue(true); expect(builder.builder('url')({ url: "^/foo" })).toBe(url); - expect(urlMatcherFactoryProvider.compile).toHaveBeenCalledWith("/foo", { params: {} }); + expect(urlMatcherFactoryProvider.compile).toHaveBeenCalledWith("/foo", { + params: {}, + paramMap: jasmine.any(Function) + }); + expect(urlMatcherFactoryProvider.isMatcher).toHaveBeenCalledWith(url); }); it('should concatenate URLs from root', function() { - root = { url: { concat: function() {} } }, url = {}; - spyOn(root.url, 'concat').and.returnValue(url); + root = { url: { append: function() {} } }, url = {}; + spyOn(root.url, 'append').and.returnValue(url); + spyOn(urlMatcherFactoryProvider, 'isMatcher').and.returnValue(true); + spyOn(urlMatcherFactoryProvider, 'compile').and.returnValue(url); expect(builder.builder('url')({ url: "/foo" })).toBe(url); - expect(root.url.concat).toHaveBeenCalledWith("/foo", { params: {} }); + expect(root.url.append).toHaveBeenCalledWith(url); }); it('should pass through empty URLs', function() { @@ -151,10 +158,12 @@ describe('state helpers', function() { }); it('should pass through custom UrlMatchers', function() { - var url = ["!"]; + var url = new UrlMatcher("/"); spyOn(urlMatcherFactoryProvider, 'isMatcher').and.returnValue(true); + spyOn(root.url, 'append').and.returnValue(url); expect(builder.builder('url')({ url: url })).toBe(url); expect(urlMatcherFactoryProvider.isMatcher).toHaveBeenCalledWith(url); + expect(root.url.append).toHaveBeenCalledWith(url); }); it('should throw on invalid UrlMatchers', function() { From e5485ad0c3b132921f76e81f6c9f386adb5efa31 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Mon, 26 Oct 2015 11:22:30 -0500 Subject: [PATCH 30/31] fix(Transition): reimplement Transition.ignored() using path-based paramvalues --- src/common/common.ts | 22 +++++++++++++--------- src/transition/transition.ts | 17 ++++++++--------- test/stateDirectivesSpec.js | 2 ++ 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index 3e03a9c51..05ee83b89 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -298,12 +298,12 @@ export const allTrueR = (memo: boolean, elem) => memo && elem; /** Reduce function that returns true if any of the values are truthy. */ export const anyTrueR = (memo: boolean, elem) => memo || elem; -/** Push an object to an array, return the array */ -export const push = (arr: any[], obj) => { arr.push(obj); return arr; }; +/** Reduce function that pushes an object to an array, then returns the array */ +export const pushR = (arr: any[], obj) => { arr.push(obj); return arr; }; /** Reduce function which un-nests a single level of arrays */ export const unnestR = (memo: any[], elem) => memo.concat(elem); /** Reduce function which recursively un-nests all arrays */ -export const flattenR = (memo: any[], elem) => isArray(elem) ? memo.concat(elem.reduce(flattenR, [])) : push(memo, elem); +export const flattenR = (memo: any[], elem) => isArray(elem) ? memo.concat(elem.reduce(flattenR, [])) : pushR(memo, elem); /** Return a new array with a single level of arrays unnested. */ export const unnest = (arr: any[]) => arr.reduce(unnestR, []); /** Return a completely flattened version of an array. */ @@ -323,17 +323,21 @@ export function assertPredicate(fn: Predicate, errMsg: string = "assert fa export const pairs = (object) => Object.keys(object).map(key => [ key, object[key]] ); /** - * Given two parallel arrays, returns an array of tuples, where each tuple is composed of [ left[i], right[i] ] - * Optionally, a map function can be provided. It will be applied to each left and right element before adding it to the tuple. + * Given two or more parallel arrays, returns an array of tuples where + * each tuple is composed of [ a[i], b[i], ... z[i] ] * * let foo = [ 0, 2, 4, 6 ]; * let bar = [ 1, 3, 5, 7 ]; - * paired(foo, bar); // [ [0, 1], [2, 3], [4, 5], [6, 7] ] - * paired(foo, bar, x => x * 2); // [ [0, 2], [4, 6], [8, 10], [12, 14] ] + * let baz = [ 10, 30, 50, 70 ]; + * tuples(foo, bar); // [ [0, 1], [2, 3], [4, 5], [6, 7] ] + * tuples(foo, bar, baz); // [ [0, 1, 10], [2, 3, 30], [4, 5, 50], [6, 7, 70] ] * */ -export const paired = (left: any[], right: any[], mapFn: Function = identity) => - left.map((lval, idx) => [ mapFn(lval), mapFn(right[idx]) ]); +export function arrayTuples(...arrayArgs: any[]): any[] { + if (arrayArgs.length === 0) return []; + let length = arrayArgs.reduce((min, arr) => Math.min(arr.length, min), 9007199254740991); // aka 2^53 − 1 aka Number.MAX_SAFE_INTEGER + return Array.apply(null, Array(length)).map((ignored, idx) => arrayArgs.map(arr => arr[idx]).reduce(pushR, [])); +} /** * Reduce function which builds an object from an array of [key, value] pairs. diff --git a/src/transition/transition.ts b/src/transition/transition.ts index 5a7b9cce5..d55724148 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -23,7 +23,7 @@ import {ViewConfig} from "../view/view"; import { map, find, extend, mergeR, flatten, unnest, tail, forEach, identity, - omit, isObject, not, prop, propEq, toJson, val, abstractKey + omit, isObject, not, prop, propEq, toJson, val, abstractKey, arrayTuples, allTrueR } from "../common/common"; let transitionCount = 0, REJECT = new RejectFactory(); @@ -269,14 +269,13 @@ export class Transition implements IHookRegistry { */ ignored() { let {to, from} = this._treeChanges; - let [toState, fromState] = [to, from].map(path => tail(path).state); - let [toParams, fromParams] = [to, from].map(path => tail(path).values); + if (this._options.reload || tail(to).state !== tail(from).state) return false; - return ( - !this._options.reload && - toState === fromState && - Param.equals(toState.parameters().filter(not(prop('dynamic'))), toParams, fromParams) - ); + let nodeSchemas: Param[][] = to.map(node => node.schema.filter(not(prop('dynamic')))); + let [toValues, fromValues] = [to, from].map(path => path.map(prop('values'))); + let tuples = arrayTuples(nodeSchemas, toValues, fromValues); + + return tuples.map(([schema, toVals, fromVals]) => Param.equals(schema, toVals, fromVals)).reduce(allTrueR, true); } hookBuilder(): HookBuilder { @@ -344,7 +343,7 @@ export class Transition implements IHookRegistry { // (X) means the to state is invalid. let id = this.$id, from = isObject(fromStateOrName) ? fromStateOrName.name : fromStateOrName, - fromParams = toJson(avoidEmptyHash(tail(this._treeChanges.from).values)), + fromParams = toJson(avoidEmptyHash(this._treeChanges.from.map(prop('values')).reduce(mergeR, {}))), toValid = this.valid() ? "" : "(X) ", to = isObject(toStateOrName) ? toStateOrName.name : toStateOrName, toParams = toJson(avoidEmptyHash(this.params())); diff --git a/test/stateDirectivesSpec.js b/test/stateDirectivesSpec.js index 798364657..03b1c92f2 100644 --- a/test/stateDirectivesSpec.js +++ b/test/stateDirectivesSpec.js @@ -481,11 +481,13 @@ describe('uiSrefActive', function() { $state.transitionTo('contacts.item.edit', { id: 1 }); $q.flush(); timeoutFlush(); + expect($state.params.id).toBe('1'); expect(a.attr('class')).toMatch(/active/); $state.transitionTo('contacts.item.edit', { id: 4 }); $q.flush(); timeoutFlush(); + expect($state.params.id).toBe('4'); expect(a.attr('class')).not.toMatch(/active/); })); From 9fec82def7fba19b990cc7177a25313653043fc5 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Mon, 26 Oct 2015 11:42:06 -0500 Subject: [PATCH 31/31] fix(State): do not return duplicate Param objs for url params in State.parameters() --- src/state/hooks/transitionManager.ts | 3 ++- src/state/state.ts | 8 +++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/state/hooks/transitionManager.ts b/src/state/hooks/transitionManager.ts index b171c0cce..6fc5830c1 100644 --- a/src/state/hooks/transitionManager.ts +++ b/src/state/hooks/transitionManager.ts @@ -89,7 +89,8 @@ export default class TransitionManager { if (error instanceof TransitionRejection) { if (error.type === RejectType.IGNORED) { // Update $stateParmas/$state.params/$location.url if transition ignored, but dynamic params have changed. - if (!Param.equals($state.$current.parameters().filter(prop('dynamic')), $stateParams, transition.params())) { + let dynamic = $state.$current.parameters().filter(prop('dynamic')); + if (!Param.equals(dynamic, $stateParams, transition.params())) { this.updateStateParams(); } return $state.current; diff --git a/src/state/state.ts b/src/state/state.ts index f4556f5df..391c71852 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -114,12 +114,10 @@ export class State { return this.parent && this.parent.root() || this; } - parameters(opts: any = {}): Param[] { + parameters(opts?): Param[] { opts = defaults(opts, { inherit: true }); - - return (this.parent && opts.inherit && this.parent.parameters() || []) - .concat(this.url && this.url.parameters({ inherit: false }) || []) - .concat(values(this.params)); + var inherited = opts.inherit && this.parent && this.parent.parameters() || []; + return inherited.concat(values(this.params)); } parameter(id: string, opts: any = {}): Param {