Skip to content

Commit 9f46a1b

Browse files
refactor(Resolvable): make Node.resolvables an array, not an object
This is the first step in allowing arbitrary (non-string) keys for injecting Resolvables. refactor(matching): When calculating Node.matching(), require the node parameter values to match. refactor(redirect): During redirect, only copy resolveables for matching entering states (between the original transition and the redirect transition). refactor(stringify): Defer creation of stringify `pattern` This mitigates the possibility of modules loading in a different order and imported classes such as Rejection, Transition, Resolvable not being immediately available until
1 parent 013c77a commit 9f46a1b

File tree

7 files changed

+98
-84
lines changed

7 files changed

+98
-84
lines changed

src/common/strings.ts

+16-11
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,22 @@ export function fnToString(fn: IInjectable) {
6767

6868
const isTransitionRejectionPromise = Rejection.isTransitionRejectionPromise;
6969

70-
let stringifyPattern = pattern([
71-
[not(isDefined), val("undefined")],
72-
[isNull, val("null")],
73-
[isPromise, promiseToString],
74-
[isTransitionRejectionPromise, (x: any) => x._transitionRejection.toString()],
75-
[is(Rejection), invoke("toString")],
76-
[is(Transition), invoke("toString")],
77-
[is(Resolvable), invoke("toString")],
78-
[isInjectable, functionToString],
79-
[val(true), identity]
80-
]);
70+
let stringifyPatternFn = null;
71+
let stringifyPattern = function(val) {
72+
stringifyPatternFn = stringifyPatternFn || pattern([
73+
[not(isDefined), val("undefined")],
74+
[isNull, val("null")],
75+
[isPromise, val("[Promise]")],
76+
[isTransitionRejectionPromise, (x: any) => x._transitionRejection.toString()],
77+
[is(Rejection), invoke("toString")],
78+
[is(Transition), invoke("toString")],
79+
[is(Resolvable), invoke("toString")],
80+
[isInjectable, functionToString],
81+
[val(true), identity]
82+
]);
83+
84+
return stringifyPatternFn(val);
85+
};
8186

8287
export function stringify(o: Object) {
8388
var seen: any[] = [];

src/path/node.ts

+20-14
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
/** @module path */ /** for typedoc */
2-
import {extend, applyPairs, map, find, allTrueR, values, mapObj} from "../common/common";
2+
import {extend, applyPairs, find, allTrueR} from "../common/common";
33
import {prop, propEq} from "../common/hof";
44
import {State} from "../state/module";
55
import {RawParams} from "../params/interface";
66
import {Param} from "../params/module";
77
import {Resolvable, ResolveContext} from "../resolve/module";
88
import {ViewConfig} from "../view/interface";
9-
import {Resolvables} from "../resolve/interface";
109

1110
export class Node {
1211
public state: State;
1312
public paramSchema: Param[];
1413
public paramValues: { [key: string]: any };
15-
public resolves: Resolvables;
14+
public resolvables: Resolvable[];
1615
public views: ViewConfig[];
1716
public resolveContext: ResolveContext;
1817

@@ -24,14 +23,14 @@ export class Node {
2423
this.state = node.state;
2524
this.paramSchema = node.paramSchema.slice();
2625
this.paramValues = extend({}, node.paramValues);
27-
this.resolves = extend({}, node.resolves);
26+
this.resolvables = node.resolvables.slice();
2827
this.views = node.views && node.views.slice();
2928
this.resolveContext = node.resolveContext;
3029
} else {
3130
this.state = state;
3231
this.paramSchema = state.parameters({ inherit: false });
3332
this.paramValues = {};
34-
this.resolves = mapObj(state.resolve, (fn: Function, name: string) => new Resolvable(name, fn));
33+
this.resolvables = Object.keys(state.resolve || {}).map(key => new Resolvable(key, state.resolve[key]));
3534
}
3635
}
3736

@@ -55,15 +54,22 @@ export class Node {
5554
}
5655

5756
/**
58-
* Returns a new path which is a subpath of the first path. The new path starts from root and contains any nodes
59-
* that match the nodes in the second path. Nodes are compared using their state property.
60-
* @param first {Node[]}
61-
* @param second {Node[]}
62-
* @returns {Node[]}
57+
* Returns a new path which is a subpath of the first path which matched the second path.
58+
*
59+
* The new path starts from root and contains any nodes that match the nodes in the second path.
60+
* Nodes are compared using their state property and parameter values.
6361
*/
64-
static matching(first: Node[], second: Node[]): Node[] {
65-
let matchedCount = first.reduce((prev, node, i) =>
66-
prev === i && i < second.length && node.state === second[i].state ? i + 1 : prev, 0);
67-
return first.slice(0, matchedCount);
62+
static matching(pathA: Node[], pathB: Node[]): Node[] {
63+
let matching = [];
64+
65+
for (let i = 0; i < pathA.length && i < pathB.length; i++) {
66+
let a = pathA[i], b = pathB[i];
67+
68+
if (a.state !== b.state) break;
69+
if (!Param.equals(a.paramSchema, a.paramValues, b.paramValues)) break;
70+
matching.push(a);
71+
}
72+
73+
return matching
6874
}
6975
}

src/resolve/resolvable.ts

+10-12
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,19 @@ import {ResolveContext} from "./resolveContext";
2222
* parameter to those fns.
2323
*/
2424
export class Resolvable {
25-
constructor(name: string, resolveFn: Function, preResolvedData?: any) {
26-
extend(this, {
27-
name,
28-
resolveFn,
29-
deps: services.$injector.annotate(resolveFn, services.$injector.strictDi),
30-
data: preResolvedData
31-
});
32-
}
33-
3425
name: string;
3526
resolveFn: Function;
3627
deps: string[];
3728

3829
promise: Promise<any> = undefined;
3930
data: any;
31+
32+
constructor(name: string, resolveFn: Function, preResolvedData?: any) {
33+
this.name = name;
34+
this.resolveFn = resolveFn;
35+
this.deps = services.$injector.annotate(resolveFn, services.$injector.strictDi);
36+
this.data = preResolvedData;
37+
}
4038

4139
// synchronous part:
4240
// - sets up the Resolvable's promise
@@ -92,13 +90,13 @@ export class Resolvable {
9290
}
9391

9492
/**
95-
* Validates the result map as a "resolve:" style object, then transforms the resolves into Resolvables
93+
* Validates the result map as a "resolve:" style object, then transforms the resolves into Resolvable[]
9694
*/
97-
static makeResolvables(resolves: { [key: string]: Function; }): Resolvables {
95+
static makeResolvables(resolves: { [key: string]: Function; }): Resolvable[] {
9896
// If a hook result is an object, it should be a map of strings to functions.
9997
let invalid = filter(resolves, not(isInjectable)), keys = Object.keys(invalid);
10098
if (keys.length)
10199
throw new Error(`Invalid resolve key/value: ${keys[0]}/${invalid[keys[0]]}`);
102-
return map(resolves, (fn, name: string) => new Resolvable(name, fn));
100+
return Object.keys(resolves).map(key => new Resolvable(key, resolves[key]));
103101
}
104102
}

src/resolve/resolveContext.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ interface Promises { [key: string]: Promise<any>; }
2020

2121
export class ResolveContext {
2222

23-
private _nodeFor: Function;
24-
private _pathTo: Function;
23+
private _nodeFor: (s: State) => Node;
24+
private _pathTo: (s: State) => Node[];
2525

2626
constructor(private _path: Node[]) {
2727
extend(this, {
@@ -62,9 +62,13 @@ export class ResolveContext {
6262
const path = (state ? this._pathTo(state) : this._path);
6363
const last = tail(path);
6464

65-
return path.reduce((memo, node) => {
65+
return path.reduce((memo, node: Node) => {
6666
let omitProps = (node === last) ? options.omitOwnLocals : [];
67-
let filteredResolvables = omit(node.resolves, omitProps);
67+
68+
let filteredResolvables = node.resolvables
69+
.filter(r => omitProps.indexOf(r.name) === -1)
70+
.reduce((acc, r) => { acc[r.name] = r; return acc; }, {});
71+
6872
return extend(memo, filteredResolvables);
6973
}, <Resolvables> {});
7074
}
@@ -79,13 +83,16 @@ export class ResolveContext {
7983
return new ResolveContext(this._pathTo(state));
8084
}
8185

82-
addResolvables(resolvables: Resolvables, state: State) {
83-
extend(this._nodeFor(state).resolves, resolvables);
86+
addResolvables(newResolvables: Resolvable[], state: State) {
87+
var node = this._nodeFor(state);
88+
var keys = newResolvables.map(r => r.name);
89+
node.resolvables = node.resolvables.filter(r => keys.indexOf(r.name) === -1).concat(newResolvables);
8490
}
8591

8692
/** Gets the resolvables declared on a particular state */
8793
getOwnResolvables(state: State): Resolvables {
88-
return extend({}, this._nodeFor(state).resolves);
94+
return this._nodeFor(state).resolvables
95+
.reduce((acc, r) => { acc[r.name] = r; return acc; }, <Resolvables>{});
8996
}
9097

9198
// Returns a promise for an array of resolved path Element promises

src/state/hooks/resolveHooks.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class ResolveHooks {
4343
// Resolve all the LAZY resolves, then resolve the `$resolve$` object, then add `$resolve$` to the context
4444
return context.resolvePathElement(node.state, options)
4545
.then(() => $resolve$.resolveResolvable(context))
46-
.then(() => context.addResolvables({$resolve$}, node.state));
46+
.then(() => context.addResolvables([$resolve$], node.state));
4747
}
4848

4949
// Resolve eager resolvables before when the transition starts

src/transition/transition.ts

+35-25
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
import {trace} from "../common/trace";
33
import {services} from "../common/coreservices";
44
import {
5-
map, find, extend, filter, mergeR, tail,
5+
map, find, extend, mergeR, tail,
66
omit, toJson, abstractKey, arrayTuples, unnestR, identity, anyTrueR
77
} from "../common/common";
88
import { isObject } from "../common/predicates";
9-
import { not, prop, propEq, val } from "../common/hof";
9+
import { prop, propEq, val, not } from "../common/hof";
1010

1111
import {StateDeclaration, StateOrName} from "../state/interface";
1212
import {TransitionOptions, TransitionHookOptions, TreeChanges, IHookRegistry, IHookRegistration, IHookGetter} from "./interface";
@@ -20,7 +20,6 @@ import {Resolvable} from "../resolve/module";
2020
import {TransitionService} from "./transitionService";
2121
import {ViewConfig} from "../view/interface";
2222
import {Rejection} from "./rejectFactory";
23-
import {Resolvables} from "../resolve/interface";
2423

2524

2625
let transitionCount = 0;
@@ -143,10 +142,10 @@ export class Transition implements IHookRegistry {
143142

144143
PathFactory.bindResolveContexts(this._treeChanges.to);
145144

146-
let rootResolvables: Resolvables = {
147-
"$transition$": new Resolvable('$transition$', () => this, this),
148-
"$stateParams": new Resolvable('$stateParams', () => this.params(), this.params())
149-
};
145+
let rootResolvables: Resolvable[] = [
146+
new Resolvable('$transition$', () => this, this),
147+
new Resolvable('$stateParams', () => this.params(), this.params())
148+
];
150149
let rootNode: Node = this._treeChanges.to[0];
151150
rootNode.resolveContext.addResolvables(rootResolvables, rootNode.state)
152151
}
@@ -292,34 +291,45 @@ export class Transition implements IHookRegistry {
292291
treeChanges = () => this._treeChanges;
293292

294293
/**
295-
* @ngdoc function
296-
* @name ui.router.state.type:Transition#redirect
297-
* @methodOf ui.router.state.type:Transition
294+
* Creates a new transition that is a redirection of the current one.
298295
*
299-
* @description
300-
* Creates a new transition that is a redirection of the current one. This transition can
301-
* be returned from a `$transitionsProvider` hook, `$state` event, or other method, to
296+
* This transition can be returned from a [[TransitionService]] hook to
302297
* redirect a transition to a new state and/or set of parameters.
303298
*
304-
* @returns {Transition} Returns a new `Transition` instance.
299+
* @returns Returns a new [[Transition]] instance.
305300
*/
306301
redirect(targetState: TargetState): Transition {
307302
let newOptions = extend({}, this.options(), targetState.options(), { previous: this });
308303
targetState = new TargetState(targetState.identifier(), targetState.$state(), targetState.params(), newOptions);
309304

310-
let redirectTo = new Transition(this._treeChanges.from, targetState, this._transitionService);
311-
let reloadState = targetState.options().reloadState;
305+
let newTransition = new Transition(this._treeChanges.from, targetState, this._transitionService);
306+
let originalEnteringNodes = this.treeChanges().entering;
307+
let redirectEnteringNodes = newTransition.treeChanges().entering;
308+
309+
// --- Re-use resolve data from original transition ---
310+
// When redirecting from a parent state to a child state where the parent parameter values haven't changed
311+
// (because of the redirect), the resolves fetched by the original transition are still valid in the
312+
// redirected transition.
313+
//
314+
// This allows you to define a redirect on a parent state which depends on an async resolve value.
315+
// You can wait for the resolve, then redirect to a child state based on the result.
316+
// The redirected transition does not have to re-fetch the resolve.
317+
// ---------------------------------------------------------
318+
319+
const nodeIsReloading = (reloadState: State) => (node: Node) => {
320+
return reloadState && reloadState.includes[node.state.name];
321+
};
322+
323+
// Find any "entering" nodes in the redirect path that match the original path and aren't being reloaded
324+
let matchingEnteringNodes: Node[] = Node.matching(redirectEnteringNodes, originalEnteringNodes)
325+
.filter(not(nodeIsReloading(targetState.options().reloadState)));
312326

313-
// If the current transition has already resolved any resolvables which are also in the redirected "to path", then
314-
// add those resolvables to the redirected transition. Allows you to define a resolve at a parent level, wait for
315-
// the resolve, then redirect to a child state based on the result, and not have to re-fetch the resolve.
316-
let redirectedPath = this.treeChanges().to;
317-
let copyResolvesFor: Node[] = Node.matching(redirectTo.treeChanges().to, redirectedPath)
318-
.filter(node => !reloadState || !reloadState.includes[node.state.name]);
319-
const includeResolve = (resolve, key) => ['$stateParams', '$transition$'].indexOf(key) === -1;
320-
copyResolvesFor.forEach((node, idx) => extend(node.resolves, filter(redirectedPath[idx].resolves, includeResolve)));
327+
// Use the existing (possibly pre-resolved) resolvables for the matching entering nodes.
328+
matchingEnteringNodes.forEach((node, idx) => {
329+
node.resolvables = originalEnteringNodes[idx].resolvables;
330+
});
321331

322-
return redirectTo;
332+
return newTransition;
323333
}
324334

325335
/** @hidden If a transition doesn't exit/enter any states, returns any [[Param]] whose value changed */

test/ng1/stateSpec.js

+2-14
Original file line numberDiff line numberDiff line change
@@ -1474,19 +1474,6 @@ describe('state', function () {
14741474
}));
14751475
});
14761476

1477-
describe('default properties', function() {
1478-
it('should always have a name', inject(function ($state, $q) {
1479-
$state.transitionTo(A);
1480-
$q.flush();
1481-
expect($state.$current.name).toBe('A');
1482-
expect($state.$current.toString()).toBe('A');
1483-
}));
1484-
1485-
it('should always have a resolve object', inject(function ($state) {
1486-
expect($state.$current.resolve).toEqual({});
1487-
}));
1488-
});
1489-
14901477
describe('"data" property inheritance/override', function () {
14911478
it('should stay immutable for if state doesn\'t have parent', inject(function ($state) {
14921479
initStateTo(H);
@@ -1554,7 +1541,8 @@ describe('state', function () {
15541541
}));
15551542

15561543
it('should always have a resolve object', inject(function ($state) {
1557-
expect($state.$current.resolve).toEqual({});
1544+
expect($state.$current.resolve).toBeDefined();
1545+
expect(typeof $state.$current.resolve).toBe('object');
15581546
}));
15591547

15601548
it('should include itself and parent states', inject(function ($state, $q) {

0 commit comments

Comments
 (0)