diff --git a/src/ng/compile.js b/src/ng/compile.js index bd32ce93ada9..1ac80256ae80 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -296,6 +296,8 @@ * * `$onInit` - Called on each controller after all the controllers on an element have been constructed and * 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. + * * `$onDestroy` - Called on each controller when the directive has been destroyed. This is a good place to put + * code for tearing down your controller like for example removing event listeners. * * #### `require` * Require another directive and inject its controller as the fourth argument to the linking function. The @@ -2419,11 +2421,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }); - // Trigger the `$onInit` method on all controllers that have one + // Trigger the `$onInit` method on all controllers that have one, + // and trigger `$onDestroy` method if present and when the element emits `$destroy` event forEach(elementControllers, function(controller) { if (isFunction(controller.instance.$onInit)) { controller.instance.$onInit(); } + + $element.on('$destroy', function() { + if (isFunction(controller.instance.$onDestroy)) { + controller.instance.$onDestroy(); + } + }); }); // PRELINKING diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 6ad592f91ef5..9f022061d7db 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -4584,6 +4584,7 @@ describe('$compile', function() { "class Foo {\n" + " constructor($scope) {}\n" + " $onInit() { this.check(); }\n" + + " $onDestroy() {}\n" + " check() {\n" + " expect(this.data).toEqualData({\n" + " 'foo': 'bar',\n" + @@ -4599,6 +4600,7 @@ describe('$compile', function() { " }\n" + "}"); spyOn(Controller.prototype, '$onInit').andCallThrough(); + spyOn(Controller.prototype, '$onDestroy').andCallThrough(); module(function($compileProvider) { $compileProvider.directive('fooDir', valueFn({ @@ -4625,7 +4627,11 @@ describe('$compile', function() { 'dir-str="Hello, {{whom}}!" ' + 'dir-fn="fn()">')($rootScope); expect(Controller.prototype.$onInit).toHaveBeenCalled(); + expect(Controller.prototype.$onDestroy).not.toHaveBeenCalled(); expect(controllerCalled).toBe(true); + element.remove(); + expect(Controller.prototype.$onDestroy).toHaveBeenCalled(); + }); /*jshint +W061 */ }); @@ -5351,6 +5357,78 @@ describe('$compile', function() { }); }); + it('should call `controller.$onDestroy`, if provided when the element is removed', 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.$onDestroy = jasmine.createSpy('$onDestroy').andCallFake(check); + + function Controller2($element) { this.id = 2; this.element = $element; } + Controller2.prototype.$onDestroy = jasmine.createSpy('$onDestroy').andCallFake(check); + + angular.module('my', []) + .directive('d1', valueFn({ controller: Controller1 })) + .directive('d2', valueFn({ controller: Controller2 })); + + module('my'); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + element.remove(); + expect(Controller1.prototype.$onDestroy).toHaveBeenCalledOnce(); + expect(Controller2.prototype.$onDestroy).toHaveBeenCalledOnce(); + }); + }); + + it('should call `controller.$onDestroy`, if provided when the directive is removed using ngIf', function() { + + function check() { + /*jshint validthis:true */ + expect(this.element.controller('d1').id).toEqual(1); + } + + function Controller1($element) { this.id = 1; this.element = $element; } + Controller1.prototype.$onDestroy = jasmine.createSpy('$onDestroy').andCallFake(check); + + angular.module('my', []) + .directive('d1', valueFn({ controller: Controller1 })); + + module('my'); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + $rootScope.t = true; + $rootScope.$apply(); + + $rootScope.t = false; + $rootScope.$apply(); + + expect(Controller1.prototype.$onDestroy).toHaveBeenCalledOnce(); + }); + }); + + it('should call `controller.$onDestroy`, when provided after controller initialization', function() { + + function Controller1() { + this.setDestroy = function() { + Controller1.prototype.$onDestroy = jasmine.createSpy('$onDestroy'); + }; + } + + angular.module('my', []) + .directive('d1', valueFn({ controller: Controller1 })); + + module('my'); + inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + element.controller('d1').setDestroy(); + element.remove(); + expect(Controller1.prototype.$onDestroy).toHaveBeenCalledOnce(); + }); + }); + describe('should not overwrite @-bound property each digest when not present', function() { it('when creating new scope', function() { module(function($compileProvider) { @@ -5741,6 +5819,8 @@ describe('$compile', function() { siblingController = this.friend; }; spyOn(MeController.prototype, '$onInit').andCallThrough(); + MeController.prototype.$onDestroy = function() {}; + spyOn(MeController.prototype, '$onDestroy').andCallThrough(); angular.module('my', []) .directive('me', function() { @@ -5770,8 +5850,12 @@ describe('$compile', function() { inject(function($compile, $rootScope, meDirective) { element = $compile('')($rootScope); expect(MeController.prototype.$onInit).toHaveBeenCalled(); + expect(MeController.prototype.$onDestroy).not.toHaveBeenCalled(); expect(parentController).toEqual(jasmine.any(ParentController)); expect(siblingController).toEqual(jasmine.any(SiblingController)); + element.remove(); + expect(MeController.prototype.$onDestroy).toHaveBeenCalled(); + }); }); @@ -5787,6 +5871,8 @@ describe('$compile', function() { siblingController = this.friend; }; spyOn(MeController.prototype, '$onInit').andCallThrough(); + MeController.prototype.$onDestroy = function() {}; + spyOn(MeController.prototype, '$onDestroy').andCallThrough(); angular.module('my', []) .directive('me', function() { @@ -5814,8 +5900,12 @@ describe('$compile', function() { inject(function($compile, $rootScope, meDirective) { element = $compile('')($rootScope); expect(MeController.prototype.$onInit).toHaveBeenCalled(); + expect(MeController.prototype.$onDestroy).not.toHaveBeenCalled(); expect(parentController).toBeUndefined(); expect(siblingController).toBeUndefined(); + element.remove(); + expect(MeController.prototype.$onDestroy).toHaveBeenCalled(); + }); }); @@ -5830,9 +5920,12 @@ describe('$compile', function() { $onInit: function() { parentController = this.container; siblingController = this.friend; - } + }, + $onDestroy: function() {} }; spyOn(meController, '$onInit').andCallThrough(); + spyOn(meController, '$onDestroy').andCallThrough(); + return meController; } @@ -5864,8 +5957,11 @@ describe('$compile', function() { inject(function($compile, $rootScope, meDirective) { element = $compile('')($rootScope); expect(meController.$onInit).toHaveBeenCalled(); + expect(meController.$onDestroy).not.toHaveBeenCalled(); expect(parentController).toEqual(jasmine.any(ParentController)); expect(siblingController).toEqual(jasmine.any(SiblingController)); + element.remove(); + expect(meController.$onDestroy).toHaveBeenCalled(); }); });