From 0f6aea62a3e92b99b892aa062d1f3be8e5bafa6a Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Fri, 4 Mar 2016 17:29:02 -0600 Subject: [PATCH] feat(uiView): Expose the resolved data for a state as $scope.$resolve feat(uiView): Route to components using templates closes #2175 closes #2547 --- src/common/common.ts | 2 +- src/ng1/services.ts | 4 +- src/ng1/viewDirective.ts | 31 +++++++++++++- src/state/hooks/resolveHooks.ts | 2 +- src/state/interface.ts | 13 ++++++ src/state/stateBuilder.ts | 5 ++- src/view/view.ts | 2 + test/stateSpec.js | 2 +- test/viewDirectiveSpec.js | 72 ++++++++++++++++++++++++++++++++- 9 files changed, 122 insertions(+), 11 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index d73c18f0e..fcfd3d936 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -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(collection: T[], callback: Mapper): U[]; /** Given an object, returns a new object, where each property is transformed by the callback function */ -export function map(collection: TypedMap, callback: Mapper): TypedMap; +export function map(collection: { [key: string]: T }, callback: Mapper): { [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) ? [] : {}; diff --git a/src/ng1/services.ts b/src/ng1/services.ts index aee3e7232..6493da269 100644 --- a/src/ng1/services.ts +++ b/src/ng1/services.ts @@ -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"; @@ -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 = ( find($transition$.treeChanges().to, propEq('state', vc.context))).resolveContext; let controllerDeps = annotateController(vc.controller); let resolvables = resolveCtx.getResolvables(); diff --git a/src/ng1/viewDirective.ts b/src/ng1/viewDirective.ts index 4c785488c..b10e91b5d 100644 --- a/src/ng1/viewDirective.ts +++ b/src/ng1/viewDirective.ts @@ -117,6 +117,26 @@ import {UIViewData} from "../view/interface"; * * * + * + * 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: '', + * resolve: { + * user: function(UserService) { return UserService.fetchUser(); } + * } + * }); + * ``` */ $ViewDirective.$inject = ['$view', '$animate', '$uiViewScroll', '$interpolate', '$q']; function $ViewDirective( $view, $animate, $uiViewScroll, $interpolate, $q) { @@ -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, @@ -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); } diff --git a/src/state/hooks/resolveHooks.ts b/src/state/hooks/resolveHooks.ts index 7dcec8c12..96feb0615 100644 --- a/src/state/hooks/resolveHooks.ts +++ b/src/state/hooks/resolveHooks.ts @@ -36,7 +36,7 @@ export class ResolveHooks { let node = find( 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 }); diff --git a/src/state/interface.ts b/src/state/interface.ts index 0cc20a792..317e01ee6 100644 --- a/src/state/interface.ts +++ b/src/state/interface.ts @@ -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]]: * diff --git a/src/state/stateBuilder.ts b/src/state/stateBuilder.ts index ca29462b1..8c6d64d75 100644 --- a/src/state/stateBuilder.ts +++ b/src/state/stateBuilder.ts @@ -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) { @@ -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; }], diff --git a/src/view/view.ts b/src/view/view.ts index 7b0dc9d8e..dca72c004 100644 --- a/src/view/view.ts +++ b/src/view/view.ts @@ -54,6 +54,7 @@ export class ViewConfig { template: string; controller: Function; controllerAs: string; + resolveAs: string; context: ViewContext; @@ -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; } /** diff --git a/test/stateSpec.js b/test/stateSpec.js index 5261f84f1..b8df3f5e1 100644 --- a/test/stateSpec.js +++ b/test/stateSpec.js @@ -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' } }); }); diff --git a/test/viewDirectiveSpec.js b/test/viewDirectiveSpec.js index 4ea19818e..e19e4b777 100644 --- a/test/viewDirectiveSpec.js +++ b/test/viewDirectiveSpec.js @@ -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']; @@ -120,7 +120,8 @@ describe('uiView', function () { } }; - beforeEach(module(function ($stateProvider) { + beforeEach(module(function (_$stateProvider_) { + $stateProvider = _$stateProvider_; $stateProvider .state('a', aState) .state('b', bState) @@ -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('
')(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('
')(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('
')(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('
')(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) {