Skip to content

Resolve can't inject value from parent resolve #73

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
67b16f1
$resolve service (tested) and integration into $state (only partially…
ksperling Aug 10, 2013
07eb163
Update README.md
timkindberg Jul 13, 2013
93a61f7
'data' property inheritance/override in chain of states
alexandrbaran Jul 14, 2013
0849993
Reordered Features to de-emphasize multiple views and named views
timkindberg Jul 19, 2013
585001d
Handle unmodified left left click only, allow open-in-new tab etc. to…
gigadude Jul 22, 2013
1a9623e
HTML5-mode compatibility + tests, fixes #276
nateabele Jul 23, 2013
bc8ed51
Fixes #257
timkindberg Jul 23, 2013
16be1c7
Fixed incorrect encoding/decoding in urlMatcherFactory for parameters.
dbkang Jul 24, 2013
b05cdc2
test(state): getConfig
davidnpma Aug 5, 2013
844c9f2
feat(state): getConfig
davidnpma Aug 5, 2013
0444977
Initial test spec for $urlRouter.
nateabele Aug 6, 2013
81f19da
Keep otherwise() handling last.
gigadude Jul 27, 2013
bf38b0d
$state.getConfig() now returns the actual state configuration object.
nateabele Aug 10, 2013
a954e0b
URL transitions on param-only state change, fixes #289.
nateabele Aug 12, 2013
2e8856f
Initial refactor.
nateabele Jun 27, 2013
b9045e7
Refactoring 'registerState()' into 'stateBuilder'.
nateabele Jun 28, 2013
ad91f00
'data' property inheritance/override in chain of states
alexandrbaran Jul 14, 2013
6a66b7d
Refactoring 'registerState()' into 'stateBuilder'.
nateabele Jun 28, 2013
5261cab
Additional state builder refactoring.
nateabele Jul 27, 2013
63e11e6
First pass to extract $view into separate service.
nateabele Jul 27, 2013
d128483
Methods for $state now have proper names.
nateabele Jul 29, 2013
45b7285
Implementing $state.go(), for relative transitions & param inheritance.
nateabele Jul 29, 2013
088ea44
Refactoring 3rd param in $state.transitionTo() to options hash.
nateabele Jul 29, 2013
1e00aca
Refactoring $state.go() functionality into transitionTo() as options.
nateabele Aug 6, 2013
ee705d0
Using refactored URL matcher detection in $state, fixes #154
nateabele Aug 10, 2013
7122aad
Final implementation of relative state targeting.
nateabele Aug 12, 2013
a828bfd
Convert module names to new ui.router nested convention
Aug 28, 2013
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module.exports = function (grunt) {
'src/urlMatcherFactory.js',
'src/urlRouter.js',
'src/state.js',
'src/view.js',
'src/viewDirective.js',
'src/stateDirectives.js',
'src/compat.js'
Expand Down
24 changes: 13 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
# UI-Router

####Finally a de-facto solution to nested views and routing.
>* Latest release 0.0.1: [Compressed](http://angular-ui.github.io/ui-router/release/angular-ui-router.min.js) / [Uncompressed](http://angular-ui.github.io/ui-router/release/angular-ui-router.js)
>* Latest snapshot: [Compressed](http://angular-ui.github.io/ui-router/build/angular-ui-router.min.js) / [Uncompressed](http://angular-ui.github.io/ui-router/build/angular-ui-router.js)
* Latest release 0.0.1: [Compressed](http://angular-ui.github.io/ui-router/release/angular-ui-router.min.js) / [Uncompressed](http://angular-ui.github.io/ui-router/release/angular-ui-router.js)
* Latest snapshot: [Build the Project Locally](https://github.com/angular-ui/ui-router#developing)

**Warning:** UI-Router is in active development. The API is highly subject to change. It is not recommended to use this library on projects that require guaranteed stability.

## Main Goal
To evolve the concept of an [angularjs](http://angularjs.org/) [***route***](http://docs.angularjs.org/api/ng.$routeProvider) into a more general concept of a ***state*** for managing complex application UI states.
Expand All @@ -17,20 +18,21 @@ To evolve the concept of an [angularjs](http://angularjs.org/) [***route***](htt
2. **More Powerful Views**
>`ui-view` directive (used in place of `ng-view`)

3. **Named Views**
3. **Nested Views**
>load templates that contain nested `ui-view`s as deep as you'd like.

4. **Routing**
>States can map to URLs (though it's not required)

5. **Named Views**
>`<div ui-view="chart">`

4. **Multiple Parallel Views**
6. **Multiple Parallel Views**
>
```
<div ui-view="chart1">
<div ui-view="chart2">
```
5. **Nested Views**
>load templates that contain nested `ui-view`s as deep as you'd like.

6. **Routing**
>States can map to URLs (though it's not required)


*Basically, do whatever you want with states and routes.*
Expand Down Expand Up @@ -63,10 +65,10 @@ To evolve the concept of an [angularjs](http://angularjs.org/) [***route***](htt
<script src="angular-ui-router.min.js"></script> <!-- Insert after main angular.js file -->
```

2. Set `ui.state` as a dependency in your module
2. Set `ui.router` as a dependency in your module
>
```javascript
var myapp = angular.module('myapp', ['ui.state'])
var myapp = angular.module('myapp', ['ui.router'])
```

### Nested States & Views
Expand Down
1 change: 1 addition & 0 deletions config/karma.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ files = [
'src/templateFactory.js',
'src/urlMatcherFactory.js',
'src/urlRouter.js',
'src/view.js',
'src/state.js',
'src/viewDirective.js',
'src/stateDirectives.js',
Expand Down
2 changes: 1 addition & 1 deletion sample/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
}
}

angular.module('sample', ['ui.compat'])
angular.module('sample', ['ui.router.compat'])
.config(
[ '$stateProvider', '$routeProvider', '$urlRouterProvider',
function ($stateProvider, $routeProvider, $urlRouterProvider) {
Expand Down
61 changes: 48 additions & 13 deletions src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,6 @@ function inherit(parent, extra) {
return extend(new (extend(function() {}, { prototype: parent }))(), extra);
}

/**
* Extends the destination object `dst` by copying all of the properties from the `src` object(s)
* to `dst` if the `dst` object has no own property of the same name. You can specify multiple
* `src` objects.
*
* @param {Object} dst Destination object.
* @param {...Object} src Source object(s).
* @see angular.extend
*/
function merge(dst) {
forEach(arguments, function(obj) {
if (obj !== dst) {
Expand All @@ -35,7 +26,51 @@ function merge(dst) {
return dst;
}

angular.module('ui.util', ['ng']);
angular.module('ui.router', ['ui.util']);
angular.module('ui.state', ['ui.router', 'ui.util']);
angular.module('ui.compat', ['ui.state']);
/**
* Finds the common ancestor path between two states.
*
* @param {Object} first The first state.
* @param {Object} second The second state.
* @return {Array} Returns an array of state names in descending order, not including the root.
*/
function ancestors(first, second) {
var path = [];

for (var n in first.path) {
if (first.path[n] === "") continue;
if (!second.path[n]) break;
path.push(first.path[n]);
}
return path;
}

/**
* Merges a set of parameters with all parameters inherited between the common parents of the
* current state and a given destination state.
*
* @param {Object} currentParams The value of the current state parameters ($stateParams).
* @param {Object} newParams The set of parameters which will be composited with inherited params.
* @param {Object} $current Internal definition of object representing the current state.
* @param {Object} $to Internal definition of object representing state to transition to.
*/
function inheritParams(currentParams, newParams, $current, $to) {
var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = [];

for (var i in parents) {
if (!parents[i].params || !parents[i].params.length) continue;
parentParams = parents[i].params;

for (var j in parentParams) {
if (inheritList.indexOf(parentParams[j]) >= 0) continue;
inheritList.push(parentParams[j]);
inherited[parentParams[j]] = currentParams[parentParams[j]];
}
}
return extend({}, inherited, newParams);
}

angular.module('ui.router.util', ['ng']);
angular.module('ui.router.router', ['ui.router.util']);
angular.module('ui.router.state', ['ui.router.router', 'ui.router.util']);
angular.module('ui.router', ['ui.router.state']);
angular.module('ui.router.compat', ['ui.router']);
2 changes: 1 addition & 1 deletion src/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,6 @@ function $RouteProvider( $stateProvider, $urlRouterProvider) {
}
}

angular.module('ui.compat')
angular.module('ui.router.compat')
.provider('$route', $RouteProvider)
.directive('ngView', $ViewDirective);
215 changes: 215 additions & 0 deletions src/resolve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/**
* Service (`ui-util`). Manages resolution of (acyclic) graphs of promises.
* @module $resolve
* @requires $q
* @requires $injector
*/
$Resolve.$inject = ['$q', '$injector'];
function $Resolve( $q, $injector) {

var VISIT_IN_PROGRESS = 1,
VISIT_DONE = 2,
NOTHING = {},
NO_DEPENDENCIES = [],
NO_LOCALS = NOTHING,
NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING });


/**
* Studies a set of invocables that are likely to be used multiple times.
* $resolve.study(invocables)(locals, parent, self)
* is equivalent to
* $resolve.resolve(invocables, locals, parent, self)
* but the former is more efficient (in fact `resolve` just calls `study` internally).
* See {@link module:$resolve/resolve} for details.
* @function
* @param {Object} invocables
* @return {Function}
*/
this.study = function (invocables) {
if (!isObject(invocables)) throw new Error("'invocables' must be an object");

// Perform a topological sort of invocables to build an ordered plan
var plan = [], cycle = [], visited = {};
function visit(value, key) {
if (visited[key] === VISIT_DONE) return;

cycle.push(key);
if (visited[key] === VISIT_IN_PROGRESS) {
cycle.splice(0, cycle.indexOf(key));
throw new Error("Cyclic dependency: " + cycle.join(" -> "));
}
visited[key] = VISIT_IN_PROGRESS;

if (isString(value)) {
plan.push(key, [ function() { return $injector.get(key); }], NO_DEPENDENCIES);
} else {
var params = $injector.annotate(value);
forEach(params, function (param) {
if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param);
});
plan.push(key, value, params);
}

cycle.pop();
visited[key] = VISIT_DONE;
}
forEach(invocables, visit);
invocables = cycle = visited = null; // plan is all that's required

function isResolve(value) {
return isObject(value) && value.then && value.$$promises;
}

return function (locals, parent, self) {
if (isResolve(locals) && self === undefined) {
self = parent; parent = locals; locals = null;
}
if (!locals) locals = NO_LOCALS;
else if (!isObject(locals)) {
throw new Error("'locals' must be an object");
}
if (!parent) parent = NO_PARENT;
else if (!isResolve(parent)) {
throw new Error("'parent' must be a promise returned by $resolve.resolve()");
}

// To complete the overall resolution, we have to wait for the parent
// promise and for the promise for each invokable in our plan.
var resolution = $q.defer(),
result = resolution.promise,
promises = result.$$promises = {},
values = extend({}, locals),
wait = 1 + plan.length/3,
merged = false;

function done() {
// Merge parent values we haven't got yet and publish our own $$values
if (!--wait) {
if (!merged) merge(values, parent.$$values);
result.$$values = values;
result.$$promises = true; // keep for isResolve()
resolution.resolve(values);
}
}

function fail(reason) {
result.$$failure = reason;
resolution.reject(reason);
}

// Short-circuit if parent has already failed
if (isDefined(parent.$$failure)) {
fail(parent.$$failure);
return result;
}

// Merge parent values if the parent has already resolved, or merge
// parent promises and wait if the parent resolve is still in progress.
if (parent.$$values) {
merged = merge(values, parent.$$values);
done();
} else {
extend(promises, parent.$$promises);
parent.then(done, fail);
}

// Process each invocable in the plan, but ignore any where a local of the same name exists.
for (var i=0, ii=plan.length; i<ii; i+=3) {
if (locals.hasOwnProperty(plan[i])) done();
else invoke(plan[i], plan[i+1], plan[i+2]);
}

function invoke(key, invocable, params) {
// Create a deferred for this invocation. Failures will propagate to the resolution as well.
var invocation = $q.defer(), waitParams = 0;
function onfailure(reason) {
invocation.reject(reason);
fail(reason);
}
// Wait for any parameter that we have a promise for (either from parent or from this
// resolve; in that case study() will have made sure it's ordered before us in the plan).
params.forEach(function (dep) {
if (promises.hasOwnProperty(dep) && !locals.hasOwnProperty(dep)) {
waitParams++;
promises[dep].then(function (result) {
values[dep] = result;
if (!(--waitParams)) proceed();
}, onfailure);
}
});
if (!waitParams) proceed();
function proceed() {
if (isDefined(result.$$failure)) return;
try {
invocation.resolve($injector.invoke(invocable, self, values));
invocation.promise.then(function (result) {
values[key] = result;
done();
}, onfailure);
} catch (e) {
onfailure(e);
}
}
// Publish promise synchronously; invocations further down in the plan may depend on it.
promises[key] = invocation.promise;
}

return result;
};
};

/**
* Resolves a set of invocables. An invocable is a function to be invoked via `$injector.invoke()`,
* and can have an arbitrary number of dependencies. An invocable can either return a value directly,
* or a `$q` promise. If a promise is returned it will be resolved and the resulting value will be
* used instead. Dependencies of invocables are resolved (in this order of precedence)
*
* - from the specified `locals`
* - from another invocable that is part of this `$resolve` call
* - from an invocable that is inherited from a `parent` call to `$resolve` (or recursively
* from any ancestor `$resolve` of that parent).
*
* The return value of `$resolve` is a promise for an object that contains (in this order of precedence)
*
* - any `locals` (if specified)
* - the resolved return values of all injectables
* - any values inherited from a `parent` call to `$resolve` (if specified)
*
* The promise will resolve after the `parent` promise (if any) and all promises returned by injectables
* have been resolved. If any invocable (or `$injector.invoke`) throws an exception, or if a promise
* returned by an invocable is rejected, the `$resolve` promise is immediately rejected with the same error.
* A rejection of a `parent` promise (if specified) will likewise be propagated immediately. Once the
* `$resolve` promise has been rejected, no further invocables will be called.
*
* Cyclic dependencies between invocables are not permitted and will caues `$resolve` to throw an
* error. As a special case, an injectable can depend on a parameter with the same name as the injectable,
* which will be fulfilled from the `parent` injectable of the same name. This allows inherited values
* to be decorated. Note that in this case any other injectable in the same `$resolve` with the same
* dependency would see the decorated value, not the inherited value.
*
* Note that missing dependencies -- unlike cyclic dependencies -- will cause an (asynchronous) rejection
* of the `$resolve` promise rather than a (synchronous) exception.
*
* Invocables are invoked eagerly as soon as all dependencies are available. This is true even for
* dependencies inherited from a `parent` call to `$resolve`.
*
* As a special case, an invocable can be a string, in which case it is taken to be a service name
* to be passed to `$injector.get()`. This is supported primarily for backwards-compatibility with the
* `resolve` property of `$routeProvider` routes.
*
* @function
* @param {Object.<string, Function|string>} invocables functions to invoke or `$injector` services to fetch.
* @param {Object.<string, *>} [locals] values to make available to the injectables
* @param {Promise.<Object>} [parent] a promise returned by another call to `$resolve`.
* @param {Object} [self] the `this` for the invoked methods
* @return {Promise.<Object>} Promise for an object that contains the resolved return value
* of all invocables, as well as any inherited and local values.
*/
this.resolve = function (invocables, locals, parent, self) {
return this.study(invocables)(locals, parent, self);
};
}

angular.module('ui.router.util').service('$resolve', $Resolve);

Loading