Search code examples
javascriptangularjsangularjs-directiveangularjs-scopedom-events

Getting a list of all events that can potentially pass through an angular scope. $$listenerCount?


Inside a directive I am trying to get a list of all event names that can potentially be captured by the given scope.

When observing the scope object, I can see that there is a $$listeners property, which contains a single function, and a $$listenersCount property, which does in fact seem to contain a list of events which I have defined that are relevant to the given scope.

$$listenerCount properties

I am listening to most of these events on child scopes of the one that is displayed, so I'm assuming this is a list off all events "passing through" the given scope, not events which the specific scope is listening to. I'm unsure what the numbers mean, though.

I can't find any documentation on these properties so I am assuming they are an internal thing that shouldn't be used for this specific purpose.

Are there any other ways of retrieving a list such as this or do you think it's safe to use despite the lack of documentation?


Solution

  • Let's first consider using $$listeners. Like @Blackhole quoted in his comment:

    To prevent accidental name collisions with your code, Angular prefixes names of public objects with $ and names of private objects with $$. Please do not use the $ or $$ prefix in your code.

    -Angular Api

    So $$listeners is private to angular. It has no documentation and can introduce a breaking change at any moment without notice. Also it's not exactly what you want. By looking at the code of $scope.$on we can see that, like you guessed, $$listenerCount bubbles up the scope to its $parent, all the way to root. The numbers are a count of how many listeners are listening to that one event. You might be able to get away with using $$listeners if you were developing some internal tool to debug events, but using in a production site would not be the wisest thing.


    So what would be a documented way to achieve this? Angular provides a decorator method on its provider. While the documentation on this is little, it is quite a powerful tool. It essentially allows interception of any part of angular, and act as a man in the middle (see more info on the Decorator pattern and Monkey patching on Wikipedia). Using these tools we can create a configuration that will capture each instance of $on being called:

    .config(function ($provide) {
        function wrap(oldFn, wrapFn) {
            return function () {
                return wrapFn.bind(this, oldFn)
                             .apply(this, arguments);
            }
        }
    
        $provide.decorator('$rootScope', function ($delegate) {
            var proto = Object.getPrototypeOf($delegate);
    
            proto.$on = wrap(proto.$on, function ($on, name, listener) {
                var deregister = $on.call(this, name, listener);
                console.log(this, name, listener, deregister);
                return deregister;
            });
    
            return $delegate;
        });
    });
    

    Here I have logged each time $on is called. The $scope is the this variable, name and listener are the arguments passed to $on, and deregister is the return value. Note that his requires ES5's Object.getPrototypeOf, and will add overhead each time $on is called.

    From here, getting the potential event listeners is easy. Instead of console.log, you could place them into a map, or hook into specific ones. You could also wrap the listener or delistener and do additional work every time they get called. Here is an example plunker of that.

    This is better than using $$listeners, because $$listeners can change in any way, at any time. $on on the other hand, is a published API. It will be far more stable, and it will not change without a "Breaking Change" notification in angular's changelog and is the safer choice, even though it uses more anomalous tools.