Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 56c3666

Browse files
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`. This only happens if `bindToController` is truthy. 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()` Closes #6040 Closes #5893 Closes #13763
1 parent cd21216 commit 56c3666

File tree

2 files changed

+281
-8
lines changed

2 files changed

+281
-8
lines changed

src/ng/compile.js

+91-7
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@
267267
*
268268
* The controller can provide the following methods that act as life-cycle hooks:
269269
* * `$onInit` - Called on each controller after all the controllers on an element have been constructed and
270-
* had their bindings initialized. This is a good place to put initialization code for your controller.
270+
* had their bindings initialized (and before the pre & post linking functions for the directives on
271+
* this element). This is a good place to put initialization code for your controller.
271272
*
272273
* #### `require`
273274
* Require another directive and inject its controller as the fourth argument to the linking function. The
@@ -279,8 +280,14 @@
279280
* passed to the linking function will also be an object with matching keys, whose values will hold the corresponding
280281
* controllers.
281282
*
282-
* If no such directive(s) can be found, or if the directive does not have a controller, then an error is raised
283-
* (unless no link function is specified, in which case error checking is skipped). The name can be prefixed with:
283+
* If the `require` property is an object and `bindToController` is truthy, then the required controllers are
284+
* bound to the controller using the keys of the `require` property. This binding occurs after all the controllers
285+
* have been constructed but before `$onInit` is called.
286+
* See the {@link $compileProvider#component} helper for an example of how this can be used.
287+
*
288+
* If no such required directive(s) can be found, or if the directive does not have a controller, then an error is
289+
* raised (unless no link function is specified and the required controllers are not being bound to the directive
290+
* controller, in which case error checking is skipped). The name can be prefixed with:
284291
*
285292
* * (no prefix) - Locate the required controller on the current element. Throw an error if not found.
286293
* * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found.
@@ -1032,6 +1039,80 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
10321039
*
10331040
* ```
10341041
*
1042+
* ### Intercomponent Communication
1043+
* Directives can require the controllers of other directives to enable communication
1044+
* between the directives. This can be achieved in a component by providing an
1045+
* object mapping for the `require` property. Here is the tab pane example built
1046+
* from components...
1047+
*
1048+
* <example module="docsTabsExample">
1049+
* <file name="script.js">
1050+
* angular.module('docsTabsExample', [])
1051+
* .component('myTabs', {
1052+
* transclude: true,
1053+
* controller: function() {
1054+
* var panes = this.panes = [];
1055+
*
1056+
* this.select = function(pane) {
1057+
* angular.forEach(panes, function(pane) {
1058+
* pane.selected = false;
1059+
* });
1060+
* pane.selected = true;
1061+
* };
1062+
*
1063+
* this.addPane = function(pane) {
1064+
* if (panes.length === 0) {
1065+
* this.select(pane);
1066+
* }
1067+
* panes.push(pane);
1068+
* };
1069+
* },
1070+
* templateUrl: 'my-tabs.html'
1071+
* })
1072+
* .component('myPane', {
1073+
* transclude: true,
1074+
* require: {tabsCtrl: '^myTabs'},
1075+
* bindings: {
1076+
* title: '@'
1077+
* },
1078+
* controller: function() {
1079+
* this.$onInit = function() {
1080+
* this.tabsCtrl.addPane(this);
1081+
* console.log(this);
1082+
* };
1083+
* },
1084+
* templateUrl: 'my-pane.html'
1085+
* });
1086+
* </file>
1087+
* <file name="index.html">
1088+
* <my-tabs>
1089+
* <my-pane title="Hello">
1090+
* <h4>Hello</h4>
1091+
* <p>Lorem ipsum dolor sit amet</p>
1092+
* </my-pane>
1093+
* <my-pane title="World">
1094+
* <h4>World</h4>
1095+
* <em>Mauris elementum elementum enim at suscipit.</em>
1096+
* <p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p>
1097+
* </my-pane>
1098+
* </my-tabs>
1099+
* </file>
1100+
* <file name="my-tabs.html">
1101+
* <div class="tabbable">
1102+
* <ul class="nav nav-tabs">
1103+
* <li ng-repeat="pane in $ctrl.panes" ng-class="{active:pane.selected}">
1104+
* <a href="" ng-click="$ctrl.select(pane)">{{pane.title}}</a>
1105+
* </li>
1106+
* </ul>
1107+
* <div class="tab-content" ng-transclude></div>
1108+
* </div>
1109+
* </file>
1110+
* <file name="my-pane.html">
1111+
* <div class="tab-pane" ng-show="$ctrl.selected" ng-transclude></div>
1112+
* </file>
1113+
* </example>
1114+
*
1115+
*
10351116
* <br />
10361117
* Components are also useful as route templates (e.g. when using
10371118
* {@link ngRoute ngRoute}):
@@ -2425,12 +2506,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
24252506
removeControllerBindingWatches =
24262507
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
24272508
}
2509+
}
24282510

2429-
if (isObject(controllerDirective.require) && !isArray(controllerDirective.require)) {
2430-
var controllers = getControllers(name, controllerDirective.require, $element, elementControllers);
2431-
console.log(controllers);
2511+
// Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy
2512+
forEach(controllerDirectives, function(controllerDirective, name) {
2513+
var require = controllerDirective.require;
2514+
if (controllerDirective.bindToController && !isArray(require) && isObject(require)) {
2515+
extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers));
24322516
}
2433-
}
2517+
});
24342518

24352519
// Trigger the `$onInit` method on all controllers that have one
24362520
forEach(elementControllers, function(controller) {

test/ng/compileSpec.js

+190-1
Original file line numberDiff line numberDiff line change
@@ -5354,6 +5354,195 @@ describe('$compile', function() {
53545354
});
53555355
});
53565356

5357+
it('should bind the required controllers to the directive controller, if provided as an object and bindToController is truthy', function() {
5358+
var parentController, siblingController;
5359+
5360+
function ParentController() { this.name = 'Parent'; }
5361+
function SiblingController() { this.name = 'Sibling'; }
5362+
function MeController() { this.name = 'Me'; }
5363+
MeController.prototype.$onInit = function() {
5364+
parentController = this.container;
5365+
siblingController = this.friend;
5366+
};
5367+
spyOn(MeController.prototype, '$onInit').andCallThrough();
5368+
5369+
angular.module('my', [])
5370+
.directive('me', function() {
5371+
return {
5372+
restrict: 'E',
5373+
scope: {},
5374+
require: { container: '^parent', friend: 'sibling' },
5375+
bindToController: true,
5376+
controller: MeController,
5377+
controllerAs: '$ctrl'
5378+
};
5379+
})
5380+
.directive('parent', function() {
5381+
return {
5382+
restrict: 'E',
5383+
scope: {},
5384+
controller: ParentController
5385+
};
5386+
})
5387+
.directive('sibling', function() {
5388+
return {
5389+
controller: SiblingController
5390+
};
5391+
});
5392+
5393+
module('my');
5394+
inject(function($compile, $rootScope, meDirective) {
5395+
element = $compile('<parent><me sibling></me></parent>')($rootScope);
5396+
expect(MeController.prototype.$onInit).toHaveBeenCalled();
5397+
expect(parentController).toEqual(jasmine.any(ParentController));
5398+
expect(siblingController).toEqual(jasmine.any(SiblingController));
5399+
});
5400+
});
5401+
5402+
5403+
it('should not bind required controllers if bindToController is falsy', function() {
5404+
var parentController, siblingController;
5405+
5406+
function ParentController() { this.name = 'Parent'; }
5407+
function SiblingController() { this.name = 'Sibling'; }
5408+
function MeController() { this.name = 'Me'; }
5409+
MeController.prototype.$onInit = function() {
5410+
parentController = this.container;
5411+
siblingController = this.friend;
5412+
};
5413+
spyOn(MeController.prototype, '$onInit').andCallThrough();
5414+
5415+
angular.module('my', [])
5416+
.directive('me', function() {
5417+
return {
5418+
restrict: 'E',
5419+
scope: {},
5420+
require: { container: '^parent', friend: 'sibling' },
5421+
controller: MeController
5422+
};
5423+
})
5424+
.directive('parent', function() {
5425+
return {
5426+
restrict: 'E',
5427+
scope: {},
5428+
controller: ParentController
5429+
};
5430+
})
5431+
.directive('sibling', function() {
5432+
return {
5433+
controller: SiblingController
5434+
};
5435+
});
5436+
5437+
module('my');
5438+
inject(function($compile, $rootScope, meDirective) {
5439+
element = $compile('<parent><me sibling></me></parent>')($rootScope);
5440+
expect(MeController.prototype.$onInit).toHaveBeenCalled();
5441+
expect(parentController).toBeUndefined();
5442+
expect(siblingController).toBeUndefined();
5443+
});
5444+
});
5445+
5446+
it('should bind required controllers to controller that has an explicit constructor return value', function() {
5447+
var parentController, siblingController, meController;
5448+
5449+
function ParentController() { this.name = 'Parent'; }
5450+
function SiblingController() { this.name = 'Sibling'; }
5451+
function MeController() {
5452+
meController = {
5453+
name: 'Me',
5454+
$onInit: function() {
5455+
parentController = this.container;
5456+
siblingController = this.friend;
5457+
}
5458+
};
5459+
spyOn(meController, '$onInit').andCallThrough();
5460+
return meController;
5461+
}
5462+
5463+
angular.module('my', [])
5464+
.directive('me', function() {
5465+
return {
5466+
restrict: 'E',
5467+
scope: {},
5468+
require: { container: '^parent', friend: 'sibling' },
5469+
bindToController: true,
5470+
controller: MeController,
5471+
controllerAs: '$ctrl'
5472+
};
5473+
})
5474+
.directive('parent', function() {
5475+
return {
5476+
restrict: 'E',
5477+
scope: {},
5478+
controller: ParentController
5479+
};
5480+
})
5481+
.directive('sibling', function() {
5482+
return {
5483+
controller: SiblingController
5484+
};
5485+
});
5486+
5487+
module('my');
5488+
inject(function($compile, $rootScope, meDirective) {
5489+
element = $compile('<parent><me sibling></me></parent>')($rootScope);
5490+
expect(meController.$onInit).toHaveBeenCalled();
5491+
expect(parentController).toEqual(jasmine.any(ParentController));
5492+
expect(siblingController).toEqual(jasmine.any(SiblingController));
5493+
});
5494+
});
5495+
5496+
5497+
it('should bind required controllers to controllers that return an explicit constructor return value', function() {
5498+
var parentController, containerController, siblingController, friendController, meController;
5499+
5500+
function MeController() {
5501+
this.name = 'Me';
5502+
this.$onInit = function() {
5503+
containerController = this.container;
5504+
friendController = this.friend;
5505+
};
5506+
}
5507+
function ParentController() {
5508+
return parentController = { name: 'Parent' };
5509+
}
5510+
function SiblingController() {
5511+
return siblingController = { name: 'Sibling' };
5512+
}
5513+
5514+
angular.module('my', [])
5515+
.directive('me', function() {
5516+
return {
5517+
priority: 1, // make sure it is run before sibling to test this case correctly
5518+
restrict: 'E',
5519+
scope: {},
5520+
require: { container: '^parent', friend: 'sibling' },
5521+
bindToController: true,
5522+
controller: MeController,
5523+
controllerAs: '$ctrl'
5524+
};
5525+
})
5526+
.directive('parent', function() {
5527+
return {
5528+
restrict: 'E',
5529+
scope: {},
5530+
controller: ParentController
5531+
};
5532+
})
5533+
.directive('sibling', function() {
5534+
return {
5535+
controller: SiblingController
5536+
};
5537+
});
5538+
5539+
module('my');
5540+
inject(function($compile, $rootScope, meDirective) {
5541+
element = $compile('<parent><me sibling></me></parent>')($rootScope);
5542+
expect(containerController).toEqual(parentController);
5543+
expect(friendController).toEqual(siblingController);
5544+
});
5545+
});
53575546

53585547
it('should require controller of an isolate directive from a non-isolate directive on the ' +
53595548
'same element', function() {
@@ -5728,7 +5917,7 @@ describe('$compile', function() {
57285917
return {
57295918
require: { myC1: '^c1', myC2: '^c2' },
57305919
link: function(scope, element, attrs, controllers) {
5731-
log('dep:' + controllers.myC1.name + '-' + controller.myC2.name);
5920+
log('dep:' + controllers.myC1.name + '-' + controllers.myC2.name);
57325921
}
57335922
};
57345923
});

0 commit comments

Comments
 (0)