From 15520328abba69630f5c557f17d828e0205a7f20 Mon Sep 17 00:00:00 2001 From: Chris Thielen Date: Thu, 10 Mar 2016 14:07:44 -0600 Subject: [PATCH] feat(view): Route a view to a directive using `component:` closes #2627 --- config/karma.js | 5 +- src/common/common.ts | 1 + src/ng1/interface.ts | 94 +++++++++- src/ng1/viewDirective.ts | 26 ++- src/ng1/viewsBuilder.ts | 63 ++++++- src/view/templateFactory.ts | 3 +- test/viewDirectiveSpec.js | 338 ++++++++++++++++++++++++++++++++++++ 7 files changed, 514 insertions(+), 16 deletions(-) diff --git a/config/karma.js b/config/karma.js index 04f6b6385..578fbb06e 100644 --- a/config/karma.js +++ b/config/karma.js @@ -34,7 +34,10 @@ module.exports = function (karma) { ], // Karma files available to serve is overridden using files.karmaServedFiles() in some grunt tasks (e.g., karma:ng12) - files: files.karmaServedFiles('1.4.9'), + // files: files.karmaServedFiles('1.2.28'), + // files: files.karmaServedFiles('1.3.16'), + // files: files.karmaServedFiles('1.4.9'), + files: files.karmaServedFiles('1.5.0'), // Actual tests to load is configured in systemjs.files block systemjs: { // Set up systemjs paths diff --git a/src/common/common.ts b/src/common/common.ts index e4ad63e04..44731dbdf 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -504,6 +504,7 @@ export function tail(arr: T[]): T { return arr.length && arr[arr.length - 1] || undefined; } +export const kebobString = (camelCase: string) => camelCase.replace(/([A-Z])/g, $1 => "-"+$1.toLowerCase()); function _toJson(obj) { return JSON.stringify(obj); diff --git a/src/ng1/interface.ts b/src/ng1/interface.ts index 2dfb90884..3949c680a 100644 --- a/src/ng1/interface.ts +++ b/src/ng1/interface.ts @@ -1,6 +1,7 @@ /** @module ng1 */ /** */ import {StateDeclaration, _ViewDeclaration} from "../state/interface"; import {ParamDeclaration} from "../params/interface"; +import {IInjectable} from "../common/common"; /** * The StateDeclaration object is used to define a state or nested state. @@ -242,8 +243,7 @@ export interface Ng1StateDeclaration extends StateDeclaration, Ng1ViewDeclaratio * ``` */ views?: { [key: string]: Ng1ViewDeclaration; }; - data?: any; - onEnter?: Function; + data?: any; onEnter?: Function; onRetain?: Function; onExit?: Function; @@ -254,6 +254,94 @@ export interface Ng1StateDeclaration extends StateDeclaration, Ng1ViewDeclaratio } export interface Ng1ViewDeclaration extends _ViewDeclaration { + /** + * The name of the component to use for this view. + * + * The name of an [angular 1.5+ `.component()`](https://docs.angularjs.org/guide/component) (or directive with + * bindToController and/or scope declaration) which will be used for this view. + * + * Resolve data can be provided to the component via the component's `bindings` object (for 1.3+ directives, the + * `bindToController` is used; for other directives, the `scope` declaration is used). For each binding declared + * on the component, any resolve with the same name is set on the component's controller instance. The binding + * is provided to the component as a one-time-binding. In general, * components should likewise declare their + * input bindings as [one-way (`"<"`)](https://docs.angularjs.org/api/ng/service/$compile#-scope-). + * + * Note: inside a "views:" block, a bare string `"foo"` is shorthand for `{ component: "foo" }` + * + * Note: Mapping from resolve names to component inputs may be specified using [[bindings]]. + * + * @example: + * ``` + * + * .state('profile', { + * // Unnamed view should be component + * component: 'MyProfile', + * } + * .state('messages', { + * // 'header' named view should be component + * // 'content' named view should be component + * views: { + * header: { component: 'NavBar' }, + * content: { component: 'MessageList' } + * } + * } + * .state('contacts', { + * // Inside a "views:" block, a bare string "NavBar" is shorthand for { component: "NavBar" } + * // 'header' named view should be component + * // 'content' named view should be component + * views: { + * header: 'NavBar', + * content: 'ContactList' + * } + * } + * ``` + * + * Note: When using `component` to define a view, you may _not_ use any of: `template`, `templateUrl`, + * `templateProvider`, `controller`, `controllerProvider`, `controllerAs`. + * + * See also: Todd Motto's angular 1.3 and 1.4 [backport of .component()](https://github.com/toddmotto/angular-component) + */ + component?: string; + + /** + * An object to map from component `bindings` names to `resolve` names, for [[component]] style view. + * + * When using a [[component]] declaration, each component's input binding is supplied data from a resolve of the + * same name, by default. You may supply data from a different resolve name by mapping it here. + * + * Each key in this object is the name of one of the component's input bindings. + * Each value is the name of the resolve that should be provided to that binding. + * + * Any component bindings that are omitted from this map get the default behavior of mapping to a resolve of the + * same name. + * + * @example + * ``` + * $stateProvider.state('foo', { + * resolve: { + * foo: function(FooService) { return FooService.get(); }, + * bar: function(BarService) { return BarService.get(); } + * }, + * component: 'Baz', + * // The component's `baz` binding gets data from the `bar` resolve + * // The component's `foo` binding gets data from the `foo` resolve (default behavior) + * bindings: { + * baz: 'bar' + * } + * }); + * + * app.component('Baz', { + * templateUrl: 'baz.html', + * controller: 'BazController', + * bindings: { + * foo: '<', // foo binding + * baz: '<' // baz binding + * } + * }); + * ``` + * + */ + bindings?: { [key: string]: string }; /** * A property of [[StateDeclaration]] or [[ViewDeclaration]]: @@ -372,7 +460,7 @@ export interface Ng1ViewDeclaration extends _ViewDeclaration { * } * ``` */ - templateProvider?: Function; + templateProvider?: IInjectable; } diff --git a/src/ng1/viewDirective.ts b/src/ng1/viewDirective.ts index cfd7603a3..4fb705dcf 100644 --- a/src/ng1/viewDirective.ts +++ b/src/ng1/viewDirective.ts @@ -1,6 +1,6 @@ /** @module view */ /** for typedoc */ "use strict"; -import {extend, map, unnestR, filter} from "../common/common"; +import {extend, map, unnestR, filter, kebobString} from "../common/common"; import {isDefined, isFunction} from "../common/predicates"; import {trace} from "../common/trace"; import {ActiveUIView} from "../view/interface"; @@ -338,18 +338,40 @@ function $ViewDirectiveFill ( $compile, $controller, $transitions, $view, scope[resolveAs] = locals; if (controller) { - let controllerInstance = $controller(controller, extend(locals, { $scope: scope, $element: $element })); + let controllerInstance = $controller(controller, extend({}, locals, { $scope: scope, $element: $element })); if (controllerAs) { scope[controllerAs] = controllerInstance; scope[controllerAs][resolveAs] = locals; } + // TODO: Use $view service as a central point for registering component-level hooks + // Then, when a component is created, tell the $view service, so it can invoke hooks + // $view.componentLoaded(controllerInstance, { $scope: scope, $element: $element }); + // scope.$on('$destroy', () => $view.componentUnloaded(controllerInstance, { $scope: scope, $element: $element })); + $element.data('$ngControllerController', controllerInstance); $element.children().data('$ngControllerController', controllerInstance); registerControllerCallbacks($transitions, controllerInstance, scope, cfg); } + // Wait for the component to appear in the DOM + if (cfg.viewDecl.component) { + let cmp = cfg.viewDecl.component; + let kebobName = kebobString(cmp); + let getComponentController = () => { + let directiveEl = [].slice.call($element[0].children) + .filter(el => el && el.tagName && el.tagName.toLowerCase() === kebobName) ; + return directiveEl && angular.element(directiveEl).data(`$${cmp}Controller`); + }; + + let deregisterWatch = scope.$watch(getComponentController, function(ctrlInstance) { + if (!ctrlInstance) return; + registerControllerCallbacks($transitions, ctrlInstance, scope, cfg); + deregisterWatch(); + }); + } + link(scope); }; } diff --git a/src/ng1/viewsBuilder.ts b/src/ng1/viewsBuilder.ts index 428f6f120..c26d81055 100644 --- a/src/ng1/viewsBuilder.ts +++ b/src/ng1/viewsBuilder.ts @@ -1,15 +1,16 @@ /** @module ng1 */ /** */ import {State} from "../state/stateObject"; -import {pick, forEach} from "../common/common"; +import {pick, forEach, anyTrueR, unnestR, kebobString} from "../common/common"; import {ViewConfig, ViewContext} from "../view/interface"; import {Ng1ViewDeclaration} from "./interface"; import {ViewService} from "../view/view"; -import {isInjectable} from "../common/predicates"; +import {isInjectable, isDefined, isString, isObject} from "../common/predicates"; import {services} from "../common/coreservices"; import {trace} from "../common/trace"; import {Node} from "../path/node"; import {TemplateFactory} from "../view/templateFactory"; import {ResolveContext} from "../resolve/resolveContext"; +import {prop, parse} from "../common/hof"; export const ng1ViewConfigFactory = (node, view) => new Ng1ViewConfig(node, view); @@ -24,19 +25,36 @@ export const ng1ViewConfigFactory = (node, view) => new Ng1ViewConfig(node, view */ export function ng1ViewsBuilder(state: State) { let tplKeys = ['templateProvider', 'templateUrl', 'template', 'notify', 'async'], - ctrlKeys = ['component', 'controller', 'controllerProvider', 'controllerAs', 'resolveAs'], - allKeys = tplKeys.concat(ctrlKeys); + ctrlKeys = ['controller', 'controllerProvider', 'controllerAs', 'resolveAs'], + compKeys = ['component', 'bindings'], + nonCompKeys = tplKeys.concat(ctrlKeys), + allKeys = compKeys.concat(nonCompKeys); let views = {}, viewsObject = state.views || {"$default": pick(state, allKeys)}; forEach(viewsObject, function (config: Ng1ViewDeclaration, name) { - name = name || "$default"; // Account for views: { "": { template... } } - // Allow controller settings to be defined at the state level for all views - forEach(ctrlKeys, (key) => { - if (state[key] && !config[key]) config[key] = state[key]; - }); + // Account for views: { "": { template... } } + name = name || "$default"; + // Account for views: { header: "headerComponent" } + if (isString(config)) config = { component: config }; if (!Object.keys(config).length) return; + // Configure this view for routing to an angular 1.5+ style .component (or any directive, really) + if (config.component) { + if (nonCompKeys.map(key => isDefined(config[key])).reduce(anyTrueR, false)) { + throw new Error(`Cannot combine: ${compKeys.join("|")} with: ${nonCompKeys.join("|")} in stateview: 'name@${state.name}'`); + } + + // Dynamically build a template like "" + config.templateProvider = ['$injector', function($injector) { + const resolveFor = key => config.bindings && config.bindings[key] || key; + const prefix = angular.version.minor >= 3 ? "::" : ""; + let attrs = getComponentInputs($injector, config.component).map(key => `${kebobString(key)}='${prefix}$resolve.${resolveFor(key)}'`).join(" "); + let kebobName = kebobString(config.component); + return `<${kebobName} ${attrs}>`; + }]; + } + config.resolveAs = config.resolveAs || '$resolve'; config.$type = "ng1"; config.$context = state; @@ -51,6 +69,33 @@ export function ng1ViewsBuilder(state: State) { return views; } +// for ng 1.2 style, process the scope: { input: "=foo" } object +const scopeBindings = bindingsObj => Object.keys(bindingsObj) + .map(key => [key, /^[=<](.*)/.exec(bindingsObj[key])]) + .filter(tuple => isDefined(tuple[1])) + .map(tuple => tuple[1][1] || tuple[0]); + +// for ng 1.3+ bindToController or 1.5 component style, process a $$bindings object +const bindToCtrlBindings = bindingsObj => Object.keys(bindingsObj) + .filter(key => !!/[=<]/.exec(bindingsObj[key].mode)) + .map(key => bindingsObj[key].attrName); + +// Given a directive definition, find its object input attributes +// Use different properties, depending on the type of directive (component, bindToController, normal) +const getBindings = def => { + if (isObject(def.bindToController)) return scopeBindings(def.bindToController); + if (def.$$bindings && def.$$bindings.bindToController) return bindToCtrlBindings(def.$$bindings.bindToController); + if (def.$$isolateBindings) return bindToCtrlBindings(def.$$isolateBindings); + return scopeBindings(def.scope); +}; + +// Gets all the directive(s)' inputs ('=' and '<') +function getComponentInputs($injector, name) { + let cmpDefs = $injector.get(name + "Directive"); // could be multiple + if (!cmpDefs || !cmpDefs.length) throw new Error(`Unable to find component named '${name}'`); + return cmpDefs.map(getBindings).reduce(unnestR, []); +} + export class Ng1ViewConfig implements ViewConfig { loaded: boolean = false; controller: Function; diff --git a/src/view/templateFactory.ts b/src/view/templateFactory.ts index 5c9a19f57..5a664e340 100644 --- a/src/view/templateFactory.ts +++ b/src/view/templateFactory.ts @@ -2,6 +2,7 @@ import {isDefined, isFunction} from "../common/predicates"; import {services} from "../common/coreservices"; import {Ng1ViewDeclaration} from "../ng1/interface"; +import {IInjectable} from "../common/common"; /** * Service which manages loading of templates from a ViewConfig. @@ -66,7 +67,7 @@ export class TemplateFactory { * @return {string|Promise.} The template html as a string, or a promise * for that string. */ - fromProvider(provider: Function, params: any, injectFn: Function) { + fromProvider(provider: IInjectable, params: any, injectFn: Function) { return injectFn(provider); }; } \ No newline at end of file diff --git a/test/viewDirectiveSpec.js b/test/viewDirectiveSpec.js index 2b22ae61d..f86fe77ad 100644 --- a/test/viewDirectiveSpec.js +++ b/test/viewDirectiveSpec.js @@ -743,3 +743,341 @@ describe('uiView controllers or onEnter handlers', function() { expect(count).toBe(1); })); }); + + +describe('angular 1.5+ style .component()', function() { + var el, app, scope, log, svcs, $stateProvider; + + beforeEach((function() { + app = angular.module('foo', []); + + // ng 1.2 directive (manually bindToController) + app.directive('ng12Directive', function () { + return { + restrict: 'E', + scope: { data: '=' }, + templateUrl: '/comp_tpl.html', + controller: function($scope) { this.data = $scope.data; }, + controllerAs: '$ctrl' + } + }); + + // ng 1.3-1.4 directive with bindToController + app.directive('ng13Directive', function () { + return { + scope: { data: '=' }, + templateUrl: '/comp_tpl.html', + controller: function() {}, + bindToController: true, + controllerAs: '$ctrl' + } + }); + + // ng 1.5+ component + if (angular.version.minor >= 5) { + app.component('ngComponent', { + bindings: { data: '<', data2: '<' }, + templateUrl: '/comp_tpl.html', + controller: function () { + this.$onInit = function () { + log += "onInit;" + } + } + }); + + app.component('header', { + bindings: { status: '<' }, + template: '#{{ $ctrl.status }}#' + }); + } + })); + + beforeEach(module('ui.router', 'foo')); + beforeEach(module(function(_$stateProvider_) { + $stateProvider = _$stateProvider_; + })); + + beforeEach(inject(function($rootScope, _$httpBackend_, _$compile_, _$state_, _$q_) { + svcs = { $httpBackend: _$httpBackend_, $compile: _$compile_, $state: _$state_, $q: _$q_ }; + scope = $rootScope.$new(); + log = ""; + el = angular.element('
'); + svcs.$compile(el)(scope); + })); + + describe('routing using component templates', function() { + beforeEach(function() { + $stateProvider.state('cmp_tpl', { + url: '/cmp_tpl', + templateUrl: '/state_tpl.html', + controller: function() {}, + resolve: { data: function() { return "DATA!"; } } + }); + }); + + it('should work with directives which themselves have templateUrls', function() { + var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q; + + $httpBackend.expectGET('/state_tpl.html').respond('xx'); + $httpBackend.expectGET('/comp_tpl.html').respond('-{{ $ctrl.data }}-'); + + $state.transitionTo('cmp_tpl'); + $q.flush(); + + // Template has not yet been fetched + var directiveEl = el[0].querySelector('div ui-view ng12-directive'); + expect(directiveEl).toBeNull(); + expect($state.current.name).toBe(''); + + // Fetch templates + $httpBackend.flush(); + directiveEl = el[0].querySelector('div ui-view ng12-directive'); + expect(directiveEl).toBeDefined(); + expect($state.current.name).toBe('cmp_tpl'); + + expect(angular.element(directiveEl).data('$ng12DirectiveController')).toBeDefined(); + expect(el.text()).toBe('x-DATA!-x'); + }); + + if (angular.version.minor >= 3) { + it('should work with ng 1.3+ bindToController directives', function () { + var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q; + + $httpBackend.expectGET('/state_tpl.html').respond('xx'); + $httpBackend.expectGET('/comp_tpl.html').respond('-{{ $ctrl.data }}-'); + + $state.transitionTo('cmp_tpl'); + $q.flush(); + $httpBackend.flush(); + + directiveEl = el[0].querySelector('div ui-view ng13-directive'); + expect(directiveEl).toBeDefined(); + expect($state.current.name).toBe('cmp_tpl'); + + expect(angular.element(directiveEl).data('$ng13DirectiveController')).toBeDefined(); + expect(el.text()).toBe('x-DATA!-x'); + }); + } + + if (angular.version.minor >= 5) { + it('should work with ng 1.5+ .component()s', function () { + var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q; + + $httpBackend.expectGET('/state_tpl.html').respond('xx'); + $httpBackend.expectGET('/comp_tpl.html').respond('-{{ $ctrl.data }}-'); + + $state.transitionTo('cmp_tpl'); + $q.flush(); + $httpBackend.flush(); + + directiveEl = el[0].querySelector('div ui-view ng-component'); + expect(directiveEl).toBeDefined(); + expect($state.current.name).toBe('cmp_tpl'); + + expect(angular.element(directiveEl).data('$ngComponentController')).toBeDefined(); + expect(el.text()).toBe('x-DATA!-x'); + }); + } + }); + + describe('+ component: declaration', function() { + it('should disallow controller/template configuration', function () { + var stateDef = { + url: '/route2cmp', + component: 'ng12Directive', + resolve: { data: function() { return "DATA!"; } } + }; + + expect(function() { $stateProvider.state('route2cmp', extend({template: "fail"} , stateDef)); }).toThrow(); + expect(function() { $stateProvider.state('route2cmp', extend({templateUrl: "fail.html"} , stateDef)); }).toThrow(); + expect(function() { $stateProvider.state('route2cmp', extend({templateProvider: function() {}} , stateDef)); }).toThrow(); + expect(function() { $stateProvider.state('route2cmp', extend({controllerAs: "fail"} , stateDef)); }).toThrow(); + expect(function() { $stateProvider.state('route2cmp', extend({controller: "FailCtrl"} , stateDef)); }).toThrow(); + expect(function() { $stateProvider.state('route2cmp', extend({controllerProvider: function() {}} , stateDef)); }).toThrow(); + + expect(function() { $stateProvider.state('route2cmp', stateDef); }).not.toThrow(); + }); + + it('should work with angular 1.2+ directives', function () { + $stateProvider.state('route2cmp', { + url: '/route2cmp', + component: 'ng12Directive', + resolve: { data: function() { return "DATA!"; } } + }); + + var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q; + + $state.transitionTo('route2cmp'); + $httpBackend.expectGET('/comp_tpl.html').respond('-{{ $ctrl.data }}-'); + $q.flush(); + $httpBackend.flush(); + + directiveEl = el[0].querySelector('div ui-view ng12-directive'); + expect(directiveEl).toBeDefined(); + expect($state.current.name).toBe('route2cmp'); + expect(el.text()).toBe('-DATA!-'); + }); + + if (angular.version.minor >= 3) { + it('should work with angular 1.3+ bindToComponent directives', function () { + $stateProvider.state('route2cmp', { + url: '/route2cmp', + component: 'ng13Directive', + resolve: { data: function() { return "DATA!"; } } + }); + + var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q; + + $state.transitionTo('route2cmp'); + $httpBackend.expectGET('/comp_tpl.html').respond('-{{ $ctrl.data }}-'); + $q.flush(); + $httpBackend.flush(); + + directiveEl = el[0].querySelector('div ui-view ng13-directive'); + expect(directiveEl).toBeDefined(); + expect($state.current.name).toBe('route2cmp'); + expect(el.text()).toBe('-DATA!-'); + }); + } + + if (angular.version.minor >= 5) { + it('should work with angular 1.5+ .component()s', function () { + $stateProvider.state('route2cmp', { + url: '/route2cmp', + component: 'ngComponent', + resolve: { data: function() { return "DATA!"; } } + }); + + var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q; + + $state.transitionTo('route2cmp'); + $httpBackend.expectGET('/comp_tpl.html').respond('-{{ $ctrl.data }}-'); + $q.flush(); + $httpBackend.flush(); + + directiveEl = el[0].querySelector('div ui-view ng-component'); + expect(directiveEl).toBeDefined(); + expect($state.current.name).toBe('route2cmp'); + expect(el.text()).toBe('-DATA!-'); + }); + } + }); + + if (angular.version.minor >= 5) { + describe('+ named views with component: declaration', function () { + var stateDef; + beforeEach(function () { + stateDef = { + url: '/route2cmp', + views: { + header: {component: "header"}, + content: {component: "ngComponent"} + }, + resolve: { + status: function () { return "awesome"; }, + data: function () { return "DATA!"; } + } + }; + + el = angular.element('
'); + svcs.$compile(el)(scope); + }); + + it('should disallow controller/template configuration in the view', function () { + expect(function () { + var state = extend({}, stateDef); + state.views.header.template = "fails"; + $stateProvider.state('route2cmp', state); + }).toThrow(); + expect(function () { $stateProvider.state('route2cmp', stateDef); }).not.toThrow(); + }); + + it('should render components as views', function () { + $stateProvider.state('route2cmp', stateDef); + var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q; + + $state.transitionTo('route2cmp'); + $httpBackend.expectGET('/comp_tpl.html').respond('-{{ $ctrl.data }}-'); + $q.flush(); + $httpBackend.flush(); + var header = el[0].querySelector('[ui-view=header]'); + var content = el[0].querySelector('[ui-view=content]'); + + expect(header.textContent).toBe('#awesome#'); + expect(content.textContent).toBe('-DATA!-'); + }); + + it('should allow a component view declaration to use a string as a shorthand', function () { + stateDef = { + url: '/route2cmp', + views: { header: 'header', content: 'ngComponent' }, + resolve: { + status: function () { return "awesome"; }, + data: function () { return "DATA!"; } + } + }; + $stateProvider.state('route2cmp', stateDef); + var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q; + + $state.transitionTo('route2cmp'); + $httpBackend.expectGET('/comp_tpl.html').respond('-{{ $ctrl.data }}-'); + $q.flush(); + $httpBackend.flush(); + var header = el[0].querySelector('[ui-view=header]'); + var content = el[0].querySelector('[ui-view=content]'); + + expect(header.textContent).toBe('#awesome#'); + expect(content.textContent).toBe('-DATA!-'); + }); + }); + } + + describe('+ bindings: declaration', function() { + it('should provide the named component binding with data from the named resolve', function () { + $stateProvider.state('route2cmp', { + url: '/route2cmp', + component: 'ng12Directive', + bindings: { data: "foo" }, + resolve: { foo: function() { return "DATA!"; } } + }); + + var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q; + + $state.transitionTo('route2cmp'); + $httpBackend.expectGET('/comp_tpl.html').respond('-{{ $ctrl.data }}-'); + $q.flush(); + $httpBackend.flush(); + + directiveEl = el[0].querySelector('div ui-view ng12-directive'); + expect(directiveEl).toBeDefined(); + expect($state.current.name).toBe('route2cmp'); + expect(el.text()).toBe('-DATA!-'); + }); + + if (angular.version.minor >= 5) { + it('should provide default bindings for any component bindings omitted in the state.bindings map', function () { + $stateProvider.state('route2cmp', { + url: '/route2cmp', + component: 'ngComponent', + bindings: { data: "foo" }, + resolve: { + foo: function() { return "DATA!"; }, + data2: function() { return "DATA2!"; } + } + }); + + var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q; + + $state.transitionTo('route2cmp'); + $httpBackend.expectGET('/comp_tpl.html').respond('-{{ $ctrl.data }}.{{ $ctrl.data2 }}-'); + $q.flush(); + $httpBackend.flush(); + + directiveEl = el[0].querySelector('div ui-view ng-component'); + expect(directiveEl).toBeDefined(); + expect($state.current.name).toBe('route2cmp'); + expect(el.text()).toBe('-DATA!.DATA2!-'); + }); + } + }); +});