I'm Todd, I teach the world Angular through @UltimateAngular. Conference speaker and Developer Expert at Google.

Understanding Angular’s $scope and $rootScope event system $emit, $broadcast and $on

Angular’s $emit, $broadcast and $on fall under the common “publish/subscribe” design pattern, or can do, in which you’d publish an event and subscribe/unsubscribe to it somewhere else. The Angular event system is brilliant, it makes things flawless and easy to do (as you’d expect!) but the concept behind it isn’t so simple to master and you can often be left wondering why things don’t work as you thought they might.

For those who are new to Angular and haven’t used or seen $emit, $broadcast or $on, let’s clarify what they do before we look at $scope and $rootScope event and scope relationships and how to utilise the event system correctly - as well as understand what’s really going on.

$scope.$emit up, $scope.$broadcast down

Using $scope.$emit will fire an event up the $scope. Using $scope.$broadcast will fire an event down the $scope. Using $scope.$on is how we listen for these events. A quick example:

// firing an event upwards
$scope.$emit('myCustomEvent', 'Data to send');

// firing an event downwards
$scope.$broadcast('myCustomEvent', {
  someProp: 'Sending you an Object!' // send whatever you want
});

// listen for the event in the relevant $scope
$scope.$on('myCustomEvent', function (event, data) {
  console.log(data); // 'Data to send'
});

$scope.($emit/$broadcast)

The key thing to remember when using $scope to fire your events, is that they will communicate only with immediate parent or child scopes only! Scopes aren’t always child and parent. We might have sibling scopes. Using $scope to fire an event will miss out sibling scopes, and just carry on up! They do not go sideways!

The simplest way to emulate parent and child scopes are to use Controllers. Each Controller creates new $scope, which Angular neatly outputs an ng-scope class on newly scoped elements for us:

<div ng-controller="ParentCtrl as parent" class="ng-scope">
  {{ parent.data }}
  <div ng-controller="SiblingOneCtrl as sib1" class="ng-scope">
      {{ sib1.data }}
  </div>
</div>

We could fire an event down from ParentCtrl to SiblingOneCtrl using $broadcast:

app.controller('ParentCtrl',
  function ParentCtrl ($scope) {

  $scope.$broadcast('parent', 'Some data'); // going down!

});

app.controller('SiblingOneCtrl',
  function SiblingOneCtrl ($scope) {

  $scope.$on('parent', function (event, data) {
    console.log(data); // 'Some data'
  });

});

If we wanted to communicate upwards, from SiblingOneCtrl to ParentCtrl, you guessed it, we can use $emit.

app.controller('ParentCtrl',
  function ParentCtrl ($scope) {

  $scope.$on('child', function (event, data) {
    console.log(data); // 'Some data'
  });

});

app.controller('SiblingOneCtrl',
  function SiblingOneCtrl ($scope) {

  $scope.$emit('child', 'Some data'); // going up!

});

To demonstrate how $scope works when firing the events, here’s a simple hierarchy:

<div ng-controller="ParentCtrl as parent" class="ng-scope">
  <div ng-controller="SiblingOneCtrl as sib1" class="ng-scope"></div>
  <div ng-controller="SiblingTwoCtrl as sib2" class="ng-scope"></div>
</div>

If SiblingTwoCtrl fired $scope.$broadcast, then SiblingOneCtrl would never know it happened. This can be an annoyance, but a (slightly hacky-feely) remedy can be done:

$scope.$parent.$broadcast('myevent', 'Some data');

What this does is jump up to ParentCtrl and then fire the $broadcast from there.

$rootScope.($emit/$broadcast)

If things weren’t complicated enough, let’s throw in $rootScope as well. $rootScope is the parent of all scopes, which makes every newly created $scope a descendent! I mentioned above about how $scope is limited to direct scopes, $rootScope is how we could communicate across scopes with ease. Doing this will fit certain scenarios better than others. It’s not as simple as up or down the scopes though, unfortunately…

$rootScope.$emit versus $rootScope.$broadcast

The $rootScope Object has the identical $emit, $broadcast, $on methods, but they work slightly differently to how $scope implements them. As $rootScope has no $parent, using an $emit would be pointless, right? Nope, instead, $rootScope.$emit will fire an event for all $rootScope.$on listeners only. The interesting part is that $rootScope.$broadcast will notify all $rootScope.$on as well as $scope.$on listeners, subtle but very important difference if you want to avoid issues in your application.

$rootScope examples

Let’s take an even deeper hierarchy:

<div ng-controller="ParentCtrl as parent" class="ng-scope">
  // ParentCtrl
  <div ng-controller="SiblingOneCtrl as sib1" class="ng-scope">
    // SiblingOneCtrl
  </div>
  <div ng-controller="SiblingTwoCtrl as sib2" class="ng-scope">
    // SiblingTwoCtrl
    <div ng-controller="ChildCtrl as child" class="ng-scope">
      // ChildCtrl
    </div>
  </div>
</div>

The above has 3 lexical scopes (where parent scopes are accessible in the current scope, kind of hurts your brain to think about it in terms of DOM scoping, but the concepts are there) and 4 Angular scopes, ParentCtrl, SiblingOneCtrl, SiblingTwoCtrl and ChildCtrl. Two sibling scopes.

Using $scope.$emit inside ChildCtrl would result in SiblingTwoCtrl and ParentCtrl only being notified, as the event doesn’t hit sibling scopes only direct ancestors (completely ignoring SiblingOneCtrl). If we used $rootScope, however, then we can target $rootScope listeners as well.

app.controller('SiblingOneCtrl',
  function SiblingOneCtrl ($rootScope) {

  $rootScope.$on('rootScope:emit', function (event, data) {
    console.log(data); // 'Emit!'
  });
  
  $scope.$on('rootScope:broadcast', function (event, data) {
    console.log(data); // 'Broadcast!'
  });
  
  $rootScope.$on('rootScope:broadcast', function (event, data) {
    console.log(data); // 'Broadcast!'
  });

});

app.controller('ChildCtrl',
  function ChildCtrl ($rootScope) {

  $rootScope.$emit('rootScope:emit', 'Emit!'); // $rootScope.$on
  $rootScope.$broadcast('rootScope:broadcast', 'Broadcast'); // $rootScope.$on && $scope.$on

});

Unsubscribing from events

As part of the event system, you can unsubscribe from events at any time with the $on listener. Unlike other libraries, there is no $off method. The Angular docs aren’t particularly clear on how to “unsubscribe”, the docs say that $on “Returns a deregistration function for this listener.”. We can assume by that they mean a closure which allows us to unsubscribe.

Inside the source code of v1.3.0-beta.11, we can locate the $on method and confirm suspicions of a closure:

$on: function(name, listener) {
  var namedListeners = this.$$listeners[name];
  if (!namedListeners) {
    this.$$listeners[name] = namedListeners = [];
  }
  namedListeners.push(listener);

  var current = this;
  do {
    if (!current.$$listenerCount[name]) {
      current.$$listenerCount[name] = 0;
    }
    current.$$listenerCount[name]++;
  } while ((current = current.$parent));

  var self = this;
  return function() {
    namedListeners[indexOf(namedListeners, listener)] = null;
    decrementListenerCount(self, 1, name);
  };
}

We can subscribe and unsubscribe very easily:

app.controller('ParentCtrl',
  function ParentCtrl ($scope) {

  // subscribes...
  var myListener = $scope.$on('child', function (event, data) {
    // do something
  });

  // unsubscribes...
  // this would probably sit in a callback or something
  myListener();

});

$rootScope $destroy

When using $rootScope.$on, we need to unbind those listeners each time the $scope is destroyed. $scope.$on listeners are automatically unbound, but we’ll need to call the above closure manually on the $destroy event:

app.controller('ParentCtrl',
  function ParentCtrl ($scope) {

  // $rootScope $on
  var myListener = $rootScope.$on('child', function (event, data) {
    //
  });

  // $scope $destroy
  $scope.$on('$destroy', myListener);

});

Cancelling events

If you choose to use $emit, one of your other $scope listeners can cancel it, so prevent it bubbling further. Using $broadcast has the opposite effect in which it cannot be cancelled!

Cancelling an event which was sent via $emit looks like this:

$scope.$on('myCustomEvent', function (event, data) {
  event.stopPropagation();
});

$rootScope.$$listeners

Every Angular Object has several properties, we can dig into them and observe what’s happening “under the hood”. We can take a look at $rootScope.$$listeners to observe the listeners lifecycle. We can also unsubscribe from events that way as well by using this (but I wouldn’t encourage it):

$rootScope.$$listeners.myEventName = [];

Event namespacing

Generally if I’m working on a particular Factory, I’ll communicate to other Directives or Controllers or even Factories using a specific namespace for cleaner pub/subs, which keeps things consistent and avoid naming conflicts.

If I were building an email application with an Inbox, we might use an inbox namespace for that specific section. This is easily integrated with a few simple examples:

$scope.$emit('inbox:send'[, data]);
$scope.$on('inbox:send', function (event, data) {...});

$scope.$broadcast('inbox:delete'[, data]);
$scope.$on('inbox:delete', function (event, data) {...});

$scope.$emit('inbox:save'[, data]);
$scope.$on('inbox:save', function (event, data) {...});

Further reading

Dig through the docs for anything further! :)

Todd Motto

I'm Todd, I teach the world Angular through @UltimateAngular. Conference speaker and Developer Expert at Google.