Skip to content

Commit

Permalink
feat(uiView): Expose the resolved data for a state as $scope.$resolve
Browse files Browse the repository at this point in the history
feat(uiView): Route to components using templates
closes #2175
closes #2547
  • Loading branch information
christopherthielen committed Mar 4, 2016
1 parent e432c27 commit 0f6aea6
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 11 deletions.
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
4 changes: 2 additions & 2 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,7 +272,7 @@ function getTransitionsProvider() {
loadAllControllerLocals.$inject = ['$transition$'];
function loadAllControllerLocals($transition$) {
const loadLocals = (vc: ViewConfig) => {
let resolveCtx = find($transition$.treeChanges().to, propEq('state', vc.context)).resolveContext;
let resolveCtx = (<Node> find($transition$.treeChanges().to, propEq('state', vc.context))).resolveContext;
let controllerDeps = annotateController(vc.controller);
let resolvables = resolveCtx.getResolvables();

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
2 changes: 1 addition & 1 deletion src/state/hooks/resolveHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class ResolveHooks {
let node = find(<any[]> treeChanges.entering, propEq('state', $state$));

// 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 => r.data));
let $resolve$ = new Resolvable("$resolve$", () => map(context.getResolvables(), (r: Resolvable) => r.data));
let context = node.resolveContext;
var options = extend({transition: $transition$}, { resolvePolicy: LAZY });

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

0 comments on commit 0f6aea6

Please sign in to comment.