diff --git a/src/common/trace.ts b/src/common/trace.ts index 7a25daf3..593a5e72 100644 --- a/src/common/trace.ts +++ b/src/common/trace.ts @@ -39,7 +39,7 @@ import {Transition} from "../transition/transition"; import {ActiveUIView, ViewConfig, ViewContext} from "../view/interface"; import {stringify, functionToString, maxLength, padString} from "./strings"; import {Resolvable} from "../resolve/resolvable"; -import {PathNode} from "../path/node"; +import {PathNode} from "../path/pathNode"; import {PolicyWhen} from "../resolve/interface"; import {TransitionHook} from "../transition/transitionHook"; import {HookResult} from "../transition/interface"; diff --git a/src/hooks/ignoredTransition.ts b/src/hooks/ignoredTransition.ts index 3ddd11e2..2dcfa8d2 100644 --- a/src/hooks/ignoredTransition.ts +++ b/src/hooks/ignoredTransition.ts @@ -3,6 +3,7 @@ import { trace } from '../common/trace'; import { Rejection } from '../transition/rejectFactory'; import { TransitionService } from '../transition/transitionService'; import { Transition } from '../transition/transition'; +import {PathUtils} from '../path/pathFactory'; /** * A [[TransitionHookFn]] that skips a transition if it should be ignored @@ -13,10 +14,21 @@ import { Transition } from '../transition/transition'; * then the transition is ignored and not processed. */ function ignoredHook(trans: Transition) { - if (trans.ignored()) { - trace.traceTransitionIgnored(this); - return Rejection.ignored().toPromise(); + const ignoredReason = trans._ignoredReason(); + if (!ignoredReason) return; + + trace.traceTransitionIgnored(trans); + + const pending = trans.router.globals.transition; + + // The user clicked a link going back to the *current state* ('A') + // However, there is also a pending transition in flight (to 'B') + // Abort the transition to 'B' because the user now wants to be back at 'A'. + if (ignoredReason === 'SameAsCurrent' && pending) { + pending.abort(); } + + return Rejection.ignored().toPromise(); } export const registerIgnoredTransitionHook = (transitionService: TransitionService) => diff --git a/src/path/index.ts b/src/path/index.ts index a4f8e124..997ec863 100644 --- a/src/path/index.ts +++ b/src/path/index.ts @@ -1,3 +1,3 @@ /** @module path */ /** for typedoc */ -export * from "./node"; +export * from "./pathNode"; export * from "./pathFactory"; \ No newline at end of file diff --git a/src/path/pathFactory.ts b/src/path/pathFactory.ts index 5cc5f944..b437d317 100644 --- a/src/path/pathFactory.ts +++ b/src/path/pathFactory.ts @@ -1,6 +1,8 @@ /** @module path */ /** for typedoc */ -import {extend, find, pick, omit, tail, mergeR, values, unnestR, Predicate, inArray} from "../common/common"; +import { + extend, find, pick, omit, tail, mergeR, values, unnestR, Predicate, inArray, arrayTuples, +} from "../common/common"; import {prop, propEq, not} from "../common/hof"; import {RawParams} from "../params/interface"; @@ -10,13 +12,14 @@ import {_ViewDeclaration} from "../state/interface"; import {StateObject} from "../state/stateObject"; import {TargetState} from "../state/targetState"; -import {PathNode} from "../path/node"; +import {GetParamsFn, PathNode} from "./pathNode"; import {ViewService} from "../view/view"; +import { Param } from '../params/param'; /** * This class contains functions which convert TargetStates, Nodes and paths from one type to another. */ -export class PathFactory { +export class PathUtils { constructor() { } @@ -33,9 +36,9 @@ export class PathFactory { /** Given a fromPath: PathNode[] and a TargetState, builds a toPath: PathNode[] */ static buildToPath(fromPath: PathNode[], targetState: TargetState): PathNode[] { - let toPath: PathNode[] = PathFactory.buildPath(targetState); + let toPath: PathNode[] = PathUtils.buildPath(targetState); if (targetState.options().inherit) { - return PathFactory.inheritParams(fromPath, toPath, Object.keys(targetState.params())); + return PathUtils.inheritParams(fromPath, toPath, Object.keys(targetState.params())); } return toPath; } @@ -49,7 +52,7 @@ export class PathFactory { // Only apply the viewConfigs to the nodes for the given states path.filter(node => inArray(states, node.state)).forEach(node => { let viewDecls: _ViewDeclaration[] = values(node.state.views || {}); - let subPath = PathFactory.subPath(path, n => n === node); + let subPath = PathUtils.subPath(path, n => n === node); let viewConfigs: ViewConfig[][] = viewDecls.map(view => $view.createViewConfig(subPath, view)); node.views = viewConfigs.reduce(unnestR, []); }); @@ -97,15 +100,18 @@ export class PathFactory { return toPath.map(makeInheritedParamsNode); } + static nonDynamicParams = (node: PathNode): Param[] => + node.state.parameters({ inherit: false }) + .filter(param => !param.dynamic); + /** * Computes the tree changes (entering, exiting) between a fromPath and toPath. */ static treeChanges(fromPath: PathNode[], toPath: PathNode[], reloadState: StateObject): TreeChanges { let keep = 0, max = Math.min(fromPath.length, toPath.length); - const staticParams = (state: StateObject) => - state.parameters({ inherit: false }).filter(not(prop('dynamic'))).map(prop('id')); + const nodesMatch = (node1: PathNode, node2: PathNode) => - node1.equals(node2, staticParams(node1.state)); + node1.equals(node2, PathUtils.nonDynamicParams); while (keep < max && fromPath[keep].state !== reloadState && nodesMatch(fromPath[keep], toPath[keep])) { keep++; @@ -132,6 +138,43 @@ export class PathFactory { return { from, to, retained, exiting, entering }; } + /** + * Returns a new path which is: the subpath of the first path which matches the second path. + * + * The new path starts from root and contains any nodes that match the nodes in the second path. + * It stops before the first non-matching node. + * + * Nodes are compared using their state property and their parameter values. + * If a `paramsFn` is provided, only the [[Param]] returned by the function will be considered when comparing nodes. + * + * @param pathA the first path + * @param pathB the second path + * @param paramsFn a function which returns the parameters to consider when comparing + * + * @returns an array of PathNodes from the first path which match the nodes in the second path + */ + static matching(pathA: PathNode[], pathB: PathNode[], paramsFn?: GetParamsFn): PathNode[] { + let done = false; + let tuples: PathNode[][] = arrayTuples(pathA, pathB); + return tuples.reduce((matching, [nodeA, nodeB]) => { + done = done || !nodeA.equals(nodeB, paramsFn); + return done ? matching : matching.concat(nodeA); + }, []); + } + + /** + * Returns true if two paths are identical. + * + * @param pathA + * @param pathB + * @param paramsFn a function which returns the parameters to consider when comparing + * @returns true if the the states and parameter values for both paths are identical + */ + static equals(pathA: PathNode[], pathB: PathNode[], paramsFn?: GetParamsFn): boolean { + return pathA.length === pathB.length && + PathUtils.matching(pathA, pathB, paramsFn).length === pathA.length; + } + /** * Return a subpath of a path, which stops at the first matching node * @@ -149,5 +192,6 @@ export class PathFactory { } /** Gets the raw parameter values from a path */ - static paramValues = (path: PathNode[]) => path.reduce((acc, node) => extend(acc, node.paramValues), {}); + static paramValues = (path: PathNode[]) => + path.reduce((acc, node) => extend(acc, node.paramValues), {}); } diff --git a/src/path/node.ts b/src/path/pathNode.ts similarity index 62% rename from src/path/node.ts rename to src/path/pathNode.ts index 7b9af75a..98c1e047 100644 --- a/src/path/node.ts +++ b/src/path/pathNode.ts @@ -1,5 +1,5 @@ /** @module path */ /** for typedoc */ -import {extend, applyPairs, find, allTrueR} from "../common/common"; +import {extend, applyPairs, find, allTrueR, pairs, arrayTuples} from "../common/common"; import {propEq} from "../common/hof"; import {StateObject} from "../state/stateObject"; import {RawParams} from "../params/interface"; @@ -8,6 +8,8 @@ import {Resolvable} from "../resolve/resolvable"; import {ViewConfig} from "../view/interface"; /** + * @internalapi + * * A node in a [[TreeChanges]] path * * For a [[TreeChanges]] path, this class holds the stateful information for a single node in the path. @@ -27,19 +29,19 @@ export class PathNode { public views: ViewConfig[]; /** Creates a copy of a PathNode */ - constructor(state: PathNode); + constructor(node: PathNode); /** Creates a new (empty) PathNode for a State */ constructor(state: StateObject); - constructor(stateOrPath: any) { - if (stateOrPath instanceof PathNode) { - let node: PathNode = stateOrPath; + constructor(stateOrNode: any) { + if (stateOrNode instanceof PathNode) { + let node: PathNode = stateOrNode; this.state = node.state; this.paramSchema = node.paramSchema.slice(); this.paramValues = extend({}, node.paramValues); this.resolvables = node.resolvables.slice(); this.views = node.views && node.views.slice(); } else { - let state: StateObject = stateOrPath; + let state: StateObject = stateOrNode; this.state = state; this.paramSchema = state.parameters({ inherit: false }); this.paramValues = {}; @@ -63,42 +65,35 @@ export class PathNode { * @returns true if the state and parameter values for another PathNode are * equal to the state and param values for this PathNode */ - equals(node: PathNode, keys = this.paramSchema.map(p => p.id)): boolean { - const paramValsEq = (key: string) => - this.parameter(key).type.equals(this.paramValues[key], node.paramValues[key]); - return this.state === node.state && keys.map(paramValsEq).reduce(allTrueR, true); - } - - /** Returns a clone of the PathNode */ - static clone(node: PathNode) { - return new PathNode(node); + equals(node: PathNode, paramsFn?: GetParamsFn): boolean { + const diff = this.diff(node, paramsFn); + return diff && diff.length === 0; } /** - * Returns a new path which is a subpath of the first path which matched the second path. + * Finds Params with different parameter values on another PathNode. * - * The new path starts from root and contains any nodes that match the nodes in the second path. - * Nodes are compared using their state property and parameter values. + * Given another node (of the same state), finds the parameter values which differ. + * Returns the [[Param]] (schema objects) whose parameter values differ. * - * @param pathA the first path - * @param pathB the second path - * @param ignoreDynamicParams don't compare dynamic parameter values + * Given another node for a different state, returns `false` + * + * @param node The node to compare to + * @param paramsFn A function that returns which parameters should be compared. + * @returns The [[Param]]s which differ, or null if the two nodes are for different states */ - static matching(pathA: PathNode[], pathB: PathNode[], ignoreDynamicParams = true): PathNode[] { - let matching: PathNode[] = []; - - for (let i = 0; i < pathA.length && i < pathB.length; i++) { - let a = pathA[i], b = pathB[i]; + diff(node: PathNode, paramsFn?: GetParamsFn): Param[] | false { + if (this.state !== node.state) return false; - if (a.state !== b.state) break; - - let changedParams = Param.changed(a.paramSchema, a.paramValues, b.paramValues) - .filter(param => !(ignoreDynamicParams && param.dynamic)); - if (changedParams.length) break; - - matching.push(a); - } + const params: Param[] = paramsFn ? paramsFn(this) : this.paramSchema; + return Param.changed(params, this.paramValues, node.paramValues); + } - return matching + /** Returns a clone of the PathNode */ + static clone(node: PathNode) { + return new PathNode(node); } -} \ No newline at end of file +} + +/** @hidden */ +export type GetParamsFn = (pathNode: PathNode) => Param[]; \ No newline at end of file diff --git a/src/resolve/resolvable.ts b/src/resolve/resolvable.ts index e3216f3c..769a4661 100644 --- a/src/resolve/resolvable.ts +++ b/src/resolve/resolvable.ts @@ -12,7 +12,7 @@ import {stringify} from "../common/strings"; import {isFunction, isObject} from "../common/predicates"; import {Transition} from "../transition/transition"; import {StateObject} from "../state/stateObject"; -import {PathNode} from "../path/node"; +import {PathNode} from "../path/pathNode"; // TODO: explicitly make this user configurable diff --git a/src/resolve/resolveContext.ts b/src/resolve/resolveContext.ts index 0b040a2c..43f3d80a 100644 --- a/src/resolve/resolveContext.ts +++ b/src/resolve/resolveContext.ts @@ -5,10 +5,10 @@ import { propEq, not } from "../common/hof"; import { trace } from "../common/trace"; import { services, $InjectorLike } from "../common/coreservices"; import { resolvePolicies, PolicyWhen, ResolvePolicy } from "./interface"; -import { PathNode } from "../path/node"; +import { PathNode } from "../path/pathNode"; import { Resolvable } from "./resolvable"; import { StateObject } from "../state/stateObject"; -import { PathFactory } from "../path/pathFactory"; +import { PathUtils } from "../path/pathFactory"; import { stringify } from "../common/strings"; import { Transition } from "../transition/transition"; import { UIInjector } from "../interface"; @@ -82,7 +82,7 @@ export class ResolveContext { * `let AB = ABCD.subcontext(a)` */ subContext(state: StateObject): ResolveContext { - return new ResolveContext(PathFactory.subPath(this._path, node => node.state === state)); + return new ResolveContext(PathUtils.subPath(this._path, node => node.state === state)); } /** @@ -164,7 +164,7 @@ export class ResolveContext { let node = this.findNode(resolvable); // Find which other resolvables are "visible" to the `resolvable` argument // subpath stopping at resolvable's node, or the whole path (if the resolvable isn't in the path) - let subPath: PathNode[] = PathFactory.subPath(this._path, x => x === node) || this._path; + let subPath: PathNode[] = PathUtils.subPath(this._path, x => x === node) || this._path; let availableResolvables: Resolvable[] = subPath .reduce((acc, node) => acc.concat(node.resolvables), []) //all of subpath's resolvables .filter(res => res !== resolvable); // filter out the `resolvable` argument diff --git a/src/state/stateService.ts b/src/state/stateService.ts index d8c0d6e2..832c98ba 100644 --- a/src/state/stateService.ts +++ b/src/state/stateService.ts @@ -8,8 +8,8 @@ import { isDefined, isObject, isString } from '../common/predicates'; import { Queue } from '../common/queue'; import { services } from '../common/coreservices'; -import { PathFactory } from '../path/pathFactory'; -import { PathNode } from '../path/node'; +import { PathUtils } from '../path/pathFactory'; +import { PathNode } from '../path/pathNode'; import { HookResult, TransitionOptions } from '../transition/interface'; import { defaultTransOpts } from '../transition/transitionService'; @@ -93,7 +93,7 @@ export class StateService { * @internalapi */ private _handleInvalidTargetState(fromPath: PathNode[], toState: TargetState) { - let fromState = PathFactory.makeTargetState(fromPath); + let fromState = PathUtils.makeTargetState(fromPath); let globals = this.router.globals; const latestThing = () => globals.transitionHistory.peekTail(); let latest = latestThing(); @@ -340,9 +340,11 @@ export class StateService { */ const rejectedTransitionHandler = (transition: Transition) => (error: any): Promise => { if (error instanceof Rejection) { + const isLatest = router.globals.lastStartedTransitionId === transition.$id; + if (error.type === RejectType.IGNORED) { + isLatest && router.urlRouter.update(); // Consider ignored `Transition.run()` as a successful `transitionTo` - router.urlRouter.update(); return services.$q.when(globals.current); } @@ -355,7 +357,7 @@ export class StateService { } if (error.type === RejectType.ABORTED) { - router.urlRouter.update(); + isLatest && router.urlRouter.update(); return services.$q.reject(error); } } @@ -593,7 +595,7 @@ export class StateService { if (!state || !state.lazyLoad) throw new Error("Can not lazy load " + stateOrName); let currentPath = this.getCurrentPath(); - let target = PathFactory.makeTargetState(currentPath); + let target = PathUtils.makeTargetState(currentPath); transition = transition || this.router.transitionService.create(currentPath, target); return lazyLoadState(transition, state); diff --git a/src/transition/hookBuilder.ts b/src/transition/hookBuilder.ts index 6a2fb722..cf0d85d9 100644 --- a/src/transition/hookBuilder.ts +++ b/src/transition/hookBuilder.ts @@ -14,7 +14,7 @@ import { import {Transition} from "./transition"; import {TransitionHook} from "./transitionHook"; import {StateObject} from "../state/stateObject"; -import {PathNode} from "../path/node"; +import {PathNode} from "../path/pathNode"; import {TransitionService} from "./transitionService"; import {TransitionEventType} from "./transitionEventType"; import {RegisteredHook} from "./hookRegistry"; diff --git a/src/transition/hookRegistry.ts b/src/transition/hookRegistry.ts index 0fed443c..aa448948 100644 --- a/src/transition/hookRegistry.ts +++ b/src/transition/hookRegistry.ts @@ -4,7 +4,7 @@ */ /** for typedoc */ import { extend, removeFrom, tail, values, identity, map } from "../common/common"; import {isString, isFunction} from "../common/predicates"; -import {PathNode} from "../path/node"; +import {PathNode} from "../path/pathNode"; import { TransitionStateHookFn, TransitionHookFn, TransitionHookPhase, TransitionHookScope, IHookRegistry, PathType } from "./interface"; // has or is using diff --git a/src/transition/interface.ts b/src/transition/interface.ts index 33a81cdf..21ec5a8f 100644 --- a/src/transition/interface.ts +++ b/src/transition/interface.ts @@ -7,7 +7,7 @@ import {Predicate} from "../common/common"; import {Transition} from "./transition"; import {StateObject} from "../state/stateObject"; -import {PathNode} from "../path/node"; +import {PathNode} from "../path/pathNode"; import {TargetState} from "../state/targetState"; import {RegisteredHook} from "./hookRegistry"; diff --git a/src/transition/transition.ts b/src/transition/transition.ts index 6e6f9a33..6383cf18 100644 --- a/src/transition/transition.ts +++ b/src/transition/transition.ts @@ -18,8 +18,8 @@ import { import { TransitionHook } from './transitionHook'; import { matchState, makeEvent, RegisteredHook } from './hookRegistry'; import { HookBuilder } from './hookBuilder'; -import { PathNode } from '../path/node'; -import { PathFactory } from '../path/pathFactory'; +import { PathNode } from '../path/pathNode'; +import { PathUtils } from '../path/pathFactory'; import { StateObject } from '../state/stateObject'; import { TargetState } from '../state/targetState'; import { Param } from '../params/param'; @@ -152,19 +152,19 @@ export class Transition implements IHookRegistry { // current() is assumed to come from targetState.options, but provide a naive implementation otherwise. this._options = extend({ current: val(this) }, targetState.options()); this.$id = router.transitionService._transitionCount++; - let toPath = PathFactory.buildToPath(fromPath, targetState); - this._treeChanges = PathFactory.treeChanges(fromPath, toPath, this._options.reloadState); + let toPath = PathUtils.buildToPath(fromPath, targetState); + this._treeChanges = PathUtils.treeChanges(fromPath, toPath, this._options.reloadState); this.createTransitionHookRegFns(); let onCreateHooks = this._hookBuilder.buildHooksForPhase(TransitionHookPhase.CREATE); - TransitionHook.runAllHooks(onCreateHooks); + TransitionHook.invokeHooks(onCreateHooks, () => null); this.applyViewConfigs(router); } private applyViewConfigs(router: UIRouter) { let enteringStates = this._treeChanges.entering.map(node => node.state); - PathFactory.applyViewConfigs(router.transitionService.$view, this._treeChanges.to, enteringStates); + PathUtils.applyViewConfigs(router.transitionService.$view, this._treeChanges.to, enteringStates); } /** @@ -311,7 +311,7 @@ export class Transition implements IHookRegistry { */ injector(state?: StateOrName, pathName = "to"): UIInjector { let path: PathNode[] = this._treeChanges[pathName]; - if (state) path = PathFactory.subPath(path, node => node.state === state || node.state.name === state); + if (state) path = PathUtils.subPath(path, node => node.state === state || node.state.name === state); return new ResolveContext(path).injector(); } @@ -551,7 +551,7 @@ export class Transition implements IHookRegistry { }; // Find any "entering" nodes in the redirect path that match the original path and aren't being reloaded - let matchingEnteringNodes: PathNode[] = PathNode.matching(redirectEnteringNodes, originalEnteringNodes) + let matchingEnteringNodes: PathNode[] = PathUtils.matching(redirectEnteringNodes, originalEnteringNodes, PathUtils.nonDynamicParams) .filter(not(nodeIsReloading(targetState.options().reloadState))); // Use the existing (possibly pre-resolved) resolvables for the matching entering nodes. @@ -607,8 +607,22 @@ export class Transition implements IHookRegistry { * @returns true if the Transition is ignored. */ ignored(): boolean { - let changes = this._changedParams(); - return !changes ? false : changes.length === 0; + return !!this._ignoredReason(); + } + + /** @hidden */ + _ignoredReason(): "SameAsCurrent"|"SameAsPending"|undefined { + const pending = this.router.globals.transition; + const reloadState = this._options.reloadState; + + const same = (pathA, pathB) => { + if (pathA.length !== pathB.length) return false; + const matching = PathUtils.matching(pathA, pathB); + return pathA.length === matching.filter(node => !reloadState || !node.state.includes[reloadState.name]).length; + }; + + if (same(this.treeChanges('from'), this.treeChanges('to'))) return "SameAsCurrent"; + if (pending && same(pending.treeChanges('to'), this.treeChanges('to'))) return "SameAsPending"; } /** @@ -627,19 +641,6 @@ export class Transition implements IHookRegistry { const getHooksFor = (phase: TransitionHookPhase) => this._hookBuilder.buildHooksForPhase(phase); - const startTransition = () => { - let globals = this.router.globals; - - globals.lastStartedTransitionId = this.$id; - globals.transition = this; - globals.transitionHistory.enqueue(this); - - trace.traceTransitionStart(this); - - return services.$q.when(undefined); - }; - - // When the chain is complete, then resolve or reject the deferred const transitionSuccess = () => { trace.traceSuccess(this.$to(), this); @@ -656,18 +657,30 @@ export class Transition implements IHookRegistry { runAllHooks(getHooksFor(TransitionHookPhase.ERROR)); }; - // This waits to build the RUN hook chain until after the "BEFORE" hooks complete - // This allows a BEFORE hook to dynamically add RUN hooks via the Transition object. const runTransition = () => { + // Wait to build the RUN hook chain until the BEFORE hooks are done + // This allows a BEFORE hook to dynamically add additional RUN hooks via the Transition object. let allRunHooks = getHooksFor(TransitionHookPhase.RUN); let done = () => services.$q.when(undefined); - TransitionHook.invokeHooks(allRunHooks, done) - .then(transitionSuccess, transitionError); + return TransitionHook.invokeHooks(allRunHooks, done); + }; + + const startTransition = () => { + let globals = this.router.globals; + + globals.lastStartedTransitionId = this.$id; + globals.transition = this; + globals.transitionHistory.enqueue(this); + + trace.traceTransitionStart(this); + + return services.$q.when(undefined); }; let allBeforeHooks = getHooksFor(TransitionHookPhase.BEFORE); TransitionHook.invokeHooks(allBeforeHooks, startTransition) - .then(runTransition); + .then(runTransition) + .then(transitionSuccess, transitionError); return this.promise; } diff --git a/src/transition/transitionHook.ts b/src/transition/transitionHook.ts index 538ce6db..a8ecda2f 100644 --- a/src/transition/transitionHook.ts +++ b/src/transition/transitionHook.ts @@ -104,7 +104,7 @@ export class TransitionHook { try { let result = invokeCallback(); - if (isPromise(result)) { + if (!this.type.synchronous && isPromise(result)) { return result.catch(normalizeErr) .then(handleResult, handleError); } else { @@ -217,21 +217,23 @@ export class TransitionHook { * If no hook returns a promise, then all hooks are processed synchronously. * * @param hooks the list of TransitionHooks to invoke - * @param done a callback that is invoked after all the hooks have successfully completed + * @param doneCallback a callback that is invoked after all the hooks have successfully completed * * @returns a promise for the async result, or the result of the callback */ - static invokeHooks(hooks: TransitionHook[], done: () => T): Promise | T { + static invokeHooks(hooks: TransitionHook[], doneCallback: (result?: HookResult) => T): Promise | T { for (let idx = 0; idx < hooks.length; idx++) { let hookResult = hooks[idx].invokeHook(); if (isPromise(hookResult)) { let remainingHooks = hooks.slice(idx + 1); - return TransitionHook.chain(remainingHooks, hookResult).then(done); + + return TransitionHook.chain(remainingHooks, hookResult) + .then(doneCallback); } } - return done(); + return doneCallback(); } /** diff --git a/src/transition/transitionService.ts b/src/transition/transitionService.ts index ffab636f..b91d3003 100644 --- a/src/transition/transitionService.ts +++ b/src/transition/transitionService.ts @@ -10,7 +10,7 @@ import { import { Transition } from "./transition"; import { makeEvent, RegisteredHook } from "./hookRegistry"; import { TargetState } from "../state/targetState"; -import { PathNode } from "../path/node"; +import { PathNode } from "../path/pathNode"; import { ViewService } from "../view/view"; import { UIRouter } from "../router"; import { registerAddCoreResolvables } from "../hooks/coreResolvables"; diff --git a/src/view/interface.ts b/src/view/interface.ts index 92a5c86d..1d8ff103 100644 --- a/src/view/interface.ts +++ b/src/view/interface.ts @@ -1,6 +1,6 @@ /** @module view */ /** for typedoc */ import {_ViewDeclaration} from "../state/interface"; -import {PathNode} from "../path/node"; +import {PathNode} from "../path/pathNode"; /** * The context ref can be anything that has a `name` and a `parent` reference to another IContextRef diff --git a/src/view/view.ts b/src/view/view.ts index 385e05ee..fa706f46 100644 --- a/src/view/view.ts +++ b/src/view/view.ts @@ -6,7 +6,7 @@ import {equals, applyPairs, removeFrom, TypedMap} from "../common/common"; import {curry, prop} from "../common/hof"; import {isString, isArray} from "../common/predicates"; import {trace} from "../common/trace"; -import {PathNode} from "../path/node"; +import {PathNode} from "../path/pathNode"; import {ActiveUIView, ViewContext, ViewConfig} from "./interface"; import {_ViewDeclaration} from "../state/interface"; diff --git a/test/lazyLoadSpec.ts b/test/lazyLoadSpec.ts index c797cd76..a32751f9 100644 --- a/test/lazyLoadSpec.ts +++ b/test/lazyLoadSpec.ts @@ -389,8 +389,7 @@ describe('future state', function () { }; $registry.register(futureStateDef); - $state.go('A'); - $state.go('A').then(() => { + Promise.all([$state.go('A'), $state.go('A')]).then(() => { expect(count).toBe(1); expect($state.current.name).toBe('A'); done(); diff --git a/test/pathNodeSpec.ts b/test/pathNodeSpec.ts new file mode 100644 index 00000000..2967b83e --- /dev/null +++ b/test/pathNodeSpec.ts @@ -0,0 +1,77 @@ +import {UIRouter} from '../src/router'; +import {StateRegistry} from '../src/state/stateRegistry'; +import {StateService} from '../src/state/stateService'; +import {PathNode} from '../src/path/pathNode'; +import {TestingPlugin} from './_testingPlugin'; + +let router: UIRouter; +let registry: StateRegistry; +let $state: StateService; + +describe('PathNode', () => { + let a1: PathNode, a2: PathNode, b: PathNode; + + beforeEach(() => { + router = new UIRouter(); + router.plugin(TestingPlugin); + + registry = router.stateRegistry; + $state = router.stateService; + let A = { + name: 'A', + url: '/:foo/:bar/:baz', + params: { + foo: { dynamic: true }, + nonurl: null, + }, + }; + + let B = { + name: 'B', + url: '/B/:qux', + }; + + router.stateRegistry.register(A); + router.stateRegistry.register(B); + + a1 = new PathNode(registry.get('A').$$state()); + a2 = new PathNode(registry.get('A').$$state()); + b = new PathNode(registry.get('B').$$state()); + }); + + describe('.diff()', () => { + it('returns `false` when states differ', () => { + expect(a1.diff(b)).toBe(false); + }); + + it('should return an empty array when no param values differ', () => { + a1.applyRawParams({ foo: '1', bar: '2', baz: '3' }); + a2.applyRawParams({ foo: '1', bar: '2', baz: '3' }); + + expect(a1.diff(a2)).toEqual([]); + }); + + it('should return an array of Param objects for each value that differs', () => { + a1.applyRawParams({ foo: '1', bar: '2', baz: '5' }); + a2.applyRawParams({ foo: '1', bar: '2', baz: '3' }); + + let baz = a1.parameter('baz'); + expect(a1.diff(a2)).toEqual([baz]); + }); + + it('should return an array of Param objects for each value that differs (2)', () => { + a1.applyRawParams({ foo: '0', bar: '0', nonurl: '0' }); + a2.applyRawParams({ foo: '1', bar: '1', baz: '1' }); + + expect(a1.diff(a2)).toEqual(a1.paramSchema); + }); + + it('should return an array of Param objects for each value that differs (3)', () => { + a1.applyRawParams({ foo: '1', bar: '2', baz: '3', nonurl: '4' }); + a2.applyRawParams({ foo: '1', bar: '2', baz: '3' }); + + let nonurl = a1.parameter('nonurl'); + expect(a1.diff(a2)).toEqual([nonurl]); + }); + }); +}); \ No newline at end of file diff --git a/test/resolveSpec.ts b/test/resolveSpec.ts index 1aa0f7b9..99e04457 100644 --- a/test/resolveSpec.ts +++ b/test/resolveSpec.ts @@ -375,16 +375,19 @@ describe('Resolvables system:', function () { params: { param: { dynamic: true }, }, + resolvePolicy: { when: 'EAGER' }, resolve: { data: () => { - new Promise(resolve => resolve('Expensive data ' + resolveCount++)); + new Promise(resolve => + resolve('Expensive data ' + resolveCount++)); }, }, }); $transitions.onEnter({entering: "dynamic"}, trans => { - if (trans.params()['param'] === 'initial') + if (trans.params()['param'] === 'initial') { return $state.target("dynamic", { param: 'redirected' }); + } }); $state.go("dynamic", { param: 'initial'}).then(() => { diff --git a/test/stateServiceSpec.ts b/test/stateServiceSpec.ts index 990b9a07..697123b9 100644 --- a/test/stateServiceSpec.ts +++ b/test/stateServiceSpec.ts @@ -7,7 +7,7 @@ import { isFunction } from '../src/common/predicates'; import { StateRegistry } from '../src/state/stateRegistry'; import { Transition } from '../src/transition/transition'; import { Param } from '../src/params/param'; -import { RejectType } from '../src/transition/rejectFactory'; +import {Rejection, RejectType} from '../src/transition/rejectFactory'; import { TestingPlugin } from './_testingPlugin'; import { StateDeclaration } from '../src/state/interface'; @@ -529,16 +529,53 @@ describe('stateService', function () { done(); }); + it('ignores transitions that are equivalent to the pending transition', async (done) => { + $state.defaultErrorHandler(() => null); + await initStateTo(A); + router.transitionService.onStart({}, trans => new Promise(resolve => setTimeout(resolve, 50))); + + let trans1 = $state.go(B, {}).transition.promise.catch(err => err); + let trans2 = $state.go(B, {}).transition.promise.catch(err => err); + + let result1 = await trans1; + let result2 = await trans2; + + expect($state.current).toBe(B); + expect(result1).toBe(B); + expect(result2.type).toBe(RejectType.IGNORED); + + done(); + }); + + it('cancels pending transitions that are superseded by an ignored transition', async (done) => { + $state.defaultErrorHandler(() => null); + await initStateTo(A); + router.transitionService.onStart({}, trans => new Promise(resolve => setTimeout(resolve, 50))); + + let trans1 = $state.go(B, {}).transition.promise.catch(err => err); + let trans2 = $state.go(A, {}).transition.promise.catch(err => err); + + let result1 = await trans1; + let result2 = await trans2; + + expect($state.current).toBe(A); + expect(result1.type).toBe(RejectType.ABORTED); + expect(result2.type).toBe(RejectType.IGNORED); + + done(); + }); + + it('aborts pending transitions even when going back to the current state', async(done) => { $state.defaultErrorHandler(() => null); await initStateTo(A); - let superseded = $state.transitionTo(B, {}).catch(err => err); - await $state.transitionTo(A, {}); + let superseded = $state.go(B, {}).transition.promise.catch(err => err); + await $state.go(A, {}); let result = await superseded; expect($state.current).toBe(A); - expect(result.type).toBe(RejectType.SUPERSEDED); + expect(result.type).toBe(RejectType.ABORTED); done(); }); diff --git a/test/transitionSpec.ts b/test/transitionSpec.ts index 69f69236..d024728f 100644 --- a/test/transitionSpec.ts +++ b/test/transitionSpec.ts @@ -1,4 +1,4 @@ -import { PathNode } from "../src/path/node"; +import { PathNode } from "../src/path/pathNode"; import { UIRouter, RejectType, Rejection, pluck, services, TransitionService, StateService, Resolvable, Transition, } from "../src/index"; @@ -131,7 +131,7 @@ describe('transition', function () { })); // Test for #2972 and https://github.com/ui-router/react/issues/3 - fit('should reject transitions that are superseded by a new transition', ((done) => { + it('should reject transitions that are superseded by a new transition', ((done) => { $state.defaultErrorHandler(function() {}); router.stateRegistry.register({ name: 'slowResolve', @@ -149,7 +149,7 @@ describe('transition', function () { _delay(20) .then(() => - $state.go('slowResolve').transition.promise ) + $state.go('A').transition.promise) .then(delay(50)) .then(() => expect(results).toEqual({ success: 1, error: 1 }))