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

Commit

Permalink
feat($compile): allow required controllers to be bound to the directi…
Browse files Browse the repository at this point in the history
…ve 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
  • Loading branch information
petebacondarwin committed Jan 19, 2016
1 parent cd21216 commit 56c3666
Show file tree
Hide file tree
Showing 2 changed files with 281 additions and 8 deletions.
98 changes: 91 additions & 7 deletions src/ng/compile.js
Expand Up @@ -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
Expand All @@ -279,8 +280,14 @@
* 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:
* 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.
Expand Down Expand Up @@ -1032,6 +1039,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...
*
* <example module="docsTabsExample">
* <file name="script.js">
* 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'
* });
* </file>
* <file name="index.html">
* <my-tabs>
* <my-pane title="Hello">
* <h4>Hello</h4>
* <p>Lorem ipsum dolor sit amet</p>
* </my-pane>
* <my-pane title="World">
* <h4>World</h4>
* <em>Mauris elementum elementum enim at suscipit.</em>
* <p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p>
* </my-pane>
* </my-tabs>
* </file>
* <file name="my-tabs.html">
* <div class="tabbable">
* <ul class="nav nav-tabs">
* <li ng-repeat="pane in $ctrl.panes" ng-class="{active:pane.selected}">
* <a href="" ng-click="$ctrl.select(pane)">{{pane.title}}</a>
* </li>
* </ul>
* <div class="tab-content" ng-transclude></div>
* </div>
* </file>
* <file name="my-pane.html">
* <div class="tab-pane" ng-show="$ctrl.selected" ng-transclude></div>
* </file>
* </example>
*
*
* <br />
* Components are also useful as route templates (e.g. when using
* {@link ngRoute ngRoute}):
Expand Down Expand Up @@ -2425,12 +2506,15 @@ 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);
// 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) {
Expand Down
191 changes: 190 additions & 1 deletion test/ng/compileSpec.js
Expand Up @@ -5354,6 +5354,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('<parent><me sibling></me></parent>')($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('<parent><me sibling></me></parent>')($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('<parent><me sibling></me></parent>')($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('<parent><me sibling></me></parent>')($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() {
Expand Down Expand Up @@ -5728,7 +5917,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);
}
};
});
Expand Down

0 comments on commit 56c3666

Please sign in to comment.