← all articles

How to work with Event Emitters in AngularJS

Simon Holmes | 27 January 2016 | AngularJS

In a large application you're likely to have many controllers. Sometimes, you need the controllers to talk to each other, to share information.

This is where custom events come in. The three key functions are:

  1. $broadcast - trickle down the controller hierarchy
  2. $emit - bubble up the controller hierarch
  3. $on - listen for an event

As you can see, hierarchy of controllers is important. Actually it is the hierarchy of $scope's that is important.

Just like in vanilla JavaScript there is a direct relationship between parent and child scopes, but not between two scopes at the same level.

Controller and $scope hierarchy

A good way to visualise nested controllers and scopes is to set out a hierarchy of controllers in HTML. In the example below, the Son and Daughter controllers are children of the Parent controller.

<body ng-app="family">

  <div ng-controller="Parent as darth">
    {{ darth.output }}
    <div ng-controller="Son as luke">
      {{ luke.output }}
    </div>
    <div ng-controller="Daughter as leia">
      {{ leia.output }}
    </div>
  </div>

</body>

Using broadcast to send events down the chain

The code for this requires three functions - one for each controller.

The parent controller will broadcast an event, and the children will listen for it. Let's walk through the code.

function Parent($scope) {
  var vm = this;  
  vm.output = "I am Darth Vader.";

  // Broadcast an event called 'parent' to the children
  // setTimeout is used here to mimic an asynchronous event
  setTimeout(function() {
    $scope.$broadcast('parent', 'I am your father');
  }, 1000);
}

function Son($scope) {
  var vm = this;  
  vm.output = "I am Luke Skywalker.";

  // Listen for the parent event on $scope
  $scope.$on('parent', function(event, data) {
    console.log('Really?'); // outputs "Really?"
  });
}

function Daughter($scope) {
  var vm = this;  
  vm.output = "I am Princess Leia.";

  // Listen for the parent event on $scope
  $scope.$on('parent', function(event, data) {
    console.log('Shhhh!'); // outputs "Shhh!"
  });
}

Note that even though we're using the controllerAs syntax and declaring a vm variable for the view model, these do not replace the $scope. Events have to happen on the $scope object.

Here's a working example. When you run it you'll see in the console/output:

So the children have both listened for - and heard - the event broadcast from the parent.

Using emit to send events up the chain

The child can talk to the parent in a similar way, using $emit instead of $broadcast. Updating the three controllers so that the Son controller emits an event and Parent & Daughter both listen looks like this:

function Parent($scope) {
  var vm = this;
  vm.output = "I am Darth Vader.";

  // Listen for an event called child
  $scope.$on('child', function(event, data) {
    console.log('Sadface'); // outputs Sadface
  });
}

function Son($scope) {
  var vm = this;
  vm.output = "I am Luke Skywalker.";

  // Emit an event called child to the parent
  setTimeout(function() {
    $scope.$emit('child', 'I\'ll never join you');
  }, 1000);

}

function Daughter($scope) {
  var vm = this;
  vm.output = "I am Princess Leia.";

  // Listen for an event called child
  $scope.$on('child', function(event, data) {
    console.log('Good choice'); // Doesn't output
  });

}

The parent listens in the same way using $on. The important thing to notice here is that the other child cannot hear this event - it is not emitted in their scope chain.

See this example working in JSBin. When you run it you should see this output:

We don't see the console output from the Daughter controller, as it cannot listen to events directly from a fellow child.

Children communicate through the parent

As we've seen, children cannot directly communicate through events, as their scope's are not linked. However, children do have access to their parent's scope, so it is possible to hijack and broadcast an event from there.

This is done by calling $scope.$parent.$broadcast. Any of the siblings - and the parent, can listen to this event.

Let's check it out in code.

function Parent($scope) {
  var vm = this;
  vm.output = "I am Darth Vader.";

  // Listen for an event called child
  $scope.$on('child', function(event, data) {
    console.log('I heard that!'); // outputs I heard that!
  });
}

function Son($scope) {
  var vm = this;
  vm.output = "I am Luke Skywalker.";

  // Listen for an event called child
  $scope.$on('child', function(event, data) {
    console.log('I\'ll say'); // Outputs I'll say
  });

}

function Daughter($scope) {
  var vm = this;
  vm.output = "I am Princess Leia.";

  // Emit an event called child to the parent
  setTimeout(function() {
    $scope.$parent.$broadcast('child', 'Darth Vader has a foul stench');
  }, 1000);

}

See how Daughter calls $broadcast on $scope.$parent, thus publishing the event from the parent.

Running this gives this output:

Note that both the sibling and the parent successfully listen to the event. This means that the siblings are able to talk to each other, but the parent overhears everything going on.

See this example working in JSBin.

Publishing events to all controllers using $rootScope

What we've seen so far is good for nested controllers, but what if there are additional controllers at the top level? A sibling to the parent if you like.

Let's add an Uncle controller to the HTML.

<div ng-controller="Parent as darth">
  {{ darth.output }}
  <div ng-controller="Son as luke">
    {{ luke.output }}
  </div>
  <div ng-controller="Daughter as leia">
    {{ leia.output }}
  </div>
</div>
<div ng-controller="Uncle as obiwan">
  {{ obiwan.output }}
</div>

Keeping the event published by the Daughter controller from the previous example, if the Uncle controller tries to subscribe to it, nothing will happen. That event is outside of the Uncle's scope chain, so it can't hear it.

Likewise, any events the Uncle controller broadcasts on it's $scope will not be picked up by the other controllers. They share no common scope.

Except for the $rootScope. The $rootScope is essentially the global scope. All controller scopes are nested inside it. So if you pass the $rootScope service into the controller you can use it's $broadcast method.

Let's walk through the code again.

function Parent($scope) {
  var vm = this;
  vm.output = "I am Darth Vader.";

  // Listen for an event called child
  $scope.$on('child', function(event, data) {
    console.log('I heard that!'); // outputs I heard that!
  });

  // Listen for an event called uncle
  $scope.$on('uncle', function(event, data) {
    console.log('You\'re no uncle'); // outputs You're no uncle
  });
}

function Son($scope) {
  var vm = this;
  vm.output = "I am Luke Skywalker.";

  // Listen for an event called child
  $scope.$on('child', function(event, data) {
    console.log('I\'ll say'); // Outputs I'll say
  });

  // Listen for an event called uncle
  $scope.$on('uncle', function(event, data) {
    console.log('I\'m running'); // outputs I'm running
  });

}

function Daughter($scope) {
  var vm = this;
  vm.output = "I am Princess Leia.";

  // Emit an event called child to the parent
  setTimeout(function() {
    $scope.$parent.$broadcast('child', 'Darth Vader has a foul stench');
  }, 1000);

  // Listen for an event called uncle
  $scope.$on('uncle', function(event, data) {
    console.log('I\'m not Luke'); // outputs I'm not Luke
  });

}

// Pass $rootScope in to the controller
function Uncle($scope, $rootScope) {
  var vm = this;
  vm.output = "I am Obi-Wan Kenobi.";

  // Listen for an event called child
  $scope.$on('child', function(event, data) {
    console.log('What was that?'); // No output
  });

  // Broadcast and event called uncle on the rootScope
  $rootScope.$broadcast('uncle', 'Run Luke, run');

}

Broadcasting on the $rootScope is about as public as it gets inside your application. It is at the global level and any controller can subscribe to the event.

Here's a screenshot of the output:

See this example working in JSBin.

A couple of points of interest. As Uncle is a top level controller, it's parent scope is $rootScope. So in this instance the following two lines acheive the same thing.

$rootScope.$broadcast('uncle', 'Run Luke, run');
$scope.$parent.$broadcast('uncle', 'Run Luke, run');

However, if Daughter had broadcast on $rootScope instead of $scope.$parent then Uncle would have been able to subscribe to the event. This is because the direct parent of Daughter is not the root, it is Parent. So in this case $rootScope and $scope.$parent would not acheive the same thing.

The best practice here is to be as explicit as possible. If you want to publish an event from the $rootScope then use the $rootScope. It adds clarity to your code.

Use emit or broadcast on the rootScope?

In the previous example we used $rootScope.$broadcast. This makes sense. as $rootScope is at the top of the chain and broadcast trickles down.

This trickling down is why all of the controllers could listen to the event on $scope - the even trickles down to all scopes under the root.

Now if we change the command to $rootScope.$emit where would the event go? It's got nowhere to go upwards, and it's not going down becase as we know $emit bubbles up.

The answer is: it stays in $rootScope. So let's update the $rootScope publish in Uncle to be $emit, and update the Son controller to listen on $rootScope instead of $scope.

function Son($scope, $rootScope) {
  var vm = this;
  vm.output = "I am Luke Skywalker.";

  // Listen for an event called child
  $rootScope.$on('uncle', function(event, data) {
    console.log('I\'m running'); // outputs I'm running
  });

}

function Daughter($scope) {
  var vm = this;
  vm.output = "I am Princess Leia.";

  // Listen for an event called child
  $scope.$on('uncle', function(event, data) {
    console.log('I\'m not Luke'); // no output
  });

}

function Uncle($scope, $rootScope) {
  var vm = this;
  vm.output = "I am Obi-Wan Kenobi.";

  $rootScope.$emit('uncle', 'Run Luke, run');
}

Now, because the event is on $rootScope only, any controllers listening for the uncle event on $scope no longer hear the event, because it is not being broadcast down the chain.

The output here is this:

See this example working on JSBin.

Tidying up after $rootScope.$on

Event listeners on $scope are very good at tidying up after themselves. When the current scope is destroyed the listeners unbind.

This is not the case with listeners on $rootScope, as it never gets destroyed. So you should manually unbind any listeners on the $rootScope if there's a chance that the scope will be destroyed.

To do this:

  1. Assign the listener to a variable
  2. Listen for the $destroy event on the $scope, and then call the listener variable.

This is slightly odd, but calling the listener variable will remove the listener. The code looks like this:

function Son($scope, $rootScope) {
  var vm = this;
  vm.output = "I am Luke Skywalker.";

  // Assign the rootScope listener to a variable
  var uncleListener = $rootScope.$on('uncle', function(event, data) {
    console.log('I\'m running'); // outputs I'm running
  });

  // Unbind the listener when the controller scope is destroyed
  $scope.$on('$destroy', uncleListener);

}

This is particularly useful for single page applications where the active controller could change quite frequently. The $rootScope shouldn't be clogged up with redundant event listeners.

As we said at the beginning - hierarchy of controllers is very important. When working with events it's equally important to know how to navigate through this hierarchy of scopes.

Free email mini-course on
Full Stack development

Sign up now to receive a free email mini-course covering the MEAN stack and more. Also be the first to know when we release new courses and videos.