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

Commit

Permalink
feat($compile): call $ngOnInit on directive controllers after contr…
Browse files Browse the repository at this point in the history
…oller 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
  • Loading branch information
petebacondarwin committed Jan 19, 2016
1 parent db5e0ff commit 3ffdf38
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 18 deletions.
28 changes: 25 additions & 3 deletions src/ng/compile.js
Expand Up @@ -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
Expand Down Expand Up @@ -255,6 +265,10 @@
* 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. 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
Expand Down Expand Up @@ -1079,7 +1093,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
transclude: options.transclude,
scope: {},
bindToController: options.bindings || {},
restrict: 'E'
restrict: 'E',
require: options.require
};
}

Expand Down Expand Up @@ -2401,6 +2416,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}

// 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];
Expand Down
59 changes: 44 additions & 15 deletions test/ng/compileSpec.js
Expand Up @@ -4262,6 +4262,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: '<p>isolate</p>',
Expand All @@ -4270,20 +4286,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
}));
Expand All @@ -4298,7 +4301,7 @@ describe('$compile', function() {
element = $compile('<div foo-dir dir-data="remoteData" ' +
'dir-str="Hello, {{whom}}!" ' +
'dir-fn="fn()"></div>')($rootScope);
element.data('$fooDirController').check();
expect(Controller.prototype.$onInit).toHaveBeenCalled();
expect(controllerCalled).toBe(true);
});
/*jshint +W061 */
Expand Down Expand Up @@ -4947,6 +4950,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) {
Expand Down

0 comments on commit 3ffdf38

Please sign in to comment.