One of the features I miss the most in AngularJS is ability to easy unsubscribe event handlers. There is no convenience function opposed to $on, so in order to unsubscribe event, we have to call method returned by $on function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
(function () { angular.module('app.download', []) .controller('downloadCtrl', downloadController); function downloadController($scope) { // keep the unsubscribe function in local variable var afterRenderUnsubscribe = $scope.$on('afterRender', onAfterRender); function onAfterRender() { console.log('CallingonAfterRender'); // unsubscribe form event afterRenderUnsubscribe(); } } }()); |
However in my opinion this approach fails in real life scenarios, because usually we are subscribed to more than one event – thus we have to have multiple local variables for detaching listeners. Fortunately AngularJS provides an easy way to extend existing services with some new functionalities – we can achieve that by using $provide service. This service exposes decorator method, which is able to intercept creation of any service, so we are able to augment it with some new functions. The best moment to call $provide.decorator method is during module configuration, thanks to this approach we are sure that our custom logic is invoked before module is started.
1 2 3 4 5 6 7 8 9 |
angular.module('app.common', []) .config(extendServices); /*ngInject*/ function extendServices($provide) { $provide.decorator('serviceName', function ($delegate) { /*some interception logic*/ }); } |
In my case I wanted to extend $rootScope with new method $un for unsubscribing events, so I called method decorator with parameter ‘$rootScope’ (note single quotation marks) – this tells Angular that I will be modifying $rootScope service
1 2 3 4 5 6 7 8 9 10 11 |
angular.module('app.common', []) .config(extendServices); /*ngInject*/ function extendServices($provide) { $provide.decorator('$rootScope', extendRootScope); } /*ngInject*/ function extendRootScope($delegate) { } |
Because I passed ‘$rootScope’ as a first argument of $provide.decorator method, the $delegate parameter in extendRootScope function is actually a $rootScope service. Now having access to the service I can augment it with my custom logic for detaching event handlers.
1 2 3 4 5 6 7 8 9 10 11 |
/*ngInject*/ function extendRootScope($delegate){ $delegate.$un=function(name,listener){ var namedListeners=this.$$listeners[name], idx; if(namedListeners&&(idx=namedListeners.indexOf(listener))>-1){ namedListeners.splice(idx,1); } }; return $delegate; } |
Entire code listing looks then as follows
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
(function () { angular.module('app.common', []) .config(extendServices); /*ngInject*/ function extendServices($provide) { $provide.decorator('$rootScope', extendRootScope); } /*ngInject*/ function extendRootScope($delegate) { $delegate.$un = function (name, listener) { var namedListeners = this.$$listeners[name], idx; if (namedListeners && (idx = namedListeners.indexOf(listener)) > -1) { namedListeners.splice(idx, 1); } }; return $delegate; } }()); |
From now on, you can unsubscribe events handlers simply by calling $un method
1 2 3 4 5 6 7 8 9 10 11 12 13 |
(function () { angular.module('app.download', ['app.common']) .controller('downloadCtrl', downloadController); function downloadController($scope) { $scope.$on('afterRender', onAfterRender); function onAfterRender() { console.log('Calling afterRender handler'); $scope.$un('afterRender', onAfterRender); } } }()); |
and our listener will react only once
1 2 3 4 |
console.log('Broadcasting afterRender event for the first time'); $rootScope.$broadcast('afterRender'); console.log('Broadcasting afterRender event for the second time'); $rootScope.$broadcast('afterRender'); |
Source code for this post can be found here