290

I have registered my listener to a $broadcast event using $on function

$scope.$on("onViewUpdated", this.callMe);

and I want to un-register this listener based on a particular business rule. But my problem is that once it is registered I am not able to un-register it.

Is there any method in AngularJS to un-register a particular listener? A method like $on that un-register this event, may be $off. So that based on the business logic i can say

 $scope.$off("onViewUpdated", this.callMe);

and this function stop being called when somebody broadcast "onViewUpdated" event.

Thanks

EDIT: I want to de-register the listener from another function. Not the function where i register it.

Hitesh.Aneja
  • 3,523
  • 3
  • 18
  • 14
  • 3
    For anyone wondering, the returned function is documented [here](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$on) – Fagner Brack Jul 14 '15 at 12:51

10 Answers10

492

You need to store the returned function and call it to unsubscribe from the event.

var deregisterListener = $scope.$on("onViewUpdated", callMe);
deregisterListener (); // this will deregister that listener

This is found in the source code :) at least in 1.0.4. I'll just post the full code since it's short

/**
  * @param {string} name Event name to listen on.
  * @param {function(event)} listener Function to call when the event is emitted.
  * @returns {function()} Returns a deregistration function for this listener.
  */
$on: function(name, listener) {
    var namedListeners = this.$$listeners[name];
    if (!namedListeners) {
      this.$$listeners[name] = namedListeners = [];
    }
    namedListeners.push(listener);

    return function() {
      namedListeners[indexOf(namedListeners, listener)] = null;
    };
},

Also, see the docs.

fracz
  • 20,536
  • 18
  • 103
  • 149
Liviu T.
  • 23,584
  • 10
  • 62
  • 58
  • Yes. After debugging the sorce code i found out that there is a $$listeners array that has all the events and created my $off function. Thanks – Hitesh.Aneja Feb 15 '13 at 16:27
  • What is the actual use case that you can't use the provided angular way of deregistering? Is the deregistering done in another scope not linked to the scope that created the listener? – Liviu T. Feb 15 '13 at 16:29
  • 1
    Yeah, I've actually deleted my answer because I don't want to confuse people. This is the proper way to do this. – Ben Lesh Feb 15 '13 at 16:31
  • @blesh could you post the link to the plnkr please? – Liviu T. Feb 15 '13 at 16:32
  • http://plnkr.co/edit/ZRt2wVtoJsy6fl5MrWY6 - but again, I wouldn't use my solution. It's an example of how to extend an object whose constructor is hidden by closure, but that's about it. – Ben Lesh Feb 15 '13 at 16:50
  • @Liviu: You are right this is the angular provided way. The only problem i hv is that i am registering and de-registering listeners from different functions of my class/different scope. If i go with this way I have to store reference of every de-register function in a local variable of my class, so that i can access it in different function. For simplicity i submitted code like `$scope.$on("onViewUpdated", callMe);` but actually it is like `$scope.$on("onViewUpdated", this.callMe);`. Having $off method i can de-register the listener from anywhere without any headache of keeping references. – Hitesh.Aneja Feb 15 '13 at 17:00
  • @Hitesh.Aneja I understand completly but I do think there is a valid reason for why the angular team decided to implement it this way. Cross scope actions, I think, are not recommended. You could do a workaround by also registering a offViewUpdated event with the deregister function and trigger that event from whatever scope you are actually doing the deregistering – Liviu T. Feb 15 '13 at 17:03
  • 3
    @Liviu: That will become a headache with growing application. It's not only this event there are lotsof other events as well and not necessarily that i always be de-registering in same scope function. There could be cases when i am calling a function which is registering this listener but de-registering the listener on other call, even i those cases i won't get the reference unless i store them outside my scope. So for my current implementation my implementation looks viable solution to me. But definitely would like to know the reasons why AngularJS did it in this way. – Hitesh.Aneja Feb 15 '13 at 17:11
  • 2
    I think that Angular did it this way because a lot of the time inline anonymous functions are used as arguments to the $on function. In order to call $scope.$off(type, function) we'd need to keep a reference to the anonymous function. It is just thinking in a different way to how one would normally do add/remove event listeners in a language like ActionScript or the Observable pattern in Java – dannrob Mar 07 '14 at 13:31
  • This may be the 'correct' answer, but the much more elegant approach is to have a .$off extension as proposed by Ben below. It doesn't confuse future coders. – Rick Jul 13 '15 at 17:54
  • Plus one for digging into the source code and presenting this beautiful solution. – Hinrich Mar 23 '16 at 08:58
63

Looking at most of the replies, they seem overly complicated. Angular has built in mechanisms to unregister.

Use the deregistration function returned by $on :

// Register and get a handle to the listener
var listener = $scope.$on('someMessage', function () {
    $log.log("Message received");
});

// Unregister
$scope.$on('$destroy', function () {
    $log.log("Unregistering listener");
    listener();
});
Maen
  • 10,603
  • 3
  • 45
  • 71
long2know
  • 1,280
  • 10
  • 9
  • As simple as these, there's a lot answers but this is more concise. – David Aguilar Feb 16 '16 at 18:59
  • 8
    Technically correct, though a bit misleading, because `$scope.$on` doesn't have to be unregistered manually on `$destroy`. A better example would be to use a `$rootScope.$on`. – hgoebl Apr 04 '16 at 13:00
  • 2
    best answer but want to see more explanation about why calling that listener inside $destroy kills the listener. – Mohammad Rafigh Aug 16 '16 at 01:54
  • 1
    @MohammadRafigh Calling the listener inside of $destroy is just where I chose to put it. If I recall correctly, this was code that I had inside of a directive and it made sense that when the directives scope was destroyed, listeners should be unregistered. – long2know May 03 '17 at 12:33
  • @hgoebl I don't know what you mean. If I have, for example, a directive that is used in multiple places, and each is registering a listener for an event, how would using $rootScope.$on help me? The directive's scope disposal seems to be the best place to dispose of its listeners. – long2know May 03 '17 at 12:36
  • @long2know the point is that `$scope.$on` has it's own automatic unregistration, so providing an example with code which is unnecessary is confusing. In your context `$rootScope.$on` doesn't make sense, but there are examples in real world where `$rootScope.$on` is the right choice. – hgoebl May 03 '17 at 16:06
26

This code works for me:

$rootScope.$$listeners.nameOfYourEvent=[];
XML
  • 19,206
  • 9
  • 64
  • 65
  • 1
    Looking at $rootScope.$$listeners is also a good way to observe the listener's lifecycle, and to experiment with it. – XML Jan 08 '14 at 02:22
  • Looks simple and great. I think its just removed reference of function. isn't it? – Jay Shukla Mar 07 '14 at 13:59
  • 27
    This solution is not recommended because the $$listeners member is considered private. In fact, any member of an angular object with the '$$' prefix is private by convention. – shovavnik Nov 12 '14 at 16:46
  • 5
    I wouldnt recommend this option, because it removes all listeners, not just the one you need to remove. It may cause problems in the future when you add another listener in another part of the script. – Rainer Plumer Sep 14 '15 at 02:23
10

EDIT: The correct way to do this is in @LiviuT's answer!

You can always extend Angular's scope to allow you to remove such listeners like so:

//A little hack to add an $off() method to $scopes.
(function () {
  var injector = angular.injector(['ng']),
      rootScope = injector.get('$rootScope');
      rootScope.constructor.prototype.$off = function(eventName, fn) {
        if(this.$$listeners) {
          var eventArr = this.$$listeners[eventName];
          if(eventArr) {
            for(var i = 0; i < eventArr.length; i++) {
              if(eventArr[i] === fn) {
                eventArr.splice(i, 1);
              }
            }
          }
        }
      }
}());

And here's how it would work:

  function myEvent() {
    alert('test');
  }
  $scope.$on('test', myEvent);
  $scope.$broadcast('test');
  $scope.$off('test', myEvent);
  $scope.$broadcast('test');

And here's a plunker of it in action

Ben Lesh
  • 107,825
  • 47
  • 247
  • 232
7

After debugging the code, i created my own function just like "blesh"'s answer. So this is what i did

MyModule = angular.module('FIT', [])
.run(function ($rootScope) {
        // Custom $off function to un-register the listener.
        $rootScope.$off = function (name, listener) {
            var namedListeners = this.$$listeners[name];
            if (namedListeners) {
                // Loop through the array of named listeners and remove them from the array.
                for (var i = 0; i < namedListeners.length; i++) {
                    if (namedListeners[i] === listener) {
                        return namedListeners.splice(i, 1);
                    }
                }
            }
        }
});

so by attaching my function to $rootscope now it is available to all my controllers.

and in my code I am doing

$scope.$off("onViewUpdated", callMe);

Thanks

EDIT: The AngularJS way to do this is in @LiviuT's answer! But if you want to de-register the listener in another scope and at the same time want to stay away from creating local variables to keep references of de-registeration function. This is a possible solution.

Hitesh.Aneja
  • 3,523
  • 3
  • 18
  • 14
  • 1
    I'm actually deleting my answer, because @LiviuT's answer is 100% correct. – Ben Lesh Feb 15 '13 at 16:30
  • @blesh LiviuT's answer is correct and acually an angualar provided approach to de-register but does not hook-up well for the scenarios where you have to de-register the listener in different scope. So this is an easy alternative. – Hitesh.Aneja Feb 15 '13 at 17:53
  • 1
    It provides the same hook up any other solution would. You'd just put the variable containing the destruction function in an exterior closure or even in a global collection... or anywhere you want. – Ben Lesh Feb 15 '13 at 17:57
  • I don't want to keep creating global variables to keep references of the de-registeration functions and also i don't see any issues with using my own $off function. – Hitesh.Aneja Feb 17 '13 at 02:30
1

@LiviuT's answer is awesome, but seems to leave lots of folks wondering how to re-access the handler's tear-down function from another $scope or function, if you want to destroy it from a place other than where it was created. @Рустем Мусабеков's answer works just great, but isn't very idiomatic. (And relies on what's supposed to be a private implementation detail, which could change any time.) And from there, it just gets more complicated...

I think the easy answer here is to simply carry a reference to the tear-down function (offCallMeFn in his example) in the handler itself, and then call it based on some condition; perhaps an arg that you include on the event you $broadcast or $emit. Handlers can thus tear down themselves, whenever you want, wherever you want, carrying around the seeds of their own destruction. Like so:

// Creation of our handler:
var tearDownFunc = $rootScope.$on('demo-event', function(event, booleanParam) {
    var selfDestruct = tearDownFunc;
    if (booleanParam === false) {
        console.log('This is the routine handler here. I can do your normal handling-type stuff.')
    }
    if (booleanParam === true) {
        console.log("5... 4... 3... 2... 1...")
        selfDestruct();
    }
});

// These two functions are purely for demonstration
window.trigger = function(booleanArg) {
    $scope.$emit('demo-event', booleanArg);
}
window.check = function() {
    // shows us where Angular is stashing our handlers, while they exist
    console.log($rootScope.$$listeners['demo-event'])
};

// Interactive Demo:

>> trigger(false);
// "This is the routine handler here. I can do your normal handling-type stuff."

>> check();
// [function] (So, there's a handler registered at this point.)  

>> trigger(true);
// "5... 4... 3... 2... 1..."

>> check();
// [null] (No more handler.)

>> trigger(false);
// undefined (He's dead, Jim.)

Two thoughts:

  1. This is a great formula for a run-once handler. Just drop the conditionals and run selfDestruct as soon as it has completed its suicide mission.
  2. I wonder about whether the originating scope will ever be properly destroyed and garbage-collected, given that you're carrying references to closured variables. You'd have to use a million of these to even have it be a memory problem, but I'm curious. If anybody has any insight, please share.
XML
  • 19,206
  • 9
  • 64
  • 65
1

Register a hook to unsubscribe your listeners when the component is removed:

$scope.$on('$destroy', function () {
   delete $rootScope.$$listeners["youreventname"];
});  
noetix
  • 4,773
  • 3
  • 26
  • 47
Dima
  • 1,045
  • 14
  • 23
1

In case that you need to turn on and off the listener multiple times, you can create a function with boolean parameter

function switchListen(_switch) {
    if (_switch) {
      $scope.$on("onViewUpdated", this.callMe);
    } else {
      $rootScope.$$listeners.onViewUpdated = [];
    }
}
Rich
  • 3,928
  • 4
  • 37
  • 66
0

'$on' itself returns function for unregister

 var unregister=  $rootScope.$on('$stateChangeStart',
            function(event, toState, toParams, fromState, fromParams, options) { 
                alert('state changing'); 
            });

you can call unregister() function to unregister that listener

Rajiv
  • 1,245
  • 14
  • 28
0

One way is to simply destroy the listener once you are done with it.

var removeListener = $scope.$on('navBarRight-ready', function () {
        $rootScope.$broadcast('workerProfile-display', $scope.worker)
        removeListener(); //destroy the listener
    })
user1502826
  • 395
  • 4
  • 12