From 50b0c1a478f64dfac146c1ca5f92a039cb82dc1c Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 13 Jan 2016 10:12:02 +0000 Subject: [PATCH 01/20] feat($compile): call `$ngOnInit` on directive controllers after controller construction This enables option three of https://github.com/angular/angular.js/issues/13510#issuecomment-164140194 by allowing the creator of directive controllers using ES6 classes to have a hook that is called when the bindings are definitely available. Moreover this will help solve the problem of accessing `require`d controllers from controller instances without resorting to wiring up in a `link` function. See https://github.com/angular/angular.js/issues/5893 --- src/ng/compile.js | 10 +++++++++- test/ng/compileSpec.js | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 995eaf23730e..d944d65cf984 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1072,7 +1072,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { transclude: options.transclude, scope: {}, bindToController: options.bindings || {}, - restrict: 'E' + restrict: 'E', + require: options.require }; } @@ -2388,6 +2389,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } + // Trigger the `$onInit` method on all controllers that have one + forEach(elementControllers, function(controller) { + if (isFunction(controller.instance.$onInit)) { + controller.instance.$onInit(); + } + }); + // PRELINKING for (i = 0, ii = preLinkFns.length; i < ii; i++) { linkFn = preLinkFns[i]; diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index f885d3106433..eeb3df03513d 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -4929,6 +4929,32 @@ describe('$compile', function() { }); }); + it('should call `controller.$onInit`, if provided after all the controllers have been constructed', function() { + + function Controller1($element) { this.id = 1; this.element = $element; } + Controller1.prototype.$onInit = jasmine.createSpy('$onInit').andCallFake(function() { + expect(this.element.controller('d1').id).toEqual(1); + expect(this.element.controller('d2').id).toEqual(2); + }); + + function Controller2($element) { this.id = 2; this.element = $element; } + Controller2.prototype.$onInit = jasmine.createSpy('$onInit').andCallFake(function() { + expect(this.element.controller('d1').id).toEqual(1); + expect(this.element.controller('d2').id).toEqual(2); + }); + + angular.module('my', []) + .directive('d1', function() { return { controller: Controller1 }; }) + .directive('d2', function() { return { controller: Controller2 }; }); + + module('my'); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + expect(Controller1.prototype.$onInit).toHaveBeenCalledOnce(); + expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce(); + }); + }); + describe('should not overwrite @-bound property each digest when not present', function() { it('when creating new scope', function() { module(function($compileProvider) { From cf4e7beb4c1e0d82d08e307f3ddd4448af9f6951 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 13 Jan 2016 14:08:11 +0000 Subject: [PATCH 02/20] test($compile): check that $onInit is called correctly for ES6 classes --- test/ng/compileSpec.js | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index eeb3df03513d..abb186bb95c2 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -4244,6 +4244,22 @@ describe('$compile', function() { if (!/chrome/i.test(navigator.userAgent)) return; /*jshint -W061 */ var controllerCalled = false; + var Controller = eval( + "class Foo {\n" + + " constructor($scope) {}\n" + + " $onInit() { this.check(); }\n" + + " check() {\n" + + " expect(this.data).toEqualData({\n" + + " 'foo': 'bar',\n" + + " 'baz': 'biz'\n" + + " });\n" + + " expect(this.str).toBe('Hello, world!');\n" + + " expect(this.fn()).toBe('called!');\n" + + " controllerCalled = true;\n" + + " }\n" + + "}"); + spyOn(Controller.prototype, '$onInit'); + module(function($compileProvider) { $compileProvider.directive('fooDir', valueFn({ template: '

isolate

', @@ -4252,20 +4268,7 @@ describe('$compile', function() { 'str': '@dirStr', 'fn': '&dirFn' }, - controller: eval( - "class Foo {" + - " constructor($scope) {}" + - " check() {" + - " expect(this.data).toEqualData({" + - " 'foo': 'bar'," + - " 'baz': 'biz'" + - " });" + - " expect(this.str).toBe('Hello, world!');" + - " expect(this.fn()).toBe('called!');" + - " controllerCalled = true;" + - " }" + - "}" - ), + controller: Controller, controllerAs: 'test', bindToController: true })); @@ -4280,6 +4283,7 @@ describe('$compile', function() { element = $compile('
')($rootScope); + expect(Controller.prototype.$onInit).toHaveBeenCalled(); element.data('$fooDirController').check(); expect(controllerCalled).toBe(true); }); @@ -4287,6 +4291,7 @@ describe('$compile', function() { }); + it('should update @-bindings on controller when bindToController and attribute change observed', function() { module(function($compileProvider) { $compileProvider.directive('atBinding', valueFn({ From 452dd398ae715dafd241a0bf2dafaef8d36a4faa Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 13 Jan 2016 14:23:19 +0000 Subject: [PATCH 03/20] docs($compile): document the new `$onInit` controller hook --- src/ng/compile.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index d944d65cf984..81063db9593a 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -218,8 +218,18 @@ * definition: `controller: 'myCtrl as myAlias'`. * * When an isolate scope is used for a directive (see above), `bindToController: true` will - * allow a component to have its properties bound to the controller, rather than to scope. When the controller - * is instantiated, the initial values of the isolate scope bindings will be available if the controller is not an ES6 class. + * allow a component to have its properties bound to the controller, rather than to scope. + * + * After the controller is instantiated, the initial values of the isolate scope bindings will bound to the controller + * properties. You can access these bindings once they have been initialized by providing a controller method called + * `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings + * initialized. + * + *
+ * **Deprecation warning:** although bindings for non-ES6 class controllers are currently + * bound to `this` before the controller constructor is called, this use is now deprecated. Please place initialization + * code that relies upon bindings inside a `$onInit` method on the controller, instead. + *
* * It is also possible to set `bindToController` to an object hash with the same format as the `scope` property. * This will set up the scope bindings to the controller directly. Note that `scope` can still be used @@ -255,6 +265,10 @@ * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns * `true` if the specified slot contains content (i.e. one or more DOM nodes). * + * The controller can provide the following methods that act as life-cycle hooks: + * * `$onInit` - Called on each controller after all the controllers on an element have been constructed and + * had their bindings initialized. This is a good place to put initialization code for your controller. + * * #### `require` * Require another directive and inject its controller as the fourth argument to the linking function. The * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the From f72ecbd48ddc02ee0ba3c39fa0e36e80e7655394 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 13 Jan 2016 14:49:33 +0000 Subject: [PATCH 04/20] feat($compile): allow `require` to be an object This provides an elegant alternative to the array form of the `require` property but also helps to support binding of `require`d controllers to directive controllers. --- src/ng/compile.js | 24 ++++++++++++++++++++---- test/ng/compileSpec.js | 22 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 81063db9593a..e4d4d4d3cfa7 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -271,10 +271,16 @@ * * #### `require` * Require another directive and inject its controller as the fourth argument to the linking function. The - * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the - * injected argument will be an array in corresponding order. If no such directive can be - * found, or if the directive does not have a controller, then an error is raised (unless no link function - * is specified, in which case error checking is skipped). The name can be prefixed with: + * `require` property can be a string, an array or an object: + * * a **string** containing the name of the directive to pass to the linking function + * * an **array** containing the names of directives to pass to the linking function. The argument passed to the + * linking function will be an array of controllers in the same order as the names in the `require` property + * * an **object** whose property values are the names of the directibes to pass to the linking function. The argument + * passed to the linking function will also be an object with matching keys, whose values will be the corresponding + * controller. + * + * If no such directive(s) can be found, or if the directive does not have a controller, then an error is raised + * (unless no link function is specified, in which case error checking is skipped). The name can be prefixed with: * * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. @@ -2295,6 +2301,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { for (var i = 0, ii = require.length; i < ii; i++) { value[i] = getControllers(directiveName, require[i], $element, elementControllers); } + } else if (isObject(require)) { + value = {}; + forEach(require, function(controller, property) { + value[property] = getControllers(directiveName, controller, $element, elementControllers); + }); } return value || null; @@ -2401,6 +2412,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { removeControllerBindingWatches = initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } + + if (isObject(controllerDirective.require) && !isArray(controllerDirective.require)) { + var controllers = getControllers(name, controllerDirective.require, $element, elementControllers); + console.log(controllers); + } } // Trigger the `$onInit` method on all controllers that have one diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index abb186bb95c2..b4fa711d8dcd 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -5700,6 +5700,28 @@ describe('$compile', function() { }); }); + it('should support multiple controllers as an object hash', function() { + module(function() { + directive('c1', valueFn({ + controller: function() { this.name = 'c1'; } + })); + directive('c2', valueFn({ + controller: function() { this.name = 'c2'; } + })); + directive('dep', function(log) { + return { + require: { myC1: '^c1', myC2: '^c2' }, + link: function(scope, element, attrs, controllers) { + log('dep:' + controllers.myC1.name + '-' + controller.myC2.name); + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('dep:c1-c2'); + }); + }); it('should instantiate the controller just once when template/templateUrl', function() { var syncCtrlSpy = jasmine.createSpy('sync controller'), From b2c0b056d54dee14ff5610ab1ac11f8a4b7c79f6 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 13 Jan 2016 14:58:10 +0000 Subject: [PATCH 05/20] feat($compile): allow required controllers to be bound to the directive controller If directives are required through an object hash, rather than a string or array, the required directives' controllers are bound to the current directive's controller in much the same way as the properties are bound to using `bindToController`. The binding is done after the controller has been constructed and all the bindings are guaranteed to be complete by the time the controller's `$onInit` method is called. This change makes it much simpler to access require controllers without the need for manually wiring them up in link functions. In particular this enables support for `require` in directives defined using `mod.component()` --- src/ng/compile.js | 4 ++-- test/ng/compileSpec.js | 46 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index e4d4d4d3cfa7..6ef005071205 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -2414,8 +2414,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (isObject(controllerDirective.require) && !isArray(controllerDirective.require)) { - var controllers = getControllers(name, controllerDirective.require, $element, elementControllers); - console.log(controllers); + var controllerObj = getControllers(name, controllerDirective.require, $element, elementControllers); + extend(controller.instance, controllerObj); } } diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index b4fa711d8dcd..85ed5ce93a54 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -5338,6 +5338,48 @@ describe('$compile', function() { }); }); + it('should bind the required controllers to the directive controller, if provided as an object', function() { + var parentController, siblingController; + + function ParentController() { this.name = 'Parent'; } + function SiblingController() { this.name = 'Sibling'; } + function MeController() { this.name = 'Me'; } + MeController.prototype.$onInit = function() { + parentController = this.container; + siblingController = this.friend; + }; + spyOn(MeController.prototype, '$onInit').andCallThrough(); + + angular.module('my', []) + .directive('me', function() { + return { + restrict: 'E', + scope: {}, + require: { container: '^parent', friend: 'sibling' }, + controller: MeController + }; + }) + .directive('parent', function() { + return { + restrict: 'E', + scope: {}, + controller: ParentController + }; + }) + .directive('sibling', function() { + return { + controller: SiblingController + }; + }); + + module('my'); + inject(function($compile, $rootScope, meDirective) { + element = $compile('')($rootScope); + expect(MeController.prototype.$onInit).toHaveBeenCalled(); + expect(parentController).toEqual(jasmine.any(ParentController)); + expect(siblingController).toEqual(jasmine.any(SiblingController)); + }); + }); it('should require controller of an isolate directive from a non-isolate directive on the ' + 'same element', function() { @@ -5712,7 +5754,7 @@ describe('$compile', function() { return { require: { myC1: '^c1', myC2: '^c2' }, link: function(scope, element, attrs, controllers) { - log('dep:' + controllers.myC1.name + '-' + controller.myC2.name); + log('dep:' + controllers.myC1.name + '-' + controllers.myC2.name); } }; }); @@ -9488,5 +9530,5 @@ describe('$compile', function() { })); }); }); - }); + }); }); From 37793ce84fae5e2111025d39024de3e12d0828e0 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 14 Jan 2016 09:14:23 +0000 Subject: [PATCH 06/20] feat($compile): allow `require` to be an object This provides an elegant alternative to the array form of the `require` property but also helps to support binding of `require`d controllers to directive controllers. --- src/ng/compile.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 6ef005071205..2054417e19d8 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -275,9 +275,9 @@ * * a **string** containing the name of the directive to pass to the linking function * * an **array** containing the names of directives to pass to the linking function. The argument passed to the * linking function will be an array of controllers in the same order as the names in the `require` property - * * an **object** whose property values are the names of the directibes to pass to the linking function. The argument - * passed to the linking function will also be an object with matching keys, whose values will be the corresponding - * controller. + * * an **object** whose property values are the names of the directives to pass to the linking function. The argument + * passed to the linking function will also be an object with matching keys, whose values will hold the corresponding + * controllers. * * If no such directive(s) can be found, or if the directive does not have a controller, then an error is raised * (unless no link function is specified, in which case error checking is skipped). The name can be prefixed with: From a12ddc64091a9c94d3cefb4efd156ed930fe6b75 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 14 Jan 2016 09:30:34 +0000 Subject: [PATCH 07/20] test($compile): check explicit return controllers are not broken by binding require --- test/ng/compileSpec.js | 49 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 85ed5ce93a54..2ad90d64a998 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -5381,6 +5381,55 @@ describe('$compile', function() { }); }); + it('should bind required controllers to controller that has an explicit constructor return value', function() { + var parentController, siblingController, meController; + + function ParentController() { this.name = 'Parent'; } + function SiblingController() { this.name = 'Sibling'; } + function MeController() { + meController = { + name: 'Me', + $onInit: function() { + parentController = this.container; + siblingController = this.friend; + } + }; + spyOn(meController, '$onInit').andCallThrough(); + return meController; + } + + angular.module('my', []) + .directive('me', function() { + return { + restrict: 'E', + scope: {}, + require: { container: '^parent', friend: 'sibling' }, + controller: MeController + }; + }) + .directive('parent', function() { + return { + restrict: 'E', + scope: {}, + controller: ParentController + }; + }) + .directive('sibling', function() { + return { + controller: SiblingController + }; + }); + + module('my'); + inject(function($compile, $rootScope, meDirective) { + element = $compile('')($rootScope); + expect(meController.$onInit).toHaveBeenCalled(); + expect(parentController).toEqual(jasmine.any(ParentController)); + expect(siblingController).toEqual(jasmine.any(SiblingController)); + }); + }); + + it('should require controller of an isolate directive from a non-isolate directive on the ' + 'same element', function() { var IsolateController = function() {}; From 59feecc4231c8e6385a602f37ebe650bdad3b1b9 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 14 Jan 2016 09:43:33 +0000 Subject: [PATCH 08/20] feat($compile): allow required controllers to be bound to the directive controller If directives are required through an object hash, rather than a string or array, the required directives' controllers are bound to the current directive's controller in much the same way as the properties are bound to using `bindToController`. The binding is done after the controller has been constructed and all the bindings are guaranteed to be complete by the time the controller's `$onInit` method is called. This change makes it much simpler to access require controllers without the need for manually wiring them up in link functions. In particular this enables support for `require` in directives defined using `mod.component()` --- test/ng/compileSpec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 2ad90d64a998..0bef044155ad 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -9579,5 +9579,5 @@ describe('$compile', function() { })); }); }); - }); + }); }); From 8040babad888ab6c430b7f771e49619290775f1d Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 14 Jan 2016 09:44:08 +0000 Subject: [PATCH 09/20] feat($compile): call `$ngOnInit` on directive controllers after controller construction This enables option three of https://github.com/angular/angular.js/issues/13510#issuecomment-164140194 by allowing the creator of directive controllers using ES6 classes to have a hook that is called when the bindings are definitely available. Moreover this will help solve the problem of accessing `require`d controllers from controller instances without resorting to wiring up in a `link` function. See https://github.com/angular/angular.js/issues/5893 --- test/ng/compileSpec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 0bef044155ad..8202f9e70a4e 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -4936,21 +4936,21 @@ describe('$compile', function() { it('should call `controller.$onInit`, if provided after all the controllers have been constructed', function() { - function Controller1($element) { this.id = 1; this.element = $element; } - Controller1.prototype.$onInit = jasmine.createSpy('$onInit').andCallFake(function() { + function check() { + /*jshint validthis:true */ expect(this.element.controller('d1').id).toEqual(1); expect(this.element.controller('d2').id).toEqual(2); - }); + } + + function Controller1($element) { this.id = 1; this.element = $element; } + Controller1.prototype.$onInit = jasmine.createSpy('$onInit').andCallFake(check); function Controller2($element) { this.id = 2; this.element = $element; } - Controller2.prototype.$onInit = jasmine.createSpy('$onInit').andCallFake(function() { - expect(this.element.controller('d1').id).toEqual(1); - expect(this.element.controller('d2').id).toEqual(2); - }); + Controller2.prototype.$onInit = jasmine.createSpy('$onInit').andCallFake(check); angular.module('my', []) - .directive('d1', function() { return { controller: Controller1 }; }) - .directive('d2', function() { return { controller: Controller2 }; }); + .directive('d1', valueFn({ controller: Controller1 })) + .directive('d2', valueFn({ controller: Controller2 })); module('my'); inject(function($compile, $rootScope) { From 9e6db1a267f505c793c49c73631a602ddcf6e98b Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 14 Jan 2016 10:33:25 +0000 Subject: [PATCH 10/20] feat($compile): allow required controllers to be bound to the directive controller If directives are required through an object hash, rather than a string or array, the required directives' controllers are bound to the current directive's controller in much the same way as the properties are bound to using `bindToController`. The binding is done after the controller has been constructed and all the bindings are guaranteed to be complete by the time the controller's `$onInit` method is called. This change makes it much simpler to access require controllers without the need for manually wiring them up in link functions. In particular this enables support for `require` in directives defined using `mod.component()` --- src/ng/compile.js | 78 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/ng/compile.js b/src/ng/compile.js index 2054417e19d8..c73ab9964300 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -279,6 +279,10 @@ * passed to the linking function will also be an object with matching keys, whose values will hold the corresponding * controllers. * + * If the `require` property is an object and the directive provides a controller, then the required controllers are + * bound to the controller using the keys of the `require` property. See the {@link $compileProvider#component} helper + * for an example of how this can be used. + * * If no such directive(s) can be found, or if the directive does not have a controller, then an error is raised * (unless no link function is specified, in which case error checking is skipped). The name can be prefixed with: * @@ -1025,6 +1029,80 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * * ``` * + * ### Intercomponent Communication + * Directives can require the controllers of other directives to enable communication + * between the directives. This can be achieved in a component by providing an + * object mapping for the `require` property. Here is the tab pane example built + * from components... + * + * + * + * angular.module('docsTabsExample', []) + * .component('myTabs', { + * transclude: true, + * controller: function() { + * var panes = this.panes = []; + * + * this.select = function(pane) { + * angular.forEach(panes, function(pane) { + * pane.selected = false; + * }); + * pane.selected = true; + * }; + * + * this.addPane = function(pane) { + * if (panes.length === 0) { + * this.select(pane); + * } + * panes.push(pane); + * }; + * }, + * templateUrl: 'my-tabs.html' + * }) + * .component('myPane', { + * transclude: true, + * require: {tabsCtrl: '^myTabs'}, + * bindings: { + * title: '@' + * }, + * controller: function() { + * this.$onInit = function() { + * this.tabsCtrl.addPane(this); + * console.log(this); + * }; + * }, + * templateUrl: 'my-pane.html' + * }); + * + * + * + * + *

Hello

+ *

Lorem ipsum dolor sit amet

+ *
+ * + *

World

+ * Mauris elementum elementum enim at suscipit. + *

counter: {{i || 0}}

+ *
+ *
+ *
+ * + *
+ * + *
+ *
+ *
+ * + *
+ *
+ *
+ * + * *
* Components are also useful as route templates (e.g. when using * {@link ngRoute ngRoute}): From 1d18df20fe6dd0ab50f9285f03ff979f302f5874 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 15 Jan 2016 10:24:25 +0000 Subject: [PATCH 11/20] fix($compile): ensure controllers with return value constructors are required correctly See https://github.com/angular/angular.js/pull/13763#discussion_r49836041 --- src/ng/compile.js | 11 ++++++---- test/ng/compileSpec.js | 47 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index c73ab9964300..c97bf3cf4a64 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -2490,12 +2490,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { removeControllerBindingWatches = initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } + } - if (isObject(controllerDirective.require) && !isArray(controllerDirective.require)) { - var controllerObj = getControllers(name, controllerDirective.require, $element, elementControllers); - extend(controller.instance, controllerObj); + // Bind the required controllers to the controller, if `require` is an object + forEach(controllerDirectives, function(controllerDirective, name) { + var require = controllerDirective.require; + if (!isArray(require) && isObject(require)) { + extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers)); } - } + }); // Trigger the `$onInit` method on all controllers that have one forEach(elementControllers, function(controller) { diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 8202f9e70a4e..7353471ba43d 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -5430,6 +5430,53 @@ describe('$compile', function() { }); + it('should bind required controllers to controllers that return an explicit constructor return value', function() { + var parentController, containerController, siblingController, friendController, meController; + + function MeController() { + this.name = 'Me'; + this.$onInit = function() { + containerController = this.container; + friendController = this.friend; + }; + } + function ParentController() { + return parentController = { name: 'Parent' }; + } + function SiblingController() { + return siblingController = { name: 'Sibling' }; + } + + angular.module('my', []) + .directive('me', function() { + return { + restrict: 'E', + scope: {}, + require: { container: '^parent', friend: 'sibling' }, + controller: MeController + }; + }) + .directive('parent', function() { + return { + restrict: 'E', + scope: {}, + controller: ParentController + }; + }) + .directive('sibling', function() { + return { + controller: SiblingController + }; + }); + + module('my'); + inject(function($compile, $rootScope, meDirective) { + element = $compile('')($rootScope); + expect(containerController).toEqual(parentController); + expect(friendController).toEqual(siblingController); + }); + }); + it('should require controller of an isolate directive from a non-isolate directive on the ' + 'same element', function() { var IsolateController = function() {}; From 4b304a6201e513c6017a0ef72f14e341f983b0bf Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 15 Jan 2016 11:14:49 +0000 Subject: [PATCH 12/20] docs($compile): squash me --- src/ng/compile.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index c97bf3cf4a64..865120e50440 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -267,7 +267,8 @@ * * The controller can provide the following methods that act as life-cycle hooks: * * `$onInit` - Called on each controller after all the controllers on an element have been constructed and - * had their bindings initialized. This is a good place to put initialization code for your controller. + * had their bindings initialized (and before the pre & post linking functions for the directives on + * this element). This is a good place to put initialization code for your controller. * * #### `require` * Require another directive and inject its controller as the fourth argument to the linking function. The @@ -280,11 +281,12 @@ * controllers. * * If the `require` property is an object and the directive provides a controller, then the required controllers are - * bound to the controller using the keys of the `require` property. See the {@link $compileProvider#component} helper - * for an example of how this can be used. + * bound to the controller using the keys of the `require` property. This binding occurs after all the controllers + * have been constructed but before `$onInit` is called. + * See the {@link $compileProvider#component} helper for an example of how this can be used. * - * If no such directive(s) can be found, or if the directive does not have a controller, then an error is raised - * (unless no link function is specified, in which case error checking is skipped). The name can be prefixed with: + * If no such required directive(s) can be found, or if the directive does not have a controller, then an error is + * raised (unless no link function is specified, in which case error checking is skipped). The name can be prefixed with: * * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. From cae40cabd6dc26a381261b39944f3a21c8cb0180 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 15 Jan 2016 11:22:56 +0000 Subject: [PATCH 13/20] fix($compile): only bind required controllers if `bindToController` is truthy --- src/ng/compile.js | 4 +-- test/ng/compileSpec.js | 58 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 865120e50440..bbfb88d9d312 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -280,7 +280,7 @@ * passed to the linking function will also be an object with matching keys, whose values will hold the corresponding * controllers. * - * If the `require` property is an object and the directive provides a controller, then the required controllers are + * If the `require` property is an object and `bindToController` is truthy, then the required controllers are * bound to the controller using the keys of the `require` property. This binding occurs after all the controllers * have been constructed but before `$onInit` is called. * See the {@link $compileProvider#component} helper for an example of how this can be used. @@ -2497,7 +2497,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // Bind the required controllers to the controller, if `require` is an object forEach(controllerDirectives, function(controllerDirective, name) { var require = controllerDirective.require; - if (!isArray(require) && isObject(require)) { + if (controllerDirective.bindToController && !isArray(require) && isObject(require)) { extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers)); } }); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 7353471ba43d..2da5dfc56bd0 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -5338,7 +5338,7 @@ describe('$compile', function() { }); }); - it('should bind the required controllers to the directive controller, if provided as an object', function() { + it('should bind the required controllers to the directive controller, if provided as an object and bindToController is truthy', function() { var parentController, siblingController; function ParentController() { this.name = 'Parent'; } @@ -5356,7 +5356,9 @@ describe('$compile', function() { restrict: 'E', scope: {}, require: { container: '^parent', friend: 'sibling' }, - controller: MeController + bindToController: true, + controller: MeController, + controllerAs: '$ctrl' }; }) .directive('parent', function() { @@ -5381,6 +5383,50 @@ describe('$compile', function() { }); }); + + it('should not bind required controllers bindToController is falsy', function() { + var parentController, siblingController; + + function ParentController() { this.name = 'Parent'; } + function SiblingController() { this.name = 'Sibling'; } + function MeController() { this.name = 'Me'; } + MeController.prototype.$onInit = function() { + parentController = this.container; + siblingController = this.friend; + }; + spyOn(MeController.prototype, '$onInit').andCallThrough(); + + angular.module('my', []) + .directive('me', function() { + return { + restrict: 'E', + scope: {}, + require: { container: '^parent', friend: 'sibling' }, + controller: MeController + }; + }) + .directive('parent', function() { + return { + restrict: 'E', + scope: {}, + controller: ParentController + }; + }) + .directive('sibling', function() { + return { + controller: SiblingController + }; + }); + + module('my'); + inject(function($compile, $rootScope, meDirective) { + element = $compile('')($rootScope); + expect(MeController.prototype.$onInit).toHaveBeenCalled(); + expect(parentController).toBeUndefined(); + expect(siblingController).toBeUndefined(); + }); + }); + it('should bind required controllers to controller that has an explicit constructor return value', function() { var parentController, siblingController, meController; @@ -5404,7 +5450,9 @@ describe('$compile', function() { restrict: 'E', scope: {}, require: { container: '^parent', friend: 'sibling' }, - controller: MeController + bindToController: true, + controller: MeController, + controllerAs: '$ctrl' }; }) .directive('parent', function() { @@ -5453,7 +5501,9 @@ describe('$compile', function() { restrict: 'E', scope: {}, require: { container: '^parent', friend: 'sibling' }, - controller: MeController + bindToController: true, + controller: MeController, + controllerAs: '$ctrl' }; }) .directive('parent', function() { From 276b9eecdceada112c0cf6e6db86fa3d847f7e5b Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 15 Jan 2016 12:25:16 +0000 Subject: [PATCH 14/20] docs($compile): fix typo --- src/ng/compile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index bbfb88d9d312..48feedf0f2a5 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -220,7 +220,7 @@ * When an isolate scope is used for a directive (see above), `bindToController: true` will * allow a component to have its properties bound to the controller, rather than to scope. * - * After the controller is instantiated, the initial values of the isolate scope bindings will bound to the controller + * After the controller is instantiated, the initial values of the isolate scope bindings will be bound to the controller * properties. You can access these bindings once they have been initialized by providing a controller method called * `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings * initialized. From 6d66a7599792c7b2702cdb14ed4e8f7e21e10394 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 15 Jan 2016 12:25:40 +0000 Subject: [PATCH 15/20] fix($compile): only bind required controllers if `bindToController` is truthy --- src/ng/compile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 48feedf0f2a5..e3cc754b4c2f 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -2494,7 +2494,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - // Bind the required controllers to the controller, if `require` is an object + // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy forEach(controllerDirectives, function(controllerDirective, name) { var require = controllerDirective.require; if (controllerDirective.bindToController && !isArray(require) && isObject(require)) { From 32b7da3be10c44ab4cbcd1389bf3ebb178d5f291 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 15 Jan 2016 12:26:42 +0000 Subject: [PATCH 16/20] test($compile): check that $onInit is called correctly for ES6 classes --- test/ng/compileSpec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 2da5dfc56bd0..bbcaa51701f0 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -4284,14 +4284,12 @@ describe('$compile', function() { 'dir-str="Hello, {{whom}}!" ' + 'dir-fn="fn()">')($rootScope); expect(Controller.prototype.$onInit).toHaveBeenCalled(); - element.data('$fooDirController').check(); expect(controllerCalled).toBe(true); }); /*jshint +W061 */ }); - it('should update @-bindings on controller when bindToController and attribute change observed', function() { module(function($compileProvider) { $compileProvider.directive('atBinding', valueFn({ From cb495a502ccdcb726c0bd284804576455bf3811d Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 15 Jan 2016 12:28:01 +0000 Subject: [PATCH 17/20] fix($compile): only bind required controllers if `bindToController` is truthy --- test/ng/compileSpec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index bbcaa51701f0..ed420ecbf80b 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -5382,7 +5382,7 @@ describe('$compile', function() { }); - it('should not bind required controllers bindToController is falsy', function() { + it('should not bind required controllers if bindToController is falsy', function() { var parentController, siblingController; function ParentController() { this.name = 'Parent'; } From 270e2301273c7007e5a8d5867789a8a7302f4f72 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 15 Jan 2016 12:28:27 +0000 Subject: [PATCH 18/20] fix($compile): ensure controllers with return value constructors are required correctly See https://github.com/angular/angular.js/pull/13763#discussion_r49836041 --- test/ng/compileSpec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index ed420ecbf80b..c6a66ef1c883 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -5496,6 +5496,7 @@ describe('$compile', function() { angular.module('my', []) .directive('me', function() { return { + priority: 1, // make sure it is run before sibling to test this case correctly restrict: 'E', scope: {}, require: { container: '^parent', friend: 'sibling' }, From 281d987644c098ba6722902b11860e3fcc6b56e3 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 15 Jan 2016 12:31:20 +0000 Subject: [PATCH 19/20] fix($compile): only bind required controllers if `bindToController` is truthy --- src/ng/compile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index e3cc754b4c2f..8fc513b1e2b3 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -286,7 +286,8 @@ * See the {@link $compileProvider#component} helper for an example of how this can be used. * * If no such required directive(s) can be found, or if the directive does not have a controller, then an error is - * raised (unless no link function is specified, in which case error checking is skipped). The name can be prefixed with: + * raised (unless no link function is specified and the required controllers are not being bound to the directive + * controller, in which case error checking is skipped). The name can be prefixed with: * * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. From 9f76a11234b835289b8714fde04fc0dd09c6ff99 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 15 Jan 2016 15:59:05 +0000 Subject: [PATCH 20/20] test($compile): check that $onInit is called correctly for ES6 classes --- test/ng/compileSpec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index c6a66ef1c883..250e924d50c5 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -4258,7 +4258,7 @@ describe('$compile', function() { " controllerCalled = true;\n" + " }\n" + "}"); - spyOn(Controller.prototype, '$onInit'); + spyOn(Controller.prototype, '$onInit').andCallThrough(); module(function($compileProvider) { $compileProvider.directive('fooDir', valueFn({