Skip to content

Commit a7e5ea6

Browse files
feat(Resolve): support ng2-like provide object literals
`{ provide: token, useFactory: (Http) => http.get('data'), deps: [Http] }` fix(Resolve): fix injection of resolves using array notation refactor(path): PathFactory.subPath now takes a predicate, not a State fix(Resolvable): create the resolvable promise when pre-resolving data (useValue) feat(Resolvable): validate token and resolveFn in constructor refactor(Resolvable): Switch dependency injection from object `locals` to fn.apply(null, arrayDeps) feat(ResolveContext): getDependencies(Resolvable) uses path*.resolvables or root $injector refactor(State): use `resolvables` StateBuilder to create property for `Resolvable[]` refactor(StateBuilder): extract builder functions
1 parent bd0e3a3 commit a7e5ea6

16 files changed

+244
-188
lines changed

src/justjs.ts

+9-12
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
*/ /** */
66
export * from "./core";
77
import {services} from "./common/coreservices";
8-
import {isDefined, isFunction, isArray, isObject, isInjectable} from "./common/predicates";
9-
import {extend, assertPredicate, forEach, applyPairs} from "./common/common";
8+
import {stringify} from "./common/strings";
9+
import {isFunction, isArray, isObject, isInjectable} from "./common/predicates";
10+
import {extend, assertPredicate} from "./common/common";
1011

1112
/** $q-like promise api */
1213
services.$q = (executor: (resolve, reject) => void) => new Promise(executor);
@@ -28,20 +29,16 @@ services.$q.all = function (promises: { [key: string]: Promise<any> } | Promise<
2829
}
2930

3031
if (isObject(promises)) {
31-
// console.log("$q.all({}) Input:", promises);
32-
3332
// Convert promises map to promises array.
3433
// When each promise resolves, map it to a tuple { key: key, val: val }
35-
let chain = Object.keys(promises)
34+
let objectToTuples = Object.keys(promises)
3635
.map(key => promises[key].then(val => ({key, val})));
3736

38-
// Then wait for all promises to resolve, and convert them back to an object
39-
return services.$q.all(chain).then(values => {
40-
let value = values.reduce((acc, tuple) => { acc[tuple.key] = tuple.val; return acc; }, {});
37+
const tuplesToObject = values =>
38+
values.reduce((acc, tuple) => { acc[tuple.key] = tuple.val; return acc; }, {});
4139

42-
// console.log("$q.all({}) Output:", value);
43-
return value;
44-
});
40+
// Then wait for all promises to resolve, and convert them back to an object
41+
return services.$q.all(objectToTuples).then(tuplesToObject);
4542
}
4643
};
4744

@@ -60,7 +57,7 @@ services.$injector.has = (name) => services.$injector.get(name) != null;
6057
services.$injector.invoke = function(fn, context?, locals?) {
6158
let all = extend({}, globals, locals || {});
6259
let params = services.$injector.annotate(fn);
63-
let ensureExist = assertPredicate(key => all.hasOwnProperty(key), key => `DI can't find injectable: '${key}'`);
60+
let ensureExist = assertPredicate(key => all.hasOwnProperty(key), key => `Could not find Dependency Injection token: ${stringify(key)}`);
6461
let args = params.filter(ensureExist).map(x => all[x]);
6562
if (isFunction(fn)) return fn.apply(context, args);
6663
return fn.slice(-1)[0].apply(context, args);

src/ng1.ts

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export * from "./core";
88

99
export * from "./ng1/services";
1010
export * from "./ng1/statebuilders/views";
11-
export * from "./ng1/statebuilders/resolve";
1211

1312
import "./ng1/directives/stateDirectives";
1413
import "./ng1/stateFilters";

src/ng1/legacy/resolveService.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {State} from "../../state/stateObject";
22
import {PathNode} from "../../path/node";
33
import {ResolveContext} from "../../resolve/resolveContext";
44
import {map} from "../../common/common";
5-
import {makeResolvables} from "../statebuilders/resolve";
5+
import {resolvablesBuilder} from "../../state/stateBuilder";
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: {}, resolve: [] }));
16-
let node = new PathNode(new State(<any> { params: {}, resolve: [] }));
15+
let parentNode = new PathNode(new State(<any> { params: {}, resolvables: [] }));
16+
let node = new PathNode(new State(<any> { params: {}, resolvables: [] }));
1717
let context = new ResolveContext([parentNode, node]);
1818

19-
context.addResolvables(makeResolvables(invocables), node.state);
19+
context.addResolvables(resolvablesBuilder(<any> { resolve: invocables }), node.state);
2020

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

src/ng1/services.ts

-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {resolveFactory} from "./legacy/resolveService";
2020
import {trace} from "../common/trace";
2121
import {ng1ViewsBuilder, ng1ViewConfigFactory, Ng1ViewConfig} from "./statebuilders/views";
2222
import {TemplateFactory} from "./templateFactory";
23-
import {ng1ResolveBuilder} from "./statebuilders/resolve";
2423
import {StateParams} from "../params/stateParams";
2524
import {TransitionService} from "../transition/transitionService";
2625
import {StateService} from "../state/stateService";
@@ -168,7 +167,6 @@ function ng1UIRouter($locationProvider) {
168167

169168
// Apply ng1 specific StateBuilder code for `views`, `resolve`, and `onExit/Retain/Enter` properties
170169
router.stateRegistry.decorator("views", ng1ViewsBuilder);
171-
router.stateRegistry.decorator("resolve", ng1ResolveBuilder);
172170
router.stateRegistry.decorator("onExit", getStateHookBuilder("onExit"));
173171
router.stateRegistry.decorator("onRetain", getStateHookBuilder("onRetain"));
174172
router.stateRegistry.decorator("onEnter", getStateHookBuilder("onEnter"));

src/ng1/statebuilders/resolve.ts

-39
This file was deleted.

src/ng2/directives/uiSrefStatus.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export class UiSrefStatus {
122122
* Then appends each to [a,b,] and returns: [a, b, c], [a, b, c, d]
123123
*/
124124
function spreadToSubPaths (path: PathNode[], appendTo: PathNode[] = []): PathNode[][] {
125-
return path.map(node => appendTo.concat(PathFactory.subPath(path, node.state)));
125+
return path.map(node => appendTo.concat(PathFactory.subPath(path, n => n.state === node.state)));
126126
}
127127

128128
let tc: TreeChanges = $transition$.treeChanges();

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 = state.resolve.map(res => res.clone());
49+
this.resolvables = state.resolvables.map(res => res.clone());
5050
}
5151
}
5252

src/path/pathFactory.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @module path */ /** for typedoc */
22

3-
import {extend, find, pick, omit, tail, mergeR, values, unnestR} from "../common/common";
3+
import {extend, find, pick, omit, tail, mergeR, values, unnestR, Predicate} from "../common/common";
44
import {prop, propEq, not} from "../common/hof";
55

66
import {RawParams} from "../params/interface";
@@ -138,18 +138,18 @@ export class PathFactory {
138138
}
139139

140140
/**
141-
* Find a subpath of a path that stops at the node for a given state
141+
* Return a subpath of a path, which stops at the first matching node
142142
*
143-
* Given an array of nodes, returns a subset of the array starting from the first node, up to the
144-
* node whose state matches `stateName`
143+
* Given an array of nodes, returns a subset of the array starting from the first node,
144+
* stopping when the first node matches the predicate.
145145
*
146146
* @param path a path of [[PathNode]]s
147-
* @param state the [[State]] to stop at
147+
* @param predicate a [[Predicate]] fn that matches [[PathNode]]s
148148
*/
149-
static subPath(path: PathNode[], state): PathNode[] {
150-
let node = find(path, _node => _node.state === state);
149+
static subPath(path: PathNode[], predicate: Predicate<PathNode>): PathNode[] {
150+
let node = find(path, predicate);
151151
let elementIdx = path.indexOf(node);
152-
if (elementIdx === -1) throw new Error("The path does not contain the state: " + state);
152+
if (elementIdx === -1) throw new Error("The path does not contain a PathNode matching the predicate");
153153
return path.slice(0, elementIdx + 1);
154154
}
155155

src/resolve/resolvable.ts

+37-45
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {Resolvables, IOptions1} from "./interface";
77

88
import {ResolveContext} from "./resolveContext";
99
import {stringify} from "../common/strings";
10+
import {isFunction} from "../common/predicates";
1011

1112
/**
1213
* The basic building block for the resolve system.
@@ -48,61 +49,52 @@ export class Resolvable {
4849
if (token instanceof Resolvable) {
4950
extend(this, token);
5051
} else {
52+
if (token == null || token == undefined) throw new Error("new Resolvable(): token argument is required");
53+
if (!isFunction(resolveFn)) throw new Error("new Resolvable(): resolveFn argument must be a function");
54+
5155
this.token = token;
5256
this.resolveFn = resolveFn;
53-
this.deps = deps;
57+
this.deps = deps || [];
5458
this.data = data;
5559
this.resolved = data !== undefined;
60+
this.promise = this.resolved ? services.$q.when(this.data) : undefined;
5661
}
5762
}
5863

59-
// synchronous part:
60-
// - sets up the Resolvable's promise
61-
// - retrieves dependencies' promises
62-
// - returns promise for async part
63-
64-
// asynchronous part:
65-
// - wait for dependencies promises to resolve
66-
// - invoke the resolveFn
67-
// - wait for resolveFn promise to resolve
68-
// - store unwrapped data
69-
// - resolve the Resolvable's promise
70-
resolveResolvable(resolveContext: ResolveContext, options: IOptions1 = {}) {
71-
let {deps, resolveFn} = this;
72-
73-
trace.traceResolveResolvable(this, options);
74-
// First, set up an overall deferred/promise for this Resolvable
75-
let deferred = services.$q.defer();
76-
this.promise = deferred.promise;
77-
// Load a map of all resolvables for this state from the context path
78-
// Omit the current Resolvable from the result, so we don't try to inject this into this
79-
let ancestorsByName: Resolvables = resolveContext.getResolvables(null, { omitOwnLocals: [ this.token ] });
80-
81-
// Limit the ancestors Resolvables map to only those that the current Resolvable fn's annotations depends on
82-
let depResolvables: Resolvables = <any> pick(ancestorsByName, deps);
83-
84-
// Get promises (or synchronously invoke resolveFn) for deps
85-
let depPromises: any = map(depResolvables, (resolvable: Resolvable) => resolvable.get(resolveContext, options));
86-
87-
// Return a promise chain that waits for all the deps to resolve, then invokes the resolveFn passing in the
88-
// dependencies as locals, then unwraps the resulting promise's data.
89-
return services.$q.all(depPromises).then(locals => {
90-
try {
91-
let result = services.$injector.invoke(resolveFn, null, locals);
92-
deferred.resolve(result);
93-
} catch (error) {
94-
deferred.reject(error);
95-
}
96-
return this.promise;
97-
}).then(data => {
98-
this.data = data;
99-
trace.traceResolvableResolved(this, options);
100-
return this.promise;
101-
});
64+
/**
65+
* Asynchronously resolve this Resolvable's data
66+
*
67+
* Given a ResolveContext that this Resolvable is found in:
68+
* Wait for this Resolvable's dependencies, then invoke this Resolvable's function
69+
* and update the Resolvable's state
70+
*/
71+
resolve(resolveContext: ResolveContext, options: IOptions1 = {}) {
72+
let $q = services.$q;
73+
return this.promise = $q.when()
74+
// Get all dependencies from ResolveContext and wait for them to be resolved
75+
.then(() =>
76+
$q.all(resolveContext.getDependencies(this).map(r =>
77+
r.get(resolveContext, options))))
78+
// Invoke the resolve function passing the resolved dependencies
79+
.then(resolvedDeps =>
80+
this.resolveFn.apply(null, resolvedDeps))
81+
// Wait for returned promise to resolve then update the Resolvable state
82+
.then(resolvedValue => {
83+
this.data = resolvedValue;
84+
this.resolved = true;
85+
trace.traceResolvableResolved(this, options);
86+
return this.data;
87+
});
10288
}
10389

90+
/**
91+
* Gets a promise for this Resolvable's data.
92+
*
93+
* Fetches the data and returns a promise.
94+
* Returns the existing promise if it has already been fetched once.
95+
*/
10496
get(resolveContext: ResolveContext, options?: IOptions1): Promise<any> {
105-
return this.promise || this.resolveResolvable(resolveContext, options);
97+
return this.promise || this.resolve(resolveContext, options);
10698
}
10799

108100
toString() {

src/resolve/resolveContext.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {Resolvable} from "./resolvable";
1111
import {State} from "../state/stateObject";
1212
import {mergeR} from "../common/common";
1313
import {PathFactory} from "../path/pathFactory";
14+
import {stringify} from "../common/strings";
1415

1516
// TODO: make this configurable
1617
let defaultResolvePolicy = ResolvePolicy[ResolvePolicy.LAZY];
@@ -29,7 +30,7 @@ export class ResolveContext {
2930
return <PathNode> find(this._path, propEq('state', state));
3031
},
3132
_pathTo(state: State): PathNode[] {
32-
return PathFactory.subPath(this._path, state);
33+
return PathFactory.subPath(this._path, node => node.state === state);
3334
}
3435
});
3536
}
@@ -186,6 +187,27 @@ export class ResolveContext {
186187

187188
return {get};
188189
}
190+
191+
getDependencies(resolvable: Resolvable): Resolvable[] {
192+
// predicate that finds the node the resolvable belongs to
193+
const nodeForResolvable = node => node.resolvables.indexOf(resolvable) !== -1;
194+
// Find which other resolvables are "visible" to the `resolvable` argument
195+
var availableResolvables: Resolvable[] = PathFactory.subPath(this._path, nodeForResolvable) // subpath stopping at resolvable's node
196+
.reduce((acc, node) => acc.concat(node.resolvables), []) //all of subpath's resolvables
197+
.filter(res => res !== resolvable); // filter out the `resolvable` arg
198+
199+
const getDependency = token => {
200+
let matching = availableResolvables.filter(r => r.token === token);
201+
if (matching.length) return tail(matching);
202+
203+
let fromInjector = services.$injector.get(token);
204+
if (!fromInjector) throw new Error("Could not find Dependency Injection token: " + stringify(token));
205+
206+
return new Resolvable(token, () => fromInjector, [], fromInjector);
207+
};
208+
209+
return resolvable.deps.map(getDependency);
210+
}
189211
}
190212

191213
/**

0 commit comments

Comments
 (0)