Skip to content

Commit 6743a60

Browse files
feat(Resolve): Switch state.resolve to be an array of Resolvables
This is a prerequisite to supporting ng2 providers BC-BREAK: - Removed the built-in `$resolve$` resolve value, added in a previous alpha BC-BREAK: - `Transition.addResolves()` replaced with `Transition.addResolvable()` BC-BREAK: - The (private API) State object's .resolve property is now pre-processed as an array of Resolvables using statebuilder
1 parent 99e07b2 commit 6743a60

15 files changed

+106
-71
lines changed

src/common/hof.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* @module common_hof
55
*/
66

7+
import {Predicate} from "./common";
78
/**
89
* Returns a new function for [Partial Application](https://en.wikipedia.org/wiki/Partial_application) of the original function.
910
*
@@ -126,15 +127,15 @@ export const not = (fn) => (...args) => !fn.apply(null, args);
126127
* Given two functions that return truthy or falsey values, returns a function that returns truthy
127128
* if both functions return truthy for the given arguments
128129
*/
129-
export function and(fn1, fn2): Function {
130+
export function and(fn1, fn2): Predicate<any> {
130131
return (...args) => fn1.apply(null, args) && fn2.apply(null, args);
131132
}
132133

133134
/**
134135
* Given two functions that return truthy or falsey values, returns a function that returns truthy
135136
* if at least one of the functions returns truthy for the given arguments
136137
*/
137-
export function or(fn1, fn2): Function {
138+
export function or(fn1, fn2): Predicate<any> {
138139
return (...args) => fn1.apply(null, args) || fn2.apply(null, args);
139140
}
140141

src/common/strings.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ export function fnToString(fn: IInjectable) {
6565
return _fn && _fn.toString() || "undefined";
6666
}
6767

68-
const isTransitionRejectionPromise = Rejection.isTransitionRejectionPromise;
69-
7068
let stringifyPatternFn = null;
7169
let stringifyPattern = function(value) {
70+
let isTransitionRejectionPromise = Rejection.isTransitionRejectionPromise;
71+
7272
stringifyPatternFn = stringifyPatternFn || pattern([
7373
[not(isDefined), val("undefined")],
7474
[isNull, val("null")],

src/ng1/legacy/resolveService.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {State} from "../../state/stateObject";
22
import {PathNode} from "../../path/node";
33
import {ResolveContext} from "../../resolve/resolveContext";
4-
import {Resolvable} from "../../resolve/resolvable";
54
import {map} from "../../common/common";
5+
import {makeResolvables} from "../statebuilders/resolve";
66

77
export const resolveFactory = () => ({
88
/**
@@ -12,14 +12,14 @@ export const resolveFactory = () => ({
1212
* @param parent a promise for a "parent resolve"
1313
*/
1414
resolve: (invocables, locals = {}, parent?) => {
15-
let parentNode = new PathNode(new State(<any> { params: {} }));
16-
let node = new PathNode(new State(<any> { params: {} }));
15+
let parentNode = new PathNode(new State(<any> { params: {}, resolve: [] }));
16+
let node = new PathNode(new State(<any> { params: {}, resolve: [] }));
1717
let context = new ResolveContext([parentNode, node]);
1818

19-
context.addResolvables(Resolvable.makeResolvables(invocables), node.state);
19+
context.addResolvables(makeResolvables(invocables), node.state);
2020

2121
const resolveData = (parentLocals) => {
22-
const rewrap = _locals => Resolvable.makeResolvables(<any> map(_locals, local => () => local));
22+
const rewrap = _locals => makeResolvables(<any> map(_locals, local => () => local));
2323
context.addResolvables(rewrap(parentLocals), parentNode.state);
2424
context.addResolvables(rewrap(locals), node.state);
2525
return context.resolvePath();

src/ng1/statebuilders/resolve.ts

+28-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/** @module ng1 */ /** */
22
import {State} from "../../state/stateObject";
3-
import {forEach} from "../../common/common";
4-
import {isString} from "../../common/predicates";
3+
import {isObject, isString, isInjectable} from "../../common/predicates";
4+
import {Resolvable} from "../../resolve/resolvable";
5+
import {services} from "../../common/coreservices";
56

67
/**
78
* This is a [[StateBuilder.builder]] function for angular1 `resolve:` block on a [[Ng1StateDeclaration]].
@@ -10,9 +11,29 @@ import {isString} from "../../common/predicates";
1011
* handles the `resolve` property with logic specific to angular-ui-router (ng1).
1112
*/
1213
export function ng1ResolveBuilder(state: State) {
13-
let resolve = {};
14-
forEach(state.resolve || {}, function (resolveFn, name: string) {
15-
resolve[name] = isString(resolveFn) ? [ resolveFn, x => x ] : resolveFn;
16-
});
17-
return resolve;
14+
return isObject(state.resolve) ? makeResolvables(state.resolve) : [];
15+
}
16+
17+
/** Validates the result map as a "resolve:" style object, then transforms the resolves into Resolvable[] */
18+
export function makeResolvables(resolves: { [key: string]: Function; }): Resolvable[] {
19+
// desugar ng1 sugar to create a resolve that is a service
20+
// e.g., resolve: { myService: 'myService' }
21+
const resolveServiceFromString = tuple => {
22+
if (!isString(tuple.val)) return tuple;
23+
24+
injectService.$inject = [tuple.val];
25+
function injectService(svc) { return svc; }
26+
return { key: tuple.key, val: injectService };
27+
};
28+
29+
// Convert from object to tuple array
30+
let tuples = Object.keys(resolves).map(key => ({key, val: resolves[key]})).map(resolveServiceFromString);
31+
32+
// If a hook result is an object, it should be a map of strings to (functions|strings).
33+
let invalid = tuples.filter(tuple => !isInjectable(tuple.val));
34+
if (invalid.length)
35+
throw new Error(`Invalid resolve key/value: ${invalid[0].key}/${invalid[0].val}`);
36+
37+
const deps = (resolveFn) => services.$injector.annotate(resolveFn, services.$injector.strictDi);
38+
return tuples.map(tuple => new Resolvable(tuple.key, tuple.val, deps(tuple.val)));
1839
}

src/path/node.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class PathNode {
4646
this.state = state;
4747
this.paramSchema = state.parameters({ inherit: false });
4848
this.paramValues = {};
49-
this.resolvables = Object.keys(state.resolve || {}).map(key => new Resolvable(key, state.resolve[key]));
49+
this.resolvables = state.resolve.map(res => res.clone());
5050
}
5151
}
5252

src/resolve/resolvable.ts

+35-22
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
/** @module resolve */ /** for typedoc */
2-
import {extend, pick, map, filter} from "../common/common";
3-
import {not} from "../common/hof";
4-
import {isInjectable} from "../common/predicates";
2+
import {pick, map, extend} from "../common/common";
53

64
import {services} from "../common/coreservices";
75
import {trace} from "../common/trace";
86
import {Resolvables, IOptions1} from "./interface";
97

108
import {ResolveContext} from "./resolveContext";
9+
import {stringify} from "../common/strings";
1110

1211
/**
1312
* The basic building block for the resolve system.
@@ -22,18 +21,39 @@ import {ResolveContext} from "./resolveContext";
2221
* parameter to those fns.
2322
*/
2423
export class Resolvable {
25-
name: string;
24+
token: any;
2625
resolveFn: Function;
2726
deps: string[];
2827

2928
promise: Promise<any> = undefined;
29+
resolved: boolean = false;
3030
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;
31+
32+
/**
33+
* This constructor creates a Resolvable copy
34+
*/
35+
constructor(resolvable: Resolvable)
36+
37+
/**
38+
* This constructor creates a new `Resolvable`
39+
*
40+
* @param token The new resolvable's injection token, such as `"userList"` (a string) or `UserService` (a class).
41+
* When this token is used during injection, the resolved value will be injected.
42+
* @param resolveFn The function that returns the resolved value, or a promise for the resolved value
43+
* @param deps An array of dependencies, which will be injected into the `resolveFn`
44+
* @param data Pre-resolved data. If the resolve value is already known, it may be provided here.
45+
*/
46+
constructor(token: any, resolveFn: Function, deps?: any[], data?: any)
47+
constructor(token, resolveFn?: Function, deps?: any[], data?: any) {
48+
if (token instanceof Resolvable) {
49+
extend(this, token);
50+
} else {
51+
this.token = token;
52+
this.resolveFn = resolveFn;
53+
this.deps = deps;
54+
this.data = data;
55+
this.resolved = data !== undefined;
56+
}
3757
}
3858

3959
// synchronous part:
@@ -48,15 +68,15 @@ export class Resolvable {
4868
// - store unwrapped data
4969
// - resolve the Resolvable's promise
5070
resolveResolvable(resolveContext: ResolveContext, options: IOptions1 = {}) {
51-
let {name, deps, resolveFn} = this;
71+
let {deps, resolveFn} = this;
5272

5373
trace.traceResolveResolvable(this, options);
5474
// First, set up an overall deferred/promise for this Resolvable
5575
let deferred = services.$q.defer();
5676
this.promise = deferred.promise;
5777
// Load a map of all resolvables for this state from the context path
5878
// Omit the current Resolvable from the result, so we don't try to inject this into this
59-
let ancestorsByName: Resolvables = resolveContext.getResolvables(null, { omitOwnLocals: [ name ] });
79+
let ancestorsByName: Resolvables = resolveContext.getResolvables(null, { omitOwnLocals: [ this.token ] });
6080

6181
// Limit the ancestors Resolvables map to only those that the current Resolvable fn's annotations depends on
6282
let depResolvables: Resolvables = <any> pick(ancestorsByName, deps);
@@ -86,17 +106,10 @@ export class Resolvable {
86106
}
87107

88108
toString() {
89-
return `Resolvable(name: ${this.name}, requires: [${this.deps}])`;
109+
return `Resolvable(token: ${stringify(this.token)}, requires: [${this.deps.map(stringify)}])`;
90110
}
91111

92-
/**
93-
* Validates the result map as a "resolve:" style object, then transforms the resolves into Resolvable[]
94-
*/
95-
static makeResolvables(resolves: { [key: string]: Function; }): Resolvable[] {
96-
// If a hook result is an object, it should be a map of strings to functions.
97-
let invalid = filter(resolves, not(isInjectable)), keys = Object.keys(invalid);
98-
if (keys.length)
99-
throw new Error(`Invalid resolve key/value: ${keys[0]}/${invalid[keys[0]]}`);
100-
return Object.keys(resolves).map(key => new Resolvable(key, resolves[key]));
112+
clone(): Resolvable {
113+
return new Resolvable(this);
101114
}
102115
}

src/resolve/resolveContext.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ export class ResolveContext {
6666
let omitProps = (node === last) ? options.omitOwnLocals : [];
6767

6868
let filteredResolvables = node.resolvables
69-
.filter(r => omitProps.indexOf(r.name) === -1)
70-
.reduce((acc, r) => { acc[r.name] = r; return acc; }, {});
69+
.filter(r => omitProps.indexOf(r.token) === -1)
70+
.reduce((acc, r) => { acc[r.token] = r; return acc; }, {});
7171

7272
return extend(memo, filteredResolvables);
7373
}, <Resolvables> {});
@@ -85,14 +85,14 @@ export class ResolveContext {
8585

8686
addResolvables(newResolvables: Resolvable[], state: State) {
8787
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);
88+
var keys = newResolvables.map(r => r.token);
89+
node.resolvables = node.resolvables.filter(r => keys.indexOf(r.token) === -1).concat(newResolvables);
9090
}
9191

9292
/** Gets the resolvables declared on a particular state */
9393
getOwnResolvables(state: State): Resolvables {
9494
return this._nodeFor(state).resolvables
95-
.reduce((acc, r) => { acc[r.name] = r; return acc; }, <Resolvables>{});
95+
.reduce((acc, r) => { acc[r.token] = r; return acc; }, <Resolvables>{});
9696
}
9797

9898
// Returns a promise for an array of resolved path Element promises
@@ -200,6 +200,6 @@ function getPolicy(stateResolvePolicyConf, resolvable: Resolvable): number {
200200
// Normalize the configuration on the state to either state-level (a string) or resolve-level (a Map of string:string)
201201
let stateLevelPolicy: string = <string> (isString(stateResolvePolicyConf) ? stateResolvePolicyConf : null);
202202
let resolveLevelPolicies: IPolicies = <any> (isObject(stateResolvePolicyConf) ? stateResolvePolicyConf : {});
203-
let policyName = resolveLevelPolicies[resolvable.name] || stateLevelPolicy || defaultResolvePolicy;
203+
let policyName = resolveLevelPolicies[resolvable.token] || stateLevelPolicy || defaultResolvePolicy;
204204
return ResolvePolicy[policyName];
205205
}

src/state/hooks/resolveHooks.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,10 @@ export class ResolveHooks {
3636

3737
// A new Resolvable contains all the resolved data in this context as a single object, for injection as `$resolve$`
3838
let context = node.resolveContext;
39-
let $resolve$ = new Resolvable("$resolve$", () => map(context.getResolvables(), (r: Resolvable) => r.data));
4039
var options = extend({ transition: transition }, { resolvePolicy: LAZY });
4140

42-
// Resolve all the LAZY resolves, then resolve the `$resolve$` object, then add `$resolve$` to the context
43-
// return context.resolvePathElement(node.state, options)
44-
return context.resolvePath(options)
45-
.then(() => $resolve$.resolveResolvable(context))
46-
.then(() => context.addResolvables([$resolve$], node.state));
41+
// Resolve all the LAZY resolves
42+
return context.resolvePath(options);
4743
}
4844

4945
// Resolve eager resolvables before when the transition starts

src/state/stateQueueManager.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class StateQueueManager {
2323
// @TODO: state = new State(extend({}, config, { ... }))
2424
let state = inherit(new State(), extend({}, config, {
2525
self: config,
26-
resolve: config.resolve || {},
26+
resolve: config.resolve || [],
2727
toString: () => config.name
2828
}));
2929

src/transition/transition.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ export class Transition implements IHookRegistry {
154154
PathFactory.bindResolveContexts(this._treeChanges.to);
155155

156156
let rootResolvables: Resolvable[] = [
157-
new Resolvable('$transition$', () => this, this),
158-
new Resolvable('$stateParams', () => this.params(), this.params())
157+
new Resolvable('$transition$', () => this, [], this),
158+
new Resolvable('$stateParams', () => this.params(), [], this.params())
159159
];
160160
let rootNode: PathNode = this._treeChanges.to[0];
161161
rootNode.resolveContext.addResolvables(rootResolvables, rootNode.state)
@@ -223,16 +223,16 @@ export class Transition implements IHookRegistry {
223223
}
224224

225225
/**
226-
* Adds new resolves to this transition.
226+
* Adds a new [[Resolvable]] (`resolve`) to this transition.
227227
*
228-
* @param resolves an [[ResolveDeclarations]] object which describes the new resolves
229-
* @param state the state in the "to path" which should receive the new resolves (otherwise, the root state)
228+
* @param resolvable an [[Resolvable]] object
229+
* @param state the state in the "to path" which should receive the new resolve (otherwise, the root state)
230230
*/
231-
addResolves(resolves: { [key: string]: Function }, state: StateOrName = ""): void {
231+
addResolvable(resolvable: Resolvable, state: StateOrName = ""): void {
232232
let stateName: string = (typeof state === "string") ? state : state.name;
233233
let topath = this._treeChanges.to;
234234
let targetNode = find(topath, node => node.state.name === stateName);
235-
tail(topath).resolveContext.addResolvables(Resolvable.makeResolvables(resolves), targetNode.state);
235+
tail(topath).resolveContext.addResolvables([resolvable], targetNode.state);
236236
}
237237

238238
/**

test/core/resolveSpec.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import "../matchers.ts"
44

55
import {
6-
ResolveContext, State, PathNode, PathFactory
6+
ResolveContext, State, PathNode, PathFactory, Resolvable
77
} from "../../src/core";
88

99
import {
1010
omit, map, filter, pick, forEach, prop, copy
1111
} from "../../src/core";
1212

1313
import Spy = jasmine.Spy;
14+
import {services} from "../../src/common/coreservices";
1415

1516
///////////////////////////////////////////////
1617

@@ -66,7 +67,11 @@ beforeEach(function () {
6667
function loadStates(parent, state, name) {
6768
var thisState = pick.apply(null, [state].concat(stateProps));
6869
var substates = omit.apply(null, [state].concat(stateProps));
70+
var resolve = thisState.resolve || {};
71+
var injector = services.$injector;
6972

73+
thisState.resolve = Object.keys(resolve)
74+
.map(key => new Resolvable(key, resolve[key], injector.annotate(resolve[key])));
7075
thisState.template = thisState.template || "empty";
7176
thisState.name = name;
7277
thisState.parent = parent.name;

test/ng1/ng1StateBuilderSpec.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {StateBuilder, StateMatcher, ng1ResolveBuilder, ng1ViewsBuilder} from "../../src/ng1";
2+
import {Resolvable} from "../../src/resolve/resolvable";
23

34
describe('Ng1 StateBuilder', function() {
45
var builder, matcher, urlMatcherFactoryProvider: any = {
@@ -32,7 +33,7 @@ describe('Ng1 StateBuilder', function() {
3233
var config = { resolve: { foo: "bar" } };
3334
var locals = { "bar": 123 };
3435
expect(builder.builder('resolve')).toBeDefined();
35-
var built = builder.builder('resolve')(config);
36-
expect($injector.invoke(built.foo, null, locals)).toBe(123);
36+
var built: Resolvable[] = builder.builder('resolve')(config);
37+
expect($injector.invoke(built[0].resolveFn, null, locals)).toBe(123);
3738
}));
3839
});

test/ng1/transitionSpec.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {TargetState} from "../../src/state/targetState";
1010
import {StateQueueManager} from "../../src/state/stateQueueManager";
1111
import {Rejection} from "../../src/transition/rejectFactory";
1212
import {ResolveHooks} from "../../src/state/hooks/resolveHooks";
13+
import {Resolvable} from "../../src/resolve/resolvable";
14+
import {Transition} from "../../src/transition/transition";
1315

1416
describe('transition', function () {
1517

@@ -444,11 +446,10 @@ describe('transition', function () {
444446
log.push("Entered#"+state.name);
445447
}, { priority: -1 });
446448

447-
transitionProvider.onEnter({ entering: "B" }, function addResolves($transition$) {
449+
transitionProvider.onEnter({ entering: "B" }, function addResolves($transition$: Transition) {
448450
log.push("adding resolve");
449-
$transition$.addResolves({
450-
newResolve: function () { log.push("resolving"); return defer.promise; }
451-
})
451+
var resolveFn = function () { log.push("resolving"); return defer.promise; };
452+
$transition$.addResolvable(new Resolvable('newResolve', resolveFn));
452453
});
453454

454455
transitionProvider.onEnter({ entering: "C" }, function useTheNewResolve(trans, inj) {

test/ng1/viewSpec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('view', function() {
3131
let registerState = curry(function(_states, stateBuilder, config) {
3232
let state = inherit(new State(), extend({}, config, {
3333
self: config,
34-
resolve: config.resolve || {}
34+
resolve: config.resolve || []
3535
}));
3636
let built: State = stateBuilder.build(state);
3737
return _states[built.name] = built;

0 commit comments

Comments
 (0)