ui-router
Version:
State-based routing for Javascript
185 lines (161 loc) • 8.3 kB
text/typescript
/** @module resolve */ /** 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";
import {PathFactory} from "../path/pathFactory";
// 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[] {
return PathFactory.subPath(this._path, state);
}
});
}
/**
* 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, options.bind || 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];
}