-
Notifications
You must be signed in to change notification settings - Fork 27.4k
$onInit
and friends
#13763
$onInit
and friends
#13763
Changes from 10 commits
50b0c1a
cf4e7be
452dd39
f72ecbd
b2c0b05
37793ce
a12ddc6
59feecc
8040bab
9e6db1a
1d18df2
4b304a6
cae40ca
276b9ee
6d66a75
32b7da3
cb495a5
270e230
281d987
9f76a11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
* | ||
* <div class="alert alert-warning"> | ||
* **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. | ||
* </div> | ||
* | ||
* 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,26 @@ | |
* 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we mention that this is basically a new phase that happens before both pre and postLink? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Merge pull request #11 from 05bca054/rename-to-ats … |
||
* * `$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 | ||
* 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 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should note that the controllers are only available once $onInit has been called, no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated |
||
* | ||
* 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. | ||
|
@@ -1005,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... | ||
* | ||
* <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}): | ||
|
@@ -1072,7 +1170,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { | |
transclude: options.transclude, | ||
scope: {}, | ||
bindToController: options.bindings || {}, | ||
restrict: 'E' | ||
restrict: 'E', | ||
require: options.require | ||
}; | ||
} | ||
|
||
|
@@ -2280,6 +2379,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; | ||
|
@@ -2386,8 +2490,20 @@ 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this mean that controllers whose constructors return a value won't be properly handled ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By the time we get here we have already dealt with constructors who return a value (around lines 2406-2414). I have added a test to prove this: e571399 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't like to add parameters to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant if one of the required controllers returns a value, then we might extend this controller with the wrong value (depends on the order of execution of the directives). (Your test proves that it works if this controller returns a value.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right! I have fixed it and added another test in 1d18df2 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LGTM |
||
} | ||
} | ||
|
||
// 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]; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4244,6 +4244,22 @@ describe('$compile', function() { | |
if (!/chrome/i.test(navigator.userAgent)) return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unrelated to this issue, but can we change this to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On it [sic]! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, it is not so easy @lgalfaso since the latest Safari (9.0.2) is even worse for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it looks like there are at least 2 issues reported on JavascriptCore (Safari JS engine) regarding this I think that some other strategy. Meanwhile, just keep the old code |
||
/*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: '<p>isolate</p>', | ||
|
@@ -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,13 +4283,15 @@ describe('$compile', function() { | |
element = $compile('<div foo-dir dir-data="remoteData" ' + | ||
'dir-str="Hello, {{whom}}!" ' + | ||
'dir-fn="fn()"></div>')($rootScope); | ||
expect(Controller.prototype.$onInit).toHaveBeenCalled(); | ||
element.data('$fooDirController').check(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this redundant now ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep! removed |
||
expect(controllerCalled).toBe(true); | ||
}); | ||
/*jshint +W061 */ | ||
}); | ||
|
||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Too much whitespace ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed |
||
it('should update @-bindings on controller when bindToController and attribute change observed', function() { | ||
module(function($compileProvider) { | ||
$compileProvider.directive('atBinding', valueFn({ | ||
|
@@ -4929,6 +4934,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('<div d1 d2></div>')($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 +5338,97 @@ 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('<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 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('<parent><me sibling></me></parent>')($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() { | ||
|
@@ -5669,6 +5791,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('<div c1 c2><div dep></div></div>')($rootScope); | ||
expect(log).toEqual('dep:c1-c2'); | ||
}); | ||
}); | ||
|
||
it('should instantiate the controller just once when template/templateUrl', function() { | ||
var syncCtrlSpy = jasmine.createSpy('sync controller'), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will bound --> will be? bound
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done