Skip to content

Commit 0f6aea6

Browse files
feat(uiView): Expose the resolved data for a state as $scope.$resolve
feat(uiView): Route to components using templates closes #2175 closes #2547
1 parent e432c27 commit 0f6aea6

File tree

9 files changed

+122
-11
lines changed

9 files changed

+122
-11
lines changed

src/common/common.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ export function find(collection, callback) {
289289
/** Given an array, returns a new array, where each element is transformed by the callback function */
290290
export function map<T, U>(collection: T[], callback: Mapper<T, U>): U[];
291291
/** Given an object, returns a new object, where each property is transformed by the callback function */
292-
export function map<T, U>(collection: TypedMap<T>, callback: Mapper<T, U>): TypedMap<U>;
292+
export function map<T, U>(collection: { [key: string]: T }, callback: Mapper<T, U>): { [key: string]: U }
293293
/** Maps an array or object properties using a callback function */
294294
export function map(collection: any, callback: any): any {
295295
let result = isArray(collection) ? [] : {};

src/ng1/services.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
/** for typedoc */
1616
import {UIRouter} from "../router";
1717
import {services} from "../common/coreservices";
18-
import {map, bindFunctions, removeFrom, find, noop} from "../common/common";
18+
import {map, bindFunctions, removeFrom, find, noop, TypedMap} from "../common/common";
1919
import {prop, propEq} from "../common/hof";
2020
import {isObject} from "../common/predicates";
2121
import {Node} from "../path/module";
@@ -272,7 +272,7 @@ function getTransitionsProvider() {
272272
loadAllControllerLocals.$inject = ['$transition$'];
273273
function loadAllControllerLocals($transition$) {
274274
const loadLocals = (vc: ViewConfig) => {
275-
let resolveCtx = find($transition$.treeChanges().to, propEq('state', vc.context)).resolveContext;
275+
let resolveCtx = (<Node> find($transition$.treeChanges().to, propEq('state', vc.context))).resolveContext;
276276
let controllerDeps = annotateController(vc.controller);
277277
let resolvables = resolveCtx.getResolvables();
278278

src/ng1/viewDirective.ts

+29-2
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,26 @@ import {UIViewData} from "../view/interface";
117117
* <ui-view autoscroll='false'/>
118118
* <ui-view autoscroll='scopeVariable'/>
119119
* </pre>
120+
*
121+
* Resolve data:
122+
*
123+
* The resolved data from the state's `resolve` block is placed on the scope as `$resolve` (this
124+
* can be customized using [[ViewDeclaration.resolveAs]]). This can be then accessed from the template.
125+
*
126+
* Note that when `controllerAs` is being used, `$resolve` is set on the controller instance *after* the
127+
* controller is instantiated. The `$onInit()` hook can be used to perform initialization code which
128+
* depends on `$resolve` data.
129+
*
130+
* @example
131+
* ```
132+
*
133+
* $stateProvider.state('home', {
134+
* template: '<my-component user="$resolve.user"></my-component>',
135+
* resolve: {
136+
* user: function(UserService) { return UserService.fetchUser(); }
137+
* }
138+
* });
139+
* ```
120140
*/
121141
$ViewDirective.$inject = ['$view', '$animate', '$uiViewScroll', '$interpolate', '$q'];
122142
function $ViewDirective( $view, $animate, $uiViewScroll, $interpolate, $q) {
@@ -231,6 +251,7 @@ function $ViewDirective( $view, $animate, $uiViewScroll, $interpolate,
231251
$template: config.template,
232252
$controller: config.controller,
233253
$controllerAs: config.controllerAs,
254+
$resolveAs: config.resolveAs,
234255
$locals: config.locals,
235256
$animEnter: animEnter.promise,
236257
$animLeave: animLeave.promise,
@@ -291,11 +312,17 @@ function $ViewDirectiveFill ( $compile, $controller, $interpolate, $injec
291312
let link = $compile($element.contents());
292313
let controller = data.$controller;
293314
let controllerAs = data.$controllerAs;
315+
let resolveAs = data.$resolveAs;
316+
let locals = data.$locals;
294317

318+
scope[resolveAs] = locals;
319+
295320
if (controller) {
296-
let locals = data.$locals;
297321
let controllerInstance = $controller(controller, extend(locals, { $scope: scope, $element: $element }));
298-
if (controllerAs) scope[controllerAs] = controllerInstance;
322+
if (controllerAs) {
323+
scope[controllerAs] = controllerInstance;
324+
scope[controllerAs][resolveAs] = locals;
325+
}
299326
$element.data('$ngControllerController', controllerInstance);
300327
$element.children().data('$ngControllerController', controllerInstance);
301328
}

src/state/hooks/resolveHooks.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class ResolveHooks {
3636
let node = find(<any[]> treeChanges.entering, propEq('state', $state$));
3737

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

src/state/interface.ts

+13
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@ export interface ViewDeclaration {
7878
*/
7979
controllerProvider?: Function;
8080

81+
/**
82+
* The scope variable name to use for resolve data.
83+
*
84+
* A property of either [[StateDeclaration]] or [[ViewDeclaration]]. For a given view, the view-level property
85+
* takes precedence over the state-level property.
86+
*
87+
* When a view is activated, the resolved data for the state which the view belongs to is put on the scope.
88+
* This property sets the name of the scope variable to use for the resolved data.
89+
*
90+
* Defaults to `$resolve`.
91+
*/
92+
resolveAs?: string;
93+
8194
/**
8295
* A property of [[StateDeclaration]] or [[ViewDeclaration]]:
8396
*

src/state/stateBuilder.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export class StateBuilder {
102102
views: [function (state: State) {
103103
let views = {},
104104
tplKeys = ['templateProvider', 'templateUrl', 'template', 'notify', 'async'],
105-
ctrlKeys = ['controller', 'controllerProvider', 'controllerAs'];
105+
ctrlKeys = ['controller', 'controllerProvider', 'controllerAs', 'resolveAs'];
106106
let allKeys = tplKeys.concat(ctrlKeys);
107107

108108
forEach(state.views || {"$default": pick(state, allKeys)}, function (config, name) {
@@ -111,7 +111,8 @@ export class StateBuilder {
111111
forEach(ctrlKeys, (key) => {
112112
if (state[key] && !config[key]) config[key] = state[key];
113113
});
114-
if (Object.keys(config).length > 0) views[name] = config;
114+
config.resolveAs = config.resolveAs || '$resolve';
115+
if (Object.keys(config).length > 1) views[name] = config;
115116
});
116117
return views;
117118
}],

src/view/view.ts

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export class ViewConfig {
5454
template: string;
5555
controller: Function;
5656
controllerAs: string;
57+
resolveAs: string;
5758

5859
context: ViewContext;
5960

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

7980
extend(this, pick(stateViewConfig, "viewDeclarationObj", "params", "context", "locals", "node"), {uiViewName, uiViewContextAnchor});
8081
this.controllerAs = stateViewConfig.viewDeclarationObj.controllerAs;
82+
this.resolveAs = stateViewConfig.viewDeclarationObj.resolveAs;
8183
}
8284

8385
/**

test/stateSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ describe('state helpers', function() {
181181
it('should return filtered keys if view config is provided', function() {
182182
var config = { url: "/foo", templateUrl: "/foo.html", controller: "FooController" };
183183
expect(builder.builder('views')(config)).toEqual({
184-
$default: { templateUrl: "/foo.html", controller: "FooController" }
184+
$default: { templateUrl: "/foo.html", controller: "FooController", resolveAs: '$resolve' }
185185
});
186186
});
187187

test/viewDirectiveSpec.js

+70-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function animateFlush($animate) {
99
describe('uiView', function () {
1010
'use strict';
1111

12-
var scope, $compile, elem, log;
12+
var $stateProvider, scope, $compile, elem, log;
1313

1414
beforeEach(function() {
1515
var depends = ['ui.router'];
@@ -120,7 +120,8 @@ describe('uiView', function () {
120120
}
121121
};
122122

123-
beforeEach(module(function ($stateProvider) {
123+
beforeEach(module(function (_$stateProvider_) {
124+
$stateProvider = _$stateProvider_;
124125
$stateProvider
125126
.state('a', aState)
126127
.state('b', bState)
@@ -338,6 +339,73 @@ describe('uiView', function () {
338339
expect(elem.text()).toBe('mState');
339340
}));
340341

342+
describe('(resolved data)', function() {
343+
var _scope;
344+
function controller($scope) { _scope = $scope; }
345+
346+
var _state = {
347+
name: 'resolve',
348+
resolve: {
349+
user: function($timeout) {
350+
return $timeout(function() { return "joeschmoe"; }, 100);
351+
}
352+
}
353+
};
354+
355+
it('should provide the resolved data on the $scope', inject(function ($state, $q, $timeout) {
356+
var state = angular.extend({}, _state, { template: '{{$resolve.user}}', controller: controller });
357+
$stateProvider.state(state);
358+
elem.append($compile('<div><ui-view></ui-view></div>')(scope));
359+
360+
$state.transitionTo('resolve'); $q.flush(); $timeout.flush();
361+
expect(elem.text()).toBe('joeschmoe');
362+
expect(_scope.$resolve).toBeDefined();
363+
expect(_scope.$resolve.user).toBe('joeschmoe')
364+
}));
365+
366+
it('should put the resolved data on the resolveAs variable', inject(function ($state, $q, $timeout) {
367+
var state = angular.extend({}, _state, { template: '{{$$$resolve.user}}', resolveAs: '$$$resolve', controller: controller });
368+
$stateProvider.state(state);
369+
elem.append($compile('<div><ui-view></ui-view></div>')(scope));
370+
371+
$state.transitionTo('resolve'); $q.flush(); $timeout.flush();
372+
expect(elem.text()).toBe('joeschmoe');
373+
expect(_scope.$$$resolve).toBeDefined();
374+
expect(_scope.$$$resolve.user).toBe('joeschmoe')
375+
}));
376+
377+
it('should put the resolved data on the controllerAs', inject(function ($state, $q, $timeout) {
378+
var state = angular.extend({}, _state, { template: '{{$ctrl.$resolve.user}}', controllerAs: '$ctrl', controller: controller });
379+
$stateProvider.state(state);
380+
elem.append($compile('<div><ui-view></ui-view></div>')(scope));
381+
382+
$state.transitionTo('resolve'); $q.flush(); $timeout.flush();
383+
expect(elem.text()).toBe('joeschmoe');
384+
expect(_scope.$resolve).toBeDefined();
385+
expect(_scope.$ctrl).toBeDefined();
386+
expect(_scope.$ctrl.$resolve).toBeDefined();
387+
expect(_scope.$ctrl.$resolve.user).toBe('joeschmoe');
388+
}));
389+
390+
it('should use the view-level resolveAs over the state-level resolveAs', inject(function ($state, $q, $timeout) {
391+
var views = {
392+
"$default": {
393+
controller: controller,
394+
template: '{{$$$resolve.user}}',
395+
resolveAs: '$$$resolve'
396+
}
397+
};
398+
var state = angular.extend({}, _state, { resolveAs: 'foo', views: views })
399+
$stateProvider.state(state);
400+
elem.append($compile('<div><ui-view></ui-view></div>')(scope));
401+
402+
$state.transitionTo('resolve'); $q.flush(); $timeout.flush();
403+
expect(elem.text()).toBe('joeschmoe');
404+
expect(_scope.$$$resolve).toBeDefined();
405+
expect(_scope.$$$resolve.user).toBe('joeschmoe');
406+
}));
407+
});
408+
341409
describe('play nicely with other directives', function() {
342410
// related to issue #857
343411
it('should work with ngIf', inject(function ($state, $q, $compile) {

0 commit comments

Comments
 (0)