How to unsubscribe to a broadcast event in angularJS. How to remove function registered via $on
Asked Answered
W

10

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.

Wrasse answered 15/2, 2013 at 15:47 Comment(1)
For anyone wondering, the returned function is documented hereCandlelight
H
493

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.

Hightest answered 15/2, 2013 at 16:16 Comment(11)
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. ThanksWrasse
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?Hightest
Yeah, I've actually deleted my answer because I don't want to confuse people. This is the proper way to do this.Unpolite
@blesh could you post the link to the plnkr please?Hightest
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.Unpolite
@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.Wrasse
@Wrasse 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 deregisteringHightest
@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.Wrasse
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 JavaDrais
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.Thorbert
Plus one for digging into the source code and presenting this beautiful solution.Hartman
K
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();
});
Kopje answered 31/3, 2015 at 20:19 Comment(6)
As simple as these, there's a lot answers but this is more concise.Inherited
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.Prejudicial
best answer but want to see more explanation about why calling that listener inside $destroy kills the listener.Polymerization
@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.Kopje
@Prejudicial 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.Kopje
@Kopje 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.Prejudicial
P
26

This code works for me:

$rootScope.$$listeners.nameOfYourEvent=[];
Provender answered 21/8, 2013 at 10:39 Comment(4)
Looking at $rootScope.$$listeners is also a good way to observe the listener's lifecycle, and to experiment with it.Milka
Looks simple and great. I think its just removed reference of function. isn't it?Folketing
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.Wiretap
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.Duhon
U
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

Unpolite answered 15/2, 2013 at 16:13 Comment(2)
Worked like a charm! but i edited it a little i put it to the .run sectionYand
Love this solution. Makes for a much cleaner solution - so much easier to read. +1Thorbert
W
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.

Wrasse answered 15/2, 2013 at 16:24 Comment(4)
I'm actually deleting my answer, because @LiviuT's answer is 100% correct.Unpolite
@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.Wrasse
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.Unpolite
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.Wrasse
M
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.
Milka answered 8/1, 2014 at 4:16 Comment(0)
B
1

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

$scope.$on('$destroy', function () {
   delete $rootScope.$$listeners["youreventname"];
});  
Boden answered 11/3, 2019 at 21:30 Comment(1)
While not the generally-accepted way to do this, there are times this is the necessary solution.Heart
C
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 = [];
    }
}
Chemisorption answered 11/4, 2019 at 8:6 Comment(0)
S
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

Scofflaw answered 13/6, 2016 at 8:4 Comment(0)
E
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
    })
Extort answered 19/10, 2017 at 11:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.