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

Commit 3ffdf38

Browse files
feat($compile): call $ngOnInit on directive controllers after controller construction
This enables option three of #13510 (comment) 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 #5893 Closes #13763
1 parent db5e0ff commit 3ffdf38

File tree

2 files changed

+69
-18
lines changed

2 files changed

+69
-18
lines changed

src/ng/compile.js

+25-3
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,18 @@
218218
* definition: `controller: 'myCtrl as myAlias'`.
219219
*
220220
* When an isolate scope is used for a directive (see above), `bindToController: true` will
221-
* allow a component to have its properties bound to the controller, rather than to scope. When the controller
222-
* is instantiated, the initial values of the isolate scope bindings will be available if the controller is not an ES6 class.
221+
* allow a component to have its properties bound to the controller, rather than to scope.
222+
*
223+
* After the controller is instantiated, the initial values of the isolate scope bindings will bound to the controller
224+
* properties. You can access these bindings once they have been initialized by providing a controller method called
225+
* `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings
226+
* initialized.
227+
*
228+
* <div class="alert alert-warning">
229+
* **Deprecation warning:** although bindings for non-ES6 class controllers are currently
230+
* bound to `this` before the controller constructor is called, this use is now deprecated. Please place initialization
231+
* code that relies upon bindings inside a `$onInit` method on the controller, instead.
232+
* </div>
223233
*
224234
* It is also possible to set `bindToController` to an object hash with the same format as the `scope` property.
225235
* This will set up the scope bindings to the controller directly. Note that `scope` can still be used
@@ -255,6 +265,10 @@
255265
* The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns
256266
* `true` if the specified slot contains content (i.e. one or more DOM nodes).
257267
*
268+
* The controller can provide the following methods that act as life-cycle hooks:
269+
* * `$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.
271+
*
258272
* #### `require`
259273
* Require another directive and inject its controller as the fourth argument to the linking function. The
260274
* `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the
@@ -1079,7 +1093,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
10791093
transclude: options.transclude,
10801094
scope: {},
10811095
bindToController: options.bindings || {},
1082-
restrict: 'E'
1096+
restrict: 'E',
1097+
require: options.require
10831098
};
10841099
}
10851100

@@ -2401,6 +2416,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
24012416
}
24022417
}
24032418

2419+
// Trigger the `$onInit` method on all controllers that have one
2420+
forEach(elementControllers, function(controller) {
2421+
if (isFunction(controller.instance.$onInit)) {
2422+
controller.instance.$onInit();
2423+
}
2424+
});
2425+
24042426
// PRELINKING
24052427
for (i = 0, ii = preLinkFns.length; i < ii; i++) {
24062428
linkFn = preLinkFns[i];

test/ng/compileSpec.js

+44-15
Original file line numberDiff line numberDiff line change
@@ -4262,6 +4262,22 @@ describe('$compile', function() {
42624262
if (!/chrome/i.test(navigator.userAgent)) return;
42634263
/*jshint -W061 */
42644264
var controllerCalled = false;
4265+
var Controller = eval(
4266+
"class Foo {\n" +
4267+
" constructor($scope) {}\n" +
4268+
" $onInit() { this.check(); }\n" +
4269+
" check() {\n" +
4270+
" expect(this.data).toEqualData({\n" +
4271+
" 'foo': 'bar',\n" +
4272+
" 'baz': 'biz'\n" +
4273+
" });\n" +
4274+
" expect(this.str).toBe('Hello, world!');\n" +
4275+
" expect(this.fn()).toBe('called!');\n" +
4276+
" controllerCalled = true;\n" +
4277+
" }\n" +
4278+
"}");
4279+
spyOn(Controller.prototype, '$onInit').andCallThrough();
4280+
42654281
module(function($compileProvider) {
42664282
$compileProvider.directive('fooDir', valueFn({
42674283
template: '<p>isolate</p>',
@@ -4270,20 +4286,7 @@ describe('$compile', function() {
42704286
'str': '@dirStr',
42714287
'fn': '&dirFn'
42724288
},
4273-
controller: eval(
4274-
"class Foo {" +
4275-
" constructor($scope) {}" +
4276-
" check() {" +
4277-
" expect(this.data).toEqualData({" +
4278-
" 'foo': 'bar'," +
4279-
" 'baz': 'biz'" +
4280-
" });" +
4281-
" expect(this.str).toBe('Hello, world!');" +
4282-
" expect(this.fn()).toBe('called!');" +
4283-
" controllerCalled = true;" +
4284-
" }" +
4285-
"}"
4286-
),
4289+
controller: Controller,
42874290
controllerAs: 'test',
42884291
bindToController: true
42894292
}));
@@ -4298,7 +4301,7 @@ describe('$compile', function() {
42984301
element = $compile('<div foo-dir dir-data="remoteData" ' +
42994302
'dir-str="Hello, {{whom}}!" ' +
43004303
'dir-fn="fn()"></div>')($rootScope);
4301-
element.data('$fooDirController').check();
4304+
expect(Controller.prototype.$onInit).toHaveBeenCalled();
43024305
expect(controllerCalled).toBe(true);
43034306
});
43044307
/*jshint +W061 */
@@ -4947,6 +4950,32 @@ describe('$compile', function() {
49474950
});
49484951
});
49494952

4953+
it('should call `controller.$onInit`, if provided after all the controllers have been constructed', function() {
4954+
4955+
function check() {
4956+
/*jshint validthis:true */
4957+
expect(this.element.controller('d1').id).toEqual(1);
4958+
expect(this.element.controller('d2').id).toEqual(2);
4959+
}
4960+
4961+
function Controller1($element) { this.id = 1; this.element = $element; }
4962+
Controller1.prototype.$onInit = jasmine.createSpy('$onInit').andCallFake(check);
4963+
4964+
function Controller2($element) { this.id = 2; this.element = $element; }
4965+
Controller2.prototype.$onInit = jasmine.createSpy('$onInit').andCallFake(check);
4966+
4967+
angular.module('my', [])
4968+
.directive('d1', valueFn({ controller: Controller1 }))
4969+
.directive('d2', valueFn({ controller: Controller2 }));
4970+
4971+
module('my');
4972+
inject(function($compile, $rootScope) {
4973+
element = $compile('<div d1 d2></div>')($rootScope);
4974+
expect(Controller1.prototype.$onInit).toHaveBeenCalledOnce();
4975+
expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce();
4976+
});
4977+
});
4978+
49504979
describe('should not overwrite @-bound property each digest when not present', function() {
49514980
it('when creating new scope', function() {
49524981
module(function($compileProvider) {

0 commit comments

Comments
 (0)