diff --git a/src/ng/compile.js b/src/ng/compile.js
index 995eaf23730e..8fc513b1e2b3 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 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.
+ *
+ *
+ * **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,12 +265,29 @@
* 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 (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
- * `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 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 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.
+ *
+ * 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 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.
@@ -1005,6 +1032,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}):
@@ -1072,7 +1173,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
transclude: options.transclude,
scope: {},
bindToController: options.bindings || {},
- restrict: 'E'
+ restrict: 'E',
+ require: options.require
};
}
@@ -2280,6 +2382,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;
@@ -2388,6 +2495,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
+ // 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)) {
+ extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers));
+ }
+ });
+
+ // 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..250e924d50c5 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').andCallThrough();
+
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,7 +4283,7 @@ describe('$compile', function() {
element = $compile('')($rootScope);
- element.data('$fooDirController').check();
+ expect(Controller.prototype.$onInit).toHaveBeenCalled();
expect(controllerCalled).toBe(true);
});
/*jshint +W061 */
@@ -4929,6 +4932,32 @@ describe('$compile', function() {
});
});
+ it('should call `controller.$onInit`, if provided after all the controllers have been constructed', 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(check);
+
+ angular.module('my', [])
+ .directive('d1', valueFn({ controller: Controller1 }))
+ .directive('d2', valueFn({ 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) {
@@ -5307,6 +5336,195 @@ describe('$compile', 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'; }
+ 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' },
+ bindToController: true,
+ controller: MeController,
+ controllerAs: '$ctrl'
+ };
+ })
+ .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 not bind required controllers if 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;
+
+ 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' },
+ bindToController: true,
+ controller: MeController,
+ controllerAs: '$ctrl'
+ };
+ })
+ .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 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 {
+ priority: 1, // make sure it is run before sibling to test this case correctly
+ restrict: 'E',
+ scope: {},
+ require: { container: '^parent', friend: 'sibling' },
+ bindToController: true,
+ controller: MeController,
+ controllerAs: '$ctrl'
+ };
+ })
+ .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() {
@@ -5669,6 +5887,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 + '-' + controllers.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'),