Skip to content

Commit

Permalink
feat(Resolve): Switch state.resolve to be an array of Resolvables
Browse files Browse the repository at this point in the history
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
  • Loading branch information
christopherthielen committed Jun 10, 2016
1 parent 99e07b2 commit 6743a60
Show file tree
Hide file tree
Showing 15 changed files with 106 additions and 71 deletions.
5 changes: 3 additions & 2 deletions src/common/hof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* @module common_hof
*/

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

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

Expand Down
4 changes: 2 additions & 2 deletions src/common/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ export function fnToString(fn: IInjectable) {
return _fn && _fn.toString() || "undefined";
}

const isTransitionRejectionPromise = Rejection.isTransitionRejectionPromise;

let stringifyPatternFn = null;
let stringifyPattern = function(value) {
let isTransitionRejectionPromise = Rejection.isTransitionRejectionPromise;

stringifyPatternFn = stringifyPatternFn || pattern([
[not(isDefined), val("undefined")],
[isNull, val("null")],
Expand Down
10 changes: 5 additions & 5 deletions src/ng1/legacy/resolveService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {State} from "../../state/stateObject";
import {PathNode} from "../../path/node";
import {ResolveContext} from "../../resolve/resolveContext";
import {Resolvable} from "../../resolve/resolvable";
import {map} from "../../common/common";
import {makeResolvables} from "../statebuilders/resolve";

export const resolveFactory = () => ({
/**
Expand All @@ -12,14 +12,14 @@ export const resolveFactory = () => ({
* @param parent a promise for a "parent resolve"
*/
resolve: (invocables, locals = {}, parent?) => {
let parentNode = new PathNode(new State(<any> { params: {} }));
let node = new PathNode(new State(<any> { params: {} }));
let parentNode = new PathNode(new State(<any> { params: {}, resolve: [] }));
let node = new PathNode(new State(<any> { params: {}, resolve: [] }));
let context = new ResolveContext([parentNode, node]);

context.addResolvables(Resolvable.makeResolvables(invocables), node.state);
context.addResolvables(makeResolvables(invocables), node.state);

const resolveData = (parentLocals) => {
const rewrap = _locals => Resolvable.makeResolvables(<any> map(_locals, local => () => local));
const rewrap = _locals => makeResolvables(<any> map(_locals, local => () => local));
context.addResolvables(rewrap(parentLocals), parentNode.state);
context.addResolvables(rewrap(locals), node.state);
return context.resolvePath();
Expand Down
35 changes: 28 additions & 7 deletions src/ng1/statebuilders/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/** @module ng1 */ /** */
import {State} from "../../state/stateObject";
import {forEach} from "../../common/common";
import {isString} from "../../common/predicates";
import {isObject, isString, isInjectable} from "../../common/predicates";
import {Resolvable} from "../../resolve/resolvable";
import {services} from "../../common/coreservices";

/**
* This is a [[StateBuilder.builder]] function for angular1 `resolve:` block on a [[Ng1StateDeclaration]].
Expand All @@ -10,9 +11,29 @@ import {isString} from "../../common/predicates";
* handles the `resolve` property with logic specific to angular-ui-router (ng1).
*/
export function ng1ResolveBuilder(state: State) {
let resolve = {};
forEach(state.resolve || {}, function (resolveFn, name: string) {
resolve[name] = isString(resolveFn) ? [ resolveFn, x => x ] : resolveFn;
});
return resolve;
return isObject(state.resolve) ? makeResolvables(state.resolve) : [];
}

/** Validates the result map as a "resolve:" style object, then transforms the resolves into Resolvable[] */
export function makeResolvables(resolves: { [key: string]: Function; }): Resolvable[] {
// desugar ng1 sugar to create a resolve that is a service
// e.g., resolve: { myService: 'myService' }
const resolveServiceFromString = tuple => {
if (!isString(tuple.val)) return tuple;

injectService.$inject = [tuple.val];
function injectService(svc) { return svc; }
return { key: tuple.key, val: injectService };
};

// Convert from object to tuple array
let tuples = Object.keys(resolves).map(key => ({key, val: resolves[key]})).map(resolveServiceFromString);

// If a hook result is an object, it should be a map of strings to (functions|strings).
let invalid = tuples.filter(tuple => !isInjectable(tuple.val));
if (invalid.length)
throw new Error(`Invalid resolve key/value: ${invalid[0].key}/${invalid[0].val}`);

const deps = (resolveFn) => services.$injector.annotate(resolveFn, services.$injector.strictDi);
return tuples.map(tuple => new Resolvable(tuple.key, tuple.val, deps(tuple.val)));
}
2 changes: 1 addition & 1 deletion src/path/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class PathNode {
this.state = state;
this.paramSchema = state.parameters({ inherit: false });
this.paramValues = {};
this.resolvables = Object.keys(state.resolve || {}).map(key => new Resolvable(key, state.resolve[key]));
this.resolvables = state.resolve.map(res => res.clone());
}
}

Expand Down
57 changes: 35 additions & 22 deletions src/resolve/resolvable.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
/** @module resolve */ /** for typedoc */
import {extend, pick, map, filter} from "../common/common";
import {not} from "../common/hof";
import {isInjectable} from "../common/predicates";
import {pick, map, extend} from "../common/common";

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

import {ResolveContext} from "./resolveContext";
import {stringify} from "../common/strings";

/**
* The basic building block for the resolve system.
Expand All @@ -22,18 +21,39 @@ import {ResolveContext} from "./resolveContext";
* parameter to those fns.
*/
export class Resolvable {
name: string;
token: any;
resolveFn: Function;
deps: string[];

promise: Promise<any> = undefined;
resolved: boolean = false;
data: any;

constructor(name: string, resolveFn: Function, preResolvedData?: any) {
this.name = name;
this.resolveFn = resolveFn;
this.deps = services.$injector.annotate(resolveFn, services.$injector.strictDi);
this.data = preResolvedData;

/**
* This constructor creates a Resolvable copy
*/
constructor(resolvable: Resolvable)

/**
* This constructor creates a new `Resolvable`
*
* @param token The new resolvable's injection token, such as `"userList"` (a string) or `UserService` (a class).
* When this token is used during injection, the resolved value will be injected.
* @param resolveFn The function that returns the resolved value, or a promise for the resolved value
* @param deps An array of dependencies, which will be injected into the `resolveFn`
* @param data Pre-resolved data. If the resolve value is already known, it may be provided here.
*/
constructor(token: any, resolveFn: Function, deps?: any[], data?: any)
constructor(token, resolveFn?: Function, deps?: any[], data?: any) {
if (token instanceof Resolvable) {
extend(this, token);
} else {
this.token = token;
this.resolveFn = resolveFn;
this.deps = deps;
this.data = data;
this.resolved = data !== undefined;
}
}

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

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

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

toString() {
return `Resolvable(name: ${this.name}, requires: [${this.deps}])`;
return `Resolvable(token: ${stringify(this.token)}, requires: [${this.deps.map(stringify)}])`;
}

/**
* Validates the result map as a "resolve:" style object, then transforms the resolves into Resolvable[]
*/
static makeResolvables(resolves: { [key: string]: Function; }): Resolvable[] {
// If a hook result is an object, it should be a map of strings to functions.
let invalid = filter(resolves, not(isInjectable)), keys = Object.keys(invalid);
if (keys.length)
throw new Error(`Invalid resolve key/value: ${keys[0]}/${invalid[keys[0]]}`);
return Object.keys(resolves).map(key => new Resolvable(key, resolves[key]));
clone(): Resolvable {
return new Resolvable(this);
}
}
12 changes: 6 additions & 6 deletions src/resolve/resolveContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ export class ResolveContext {
let omitProps = (node === last) ? options.omitOwnLocals : [];

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

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

addResolvables(newResolvables: Resolvable[], state: State) {
var node = this._nodeFor(state);
var keys = newResolvables.map(r => r.name);
node.resolvables = node.resolvables.filter(r => keys.indexOf(r.name) === -1).concat(newResolvables);
var keys = newResolvables.map(r => r.token);
node.resolvables = node.resolvables.filter(r => keys.indexOf(r.token) === -1).concat(newResolvables);
}

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

// Returns a promise for an array of resolved path Element promises
Expand Down Expand Up @@ -200,6 +200,6 @@ 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;
let policyName = resolveLevelPolicies[resolvable.token] || stateLevelPolicy || defaultResolvePolicy;
return ResolvePolicy[policyName];
}
8 changes: 2 additions & 6 deletions src/state/hooks/resolveHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,10 @@ export class ResolveHooks {

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

// Resolve all the LAZY resolves, then resolve the `$resolve$` object, then add `$resolve$` to the context
// return context.resolvePathElement(node.state, options)
return context.resolvePath(options)
.then(() => $resolve$.resolveResolvable(context))
.then(() => context.addResolvables([$resolve$], node.state));
// Resolve all the LAZY resolves
return context.resolvePath(options);
}

// Resolve eager resolvables before when the transition starts
Expand Down
2 changes: 1 addition & 1 deletion src/state/stateQueueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class StateQueueManager {
// @TODO: state = new State(extend({}, config, { ... }))
let state = inherit(new State(), extend({}, config, {
self: config,
resolve: config.resolve || {},
resolve: config.resolve || [],
toString: () => config.name
}));

Expand Down
14 changes: 7 additions & 7 deletions src/transition/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ export class Transition implements IHookRegistry {
PathFactory.bindResolveContexts(this._treeChanges.to);

let rootResolvables: Resolvable[] = [
new Resolvable('$transition$', () => this, this),
new Resolvable('$stateParams', () => this.params(), this.params())
new Resolvable('$transition$', () => this, [], this),
new Resolvable('$stateParams', () => this.params(), [], this.params())
];
let rootNode: PathNode = this._treeChanges.to[0];
rootNode.resolveContext.addResolvables(rootResolvables, rootNode.state)
Expand Down Expand Up @@ -223,16 +223,16 @@ export class Transition implements IHookRegistry {
}

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

/**
Expand Down
7 changes: 6 additions & 1 deletion test/core/resolveSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
import "../matchers.ts"

import {
ResolveContext, State, PathNode, PathFactory
ResolveContext, State, PathNode, PathFactory, Resolvable
} from "../../src/core";

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

import Spy = jasmine.Spy;
import {services} from "../../src/common/coreservices";

///////////////////////////////////////////////

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

thisState.resolve = Object.keys(resolve)
.map(key => new Resolvable(key, resolve[key], injector.annotate(resolve[key])));
thisState.template = thisState.template || "empty";
thisState.name = name;
thisState.parent = parent.name;
Expand Down
5 changes: 3 additions & 2 deletions test/ng1/ng1StateBuilderSpec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {StateBuilder, StateMatcher, ng1ResolveBuilder, ng1ViewsBuilder} from "../../src/ng1";
import {Resolvable} from "../../src/resolve/resolvable";

describe('Ng1 StateBuilder', function() {
var builder, matcher, urlMatcherFactoryProvider: any = {
Expand Down Expand Up @@ -32,7 +33,7 @@ describe('Ng1 StateBuilder', function() {
var config = { resolve: { foo: "bar" } };
var locals = { "bar": 123 };
expect(builder.builder('resolve')).toBeDefined();
var built = builder.builder('resolve')(config);
expect($injector.invoke(built.foo, null, locals)).toBe(123);
var built: Resolvable[] = builder.builder('resolve')(config);
expect($injector.invoke(built[0].resolveFn, null, locals)).toBe(123);
}));
});
9 changes: 5 additions & 4 deletions test/ng1/transitionSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {TargetState} from "../../src/state/targetState";
import {StateQueueManager} from "../../src/state/stateQueueManager";
import {Rejection} from "../../src/transition/rejectFactory";
import {ResolveHooks} from "../../src/state/hooks/resolveHooks";
import {Resolvable} from "../../src/resolve/resolvable";
import {Transition} from "../../src/transition/transition";

describe('transition', function () {

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

transitionProvider.onEnter({ entering: "B" }, function addResolves($transition$) {
transitionProvider.onEnter({ entering: "B" }, function addResolves($transition$: Transition) {
log.push("adding resolve");
$transition$.addResolves({
newResolve: function () { log.push("resolving"); return defer.promise; }
})
var resolveFn = function () { log.push("resolving"); return defer.promise; };
$transition$.addResolvable(new Resolvable('newResolve', resolveFn));
});

transitionProvider.onEnter({ entering: "C" }, function useTheNewResolve(trans, inj) {
Expand Down
2 changes: 1 addition & 1 deletion test/ng1/viewSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('view', function() {
let registerState = curry(function(_states, stateBuilder, config) {
let state = inherit(new State(), extend({}, config, {
self: config,
resolve: config.resolve || {}
resolve: config.resolve || []
}));
let built: State = stateBuilder.build(state);
return _states[built.name] = built;
Expand Down
Loading

0 comments on commit 6743a60

Please sign in to comment.