Skip to content

Implement resolveAs #2605

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

Merged
merged 2 commits into from
Mar 5, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/common/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export function find(collection, callback) {
/** Given an array, returns a new array, where each element is transformed by the callback function */
export function map<T, U>(collection: T[], callback: Mapper<T, U>): U[];
/** Given an object, returns a new object, where each property is transformed by the callback function */
export function map<T, U>(collection: TypedMap<T>, callback: Mapper<T, U>): TypedMap<U>;
export function map<T, U>(collection: { [key: string]: T }, callback: Mapper<T, U>): { [key: string]: U }
/** Maps an array or object properties using a callback function */
export function map(collection: any, callback: any): any {
let result = isArray(collection) ? [] : {};
Expand Down
16 changes: 10 additions & 6 deletions src/ng1/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
/** for typedoc */
import {UIRouter} from "../router";
import {services} from "../common/coreservices";
import {map, bindFunctions, removeFrom, find, noop} from "../common/common";
import {map, bindFunctions, removeFrom, find, noop, TypedMap} from "../common/common";
import {prop, propEq} from "../common/hof";
import {isObject} from "../common/predicates";
import {Node} from "../path/module";
Expand Down Expand Up @@ -272,12 +272,16 @@ function getTransitionsProvider() {
loadAllControllerLocals.$inject = ['$transition$'];
function loadAllControllerLocals($transition$) {
const loadLocals = (vc: ViewConfig) => {
let deps = annotateController(vc.controller);
let toPath: Node[] = $transition$.treeChanges().to;
let resolveInjector = find(toPath, propEq('state', vc.context)).resolveInjector;
let resolveCtx = (<Node> find($transition$.treeChanges().to, propEq('state', vc.context))).resolveContext;
let controllerDeps = annotateController(vc.controller);
let resolvables = resolveCtx.getResolvables();

function $loadControllerLocals() { }
$loadControllerLocals.$inject = deps;
return services.$q.all(resolveInjector.getLocals($loadControllerLocals)).then((locals) => vc.locals = locals);
$loadControllerLocals.$inject = controllerDeps.filter(dep => resolvables.hasOwnProperty(dep));
// Load any controller resolves that aren't already loaded
return resolveCtx.invokeLater($loadControllerLocals)
// Then provide the view config with all the resolved data
.then(() => vc.locals = map(resolvables, res => res.data));
};

let loadAllLocals = $transition$.views("entering").filter(vc => !!vc.controller).map(loadLocals);
Expand Down
31 changes: 29 additions & 2 deletions src/ng1/viewDirective.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,26 @@ import {UIViewData} from "../view/interface";
* <ui-view autoscroll='false'/>
* <ui-view autoscroll='scopeVariable'/>
* </pre>
*
* Resolve data:
*
* The resolved data from the state's `resolve` block is placed on the scope as `$resolve` (this
* can be customized using [[ViewDeclaration.resolveAs]]). This can be then accessed from the template.
*
* Note that when `controllerAs` is being used, `$resolve` is set on the controller instance *after* the
* controller is instantiated. The `$onInit()` hook can be used to perform initialization code which
* depends on `$resolve` data.
*
* @example
* ```
*
* $stateProvider.state('home', {
* template: '<my-component user="$resolve.user"></my-component>',
* resolve: {
* user: function(UserService) { return UserService.fetchUser(); }
* }
* });
* ```
*/
$ViewDirective.$inject = ['$view', '$animate', '$uiViewScroll', '$interpolate', '$q'];
function $ViewDirective( $view, $animate, $uiViewScroll, $interpolate, $q) {
Expand Down Expand Up @@ -231,6 +251,7 @@ function $ViewDirective( $view, $animate, $uiViewScroll, $interpolate,
$template: config.template,
$controller: config.controller,
$controllerAs: config.controllerAs,
$resolveAs: config.resolveAs,
$locals: config.locals,
$animEnter: animEnter.promise,
$animLeave: animLeave.promise,
Expand Down Expand Up @@ -291,11 +312,17 @@ function $ViewDirectiveFill ( $compile, $controller, $interpolate, $injec
let link = $compile($element.contents());
let controller = data.$controller;
let controllerAs = data.$controllerAs;
let resolveAs = data.$resolveAs;
let locals = data.$locals;

scope[resolveAs] = locals;

if (controller) {
let locals = data.$locals;
let controllerInstance = $controller(controller, extend(locals, { $scope: scope, $element: $element }));
if (controllerAs) scope[controllerAs] = controllerInstance;
if (controllerAs) {
scope[controllerAs] = controllerInstance;
scope[controllerAs][resolveAs] = locals;
}
$element.data('$ngControllerController', controllerInstance);
$element.children().data('$ngControllerController', controllerInstance);
}
Expand Down
14 changes: 12 additions & 2 deletions src/state/hooks/resolveHooks.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/** @module state */ /** for typedoc */
import {extend, find, tail} from "../../common/common";
import {extend, find, tail, map} from "../../common/common";
import {propEq} from "../../common/hof";

import {ResolvePolicy} from "../../resolve/interface";

import {Transition} from "../../transition/transition";
import {val} from "../../common/hof";
import {Resolvable} from "../../resolve/resolvable";


let LAZY = ResolvePolicy[ResolvePolicy.LAZY];
Expand Down Expand Up @@ -33,7 +34,16 @@ export class ResolveHooks {
(<any> $lazyResolveEnteringState).$inject = ['$state$', '$transition$'];
function $lazyResolveEnteringState($state$, $transition$) {
let node = find(<any[]> treeChanges.entering, propEq('state', $state$));
return node.resolveContext.resolvePathElement(node.state, extend({transition: $transition$}, { resolvePolicy: LAZY }));

// A new Resolvable contains all the resolved data in this context as a single object, for injection as `$resolve$`
let $resolve$ = new Resolvable("$resolve$", () => map(context.getResolvables(), (r: Resolvable) => r.data));
let context = node.resolveContext;
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)
.then(() => $resolve$.resolveResolvable(context))
.then(() => context.addResolvables({$resolve$}, node.state));
}

// Resolve eager resolvables before when the transition starts
Expand Down
13 changes: 13 additions & 0 deletions src/state/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ export interface ViewDeclaration {
*/
controllerProvider?: Function;

/**
* The scope variable name to use for resolve data.
*
* A property of either [[StateDeclaration]] or [[ViewDeclaration]]. For a given view, the view-level property
* takes precedence over the state-level property.
*
* When a view is activated, the resolved data for the state which the view belongs to is put on the scope.
* This property sets the name of the scope variable to use for the resolved data.
*
* Defaults to `$resolve`.
*/
resolveAs?: string;

/**
* A property of [[StateDeclaration]] or [[ViewDeclaration]]:
*
Expand Down
5 changes: 3 additions & 2 deletions src/state/stateBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class StateBuilder {
views: [function (state: State) {
let views = {},
tplKeys = ['templateProvider', 'templateUrl', 'template', 'notify', 'async'],
ctrlKeys = ['controller', 'controllerProvider', 'controllerAs'];
ctrlKeys = ['controller', 'controllerProvider', 'controllerAs', 'resolveAs'];
let allKeys = tplKeys.concat(ctrlKeys);

forEach(state.views || {"$default": pick(state, allKeys)}, function (config, name) {
Expand All @@ -111,7 +111,8 @@ export class StateBuilder {
forEach(ctrlKeys, (key) => {
if (state[key] && !config[key]) config[key] = state[key];
});
if (Object.keys(config).length > 0) views[name] = config;
config.resolveAs = config.resolveAs || '$resolve';
if (Object.keys(config).length > 1) views[name] = config;
});
return views;
}],
Expand Down
2 changes: 2 additions & 0 deletions src/view/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export class ViewConfig {
template: string;
controller: Function;
controllerAs: string;
resolveAs: string;

context: ViewContext;

Expand All @@ -78,6 +79,7 @@ export class ViewConfig {

extend(this, pick(stateViewConfig, "viewDeclarationObj", "params", "context", "locals", "node"), {uiViewName, uiViewContextAnchor});
this.controllerAs = stateViewConfig.viewDeclarationObj.controllerAs;
this.resolveAs = stateViewConfig.viewDeclarationObj.resolveAs;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion test/stateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ describe('state helpers', function() {
it('should return filtered keys if view config is provided', function() {
var config = { url: "/foo", templateUrl: "/foo.html", controller: "FooController" };
expect(builder.builder('views')(config)).toEqual({
$default: { templateUrl: "/foo.html", controller: "FooController" }
$default: { templateUrl: "/foo.html", controller: "FooController", resolveAs: '$resolve' }
});
});

Expand Down
72 changes: 70 additions & 2 deletions test/viewDirectiveSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function animateFlush($animate) {
describe('uiView', function () {
'use strict';

var scope, $compile, elem, log;
var $stateProvider, scope, $compile, elem, log;

beforeEach(function() {
var depends = ['ui.router'];
Expand Down Expand Up @@ -120,7 +120,8 @@ describe('uiView', function () {
}
};

beforeEach(module(function ($stateProvider) {
beforeEach(module(function (_$stateProvider_) {
$stateProvider = _$stateProvider_;
$stateProvider
.state('a', aState)
.state('b', bState)
Expand Down Expand Up @@ -338,6 +339,73 @@ describe('uiView', function () {
expect(elem.text()).toBe('mState');
}));

describe('(resolved data)', function() {
var _scope;
function controller($scope) { _scope = $scope; }

var _state = {
name: 'resolve',
resolve: {
user: function($timeout) {
return $timeout(function() { return "joeschmoe"; }, 100);
}
}
};

it('should provide the resolved data on the $scope', inject(function ($state, $q, $timeout) {
var state = angular.extend({}, _state, { template: '{{$resolve.user}}', controller: controller });
$stateProvider.state(state);
elem.append($compile('<div><ui-view></ui-view></div>')(scope));

$state.transitionTo('resolve'); $q.flush(); $timeout.flush();
expect(elem.text()).toBe('joeschmoe');
expect(_scope.$resolve).toBeDefined();
expect(_scope.$resolve.user).toBe('joeschmoe')
}));

it('should put the resolved data on the resolveAs variable', inject(function ($state, $q, $timeout) {
var state = angular.extend({}, _state, { template: '{{$$$resolve.user}}', resolveAs: '$$$resolve', controller: controller });
$stateProvider.state(state);
elem.append($compile('<div><ui-view></ui-view></div>')(scope));

$state.transitionTo('resolve'); $q.flush(); $timeout.flush();
expect(elem.text()).toBe('joeschmoe');
expect(_scope.$$$resolve).toBeDefined();
expect(_scope.$$$resolve.user).toBe('joeschmoe')
}));

it('should put the resolved data on the controllerAs', inject(function ($state, $q, $timeout) {
var state = angular.extend({}, _state, { template: '{{$ctrl.$resolve.user}}', controllerAs: '$ctrl', controller: controller });
$stateProvider.state(state);
elem.append($compile('<div><ui-view></ui-view></div>')(scope));

$state.transitionTo('resolve'); $q.flush(); $timeout.flush();
expect(elem.text()).toBe('joeschmoe');
expect(_scope.$resolve).toBeDefined();
expect(_scope.$ctrl).toBeDefined();
expect(_scope.$ctrl.$resolve).toBeDefined();
expect(_scope.$ctrl.$resolve.user).toBe('joeschmoe');
}));

it('should use the view-level resolveAs over the state-level resolveAs', inject(function ($state, $q, $timeout) {
var views = {
"$default": {
controller: controller,
template: '{{$$$resolve.user}}',
resolveAs: '$$$resolve'
}
};
var state = angular.extend({}, _state, { resolveAs: 'foo', views: views })
$stateProvider.state(state);
elem.append($compile('<div><ui-view></ui-view></div>')(scope));

$state.transitionTo('resolve'); $q.flush(); $timeout.flush();
expect(elem.text()).toBe('joeschmoe');
expect(_scope.$$$resolve).toBeDefined();
expect(_scope.$$$resolve.user).toBe('joeschmoe');
}));
});

describe('play nicely with other directives', function() {
// related to issue #857
it('should work with ngIf', inject(function ($state, $q, $compile) {
Expand Down