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('