forked from angular-ui/ui-router
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathresolveContext.ts
187 lines (163 loc) · 8.21 KB
/
resolveContext.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/** @module path */ /** for typedoc */
import {IInjectable, find, filter, map, tail, defaults, extend, pick, omit} from "../common/common";
import {prop, propEq} from "../common/hof";
import {isString, isObject} from "../common/predicates";
import {trace} from "../common/trace";
import {services} from "../common/coreservices";
import {Resolvables, ResolvePolicy, IOptions1} from "./interface";
import {Node} from "../path/module";
import {Resolvable} from "./resolvable";
import {State} from "../state/module";
import {mergeR} from "../common/common";
// TODO: make this configurable
let defaultResolvePolicy = ResolvePolicy[ResolvePolicy.LAZY];
interface IPolicies { [key: string]: string; }
interface Promises { [key: string]: Promise<any>; }
export class ResolveContext {
private _nodeFor: Function;
private _pathTo: Function;
constructor(private _path: Node[]) {
extend(this, {
_nodeFor(state: State): Node {
return <Node> 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.
*
* @param state the State (within the ResolveContext's Path) for which to get resolvables
* @param options
*
* options.omitOwnLocals: array of property names
* Omits those Resolvables which are found on the last element of the path.
*
* This will hide a deepest-level resolvable (by name), potentially exposing a parent resolvable of
* the same name further up the state tree.
*
* This is used by Resolvable.resolve() in order to provide the Resolvable access to all the other
* Resolvables at its own PathElement level, yet disallow that Resolvable access to its own injectable Resolvable.
*
* This is also used to allow a state to override a parent state's resolve while also injecting
* that parent state's resolve:
*
* state({ name: 'G', resolve: { _G: function() { return "G"; } } });
* state({ name: 'G.G2', resolve: { _G: function(_G) { return _G + "G2"; } } });
* where injecting _G into a controller will yield "GG2"
*/
getResolvables(state?: State, options?: any): Resolvables {
options = defaults(options, { omitOwnLocals: [] });
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.resolves, omitProps);
return extend(memo, filteredResolvables);
}, <Resolvables> {});
}
/** Inspects a function `fn` for its dependencies. Returns an object containing any matching Resolvables */
getResolvablesForFn(fn: IInjectable): { [key: string]: Resolvable } {
let deps = services.$injector.annotate(<Function> fn, services.$injector.strictDi);
return <any> pick(this.getResolvables(), deps);
}
isolateRootTo(state: State): ResolveContext {
return new ResolveContext(this._pathTo(state));
}
addResolvables(resolvables: Resolvables, state: State) {
extend(this._nodeFor(state).resolves, resolvables);
}
/** Gets the resolvables declared on a particular state */
getOwnResolvables(state: State): Resolvables {
return extend({}, this._nodeFor(state).resolves);
}
// Returns a promise for an array of resolved path Element promises
resolvePath(options: IOptions1 = {}): Promise<any> {
trace.traceResolvePath(this._path, options);
const promiseForNode = (node: Node) => this.resolvePathElement(node.state, options);
return services.$q.all(<any> map(this._path, promiseForNode)).then(all => all.reduce(mergeR, {}));
}
// 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: State, options: IOptions1 = {}): Promise<any> {
// 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];
// Get path Resolvables available to this element
let resolvables = this.getOwnResolvables(state);
const matchesRequestedPolicy = resolvable => getPolicy(state.resolvePolicy, resolvable) >= policyOrdinal;
let matchingResolves = filter(resolvables, matchesRequestedPolicy);
const getResolvePromise = (resolvable: Resolvable) => resolvable.get(this.isolateRootTo(state), options);
let resolvablePromises: Promises = <any> map(matchingResolves, getResolvePromise);
trace.traceResolvePathElement(this, matchingResolves, options);
return services.$q.all(resolvablePromises);
}
/**
* 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
* the function.
*
* @return a promise for the return value of the function.
*
* @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(fn: IInjectable, locals: any = {}, options: IOptions1 = {}): Promise<any> {
let resolvables = this.getResolvablesForFn(fn);
trace.tracePathElementInvoke(tail(this._path), fn, Object.keys(resolvables), extend({when: "Later"}, options));
const getPromise = (resolvable: Resolvable) => resolvable.get(this, options);
let promises: Promises = <any> map(resolvables, getPromise);
return services.$q.all(promises).then(() => {
try {
return this.invokeNow(fn, locals, options);
} catch (error) {
return services.$q.reject(error);
}
});
}
/**
* 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 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(fn: IInjectable, locals: any, options: any = {}) {
let resolvables = this.getResolvablesForFn(fn);
trace.tracePathElementInvoke(tail(this._path), fn, Object.keys(resolvables), extend({when: "Now "}, options));
let resolvedLocals = map(resolvables, prop("data"));
return services.$injector.invoke(<Function> fn, null, extend({}, locals, resolvedLocals));
}
}
/**
* Given a state's resolvePolicy attribute and a resolvable from that state, returns the policy ordinal for the Resolvable
* Use the policy declared for the Resolve. If undefined, use the policy declared for the State. If
* undefined, use the system defaultResolvePolicy.
*
* @param stateResolvePolicyConf The raw resolvePolicy declaration on the state object; may be a String or Object
* @param resolvable The resolvable to compute the policy for
*/
function getPolicy(stateResolvePolicyConf, resolvable: Resolvable): number {
// Normalize the configuration on the state to either state-level (a string) or resolve-level (a Map of string:string)
let stateLevelPolicy: string = <string> (isString(stateResolvePolicyConf) ? stateResolvePolicyConf : null);
let resolveLevelPolicies: IPolicies = <any> (isObject(stateResolvePolicyConf) ? stateResolvePolicyConf : {});
let policyName = resolveLevelPolicies[resolvable.name] || stateLevelPolicy || defaultResolvePolicy;
return ResolvePolicy[policyName];
}