Broadcasting Events to the $rootScope in AngularJS to pass information between Directives and Controllers

In one of the labs for the Learn Verified curriculum, we’re tasked with using the $rootScope object to publish and subscribe to events. This allows us to pass information about user behavior from one part of our application to another.

For example, in this lab we have a ContactsController that has a property called contacts. This property stores an array contains a list of many contacts.

// app/js/controllers/ContactController.js
angular
.module('app')
.controller('ContactController', function($rootScope){

this.contacts = [
{
"name": {
"title": "mr",
"first": "liam",
"last": "lawson"
},
"location": {
"street": "1642 church lane",
"city": "dundee",
"state": "south yorkshire",
"zip": "LN6Y 1ZG"
},
"email": "liam.lawson@example.com",
"username": "silverwolf439",
"dob": 924314544,
"phone": "015242 99250",
"cell": "0713-889-058",
"NINO": "BX 12 80 44 W",
"picture": {
"large": "https://randomuser.me/api/portraits/men/74.jpg",
"medium": "https://randomuser.me/api/portraits/med/men/74.jpg",
"thumbnail": "https://randomuser.me/api/portraits/thumb/men/74.jpg"
}
},
....
];
});

Within the index.html file: We have an ng-controller scope for the ContactController, we have an ng-repeat directive that is displaying each contact using a custom directive called contact.

<!-- index.html -->
<div ng-app="app" class="app">
<div ng-controller="ContactController as ctrl">
<ul>
<li ng-repeat="contact in ctrl.contacts">
<contact id="$index" contact="contact"></contact>
</li>
</ul>
</div>
</div>

The directive takes two attributes, id and a contact object. And displays some information about each contact:

// app/js/directives/Contact.js
angular
.module('app')
.directive('contact', function() {
return {
restrict: 'E',
template: [
'<div class="contact">',
'Name: {{ ctrl.contact.name.title }} {{ ctrl.contact.name.first }} {{ ctrl.contact.name.last }} - <a href="" ng-click="ctrl.remove(ctrl.id)">Remove</a>',
'</div>'
].join(''),
controller: function ($rootScope) {
this.remove = function (id) {

};
},
controllerAs: 'ctrl',
bindToController: {
id: '=',
contact: '='
}
}
});

Looking within the template here, you can see that we have a link to remove a contact. It is calling the remove method on the Contact directive’s controller and passing in the id of this particular contact. But how does the directive know whether its contact’s data has been deleted from the ContactController? How does the ContactController know to remove the the contact from its list?

This is where Angular’s event system comes in. We can publish events on the $rootScope from the Contact directive (where we have the link to remove the contact) and then subscribe to those events from the Contact Controller (where the actual list is kept).

Publishing an Event to the $rootScope from a Directive

In order to publish an event to the $rootScope from the Contact Directive, we use $broadcast:

// app/js/directives/Contact.js
angular
.module('app')
.directive('contact', function() {
return {
restrict: 'E',
template: [
'<div class="contact">',
'Name: {{ ctrl.contact.name.title }} {{ ctrl.contact.name.first }} {{ ctrl.contact.name.last }} - <a href="" ng-click="ctrl.remove(ctrl.id)">Remove</a>',
'</div>'
].join(''),
controller: function ($rootScope) {
this.remove = function (id) {
$rootScope.$broadcast('remove', id);
};
},
controllerAs: 'ctrl',
bindToController: {
id: '=',
contact: '='
}
}
});

You’ll notice that we’re also passing through the id of the contact that we want to remove. This is how the controller will know which contact to remove when it receives a notification that the event has fired.

Subscribing to an Event on the $rootScope from a Controller

In order to subscribe to the event on the $rootScope that we published in the previous step, we use Angular’s $on service:

// app/js/controllers/ContactController.js
angular
.module('app')
.controller('ContactController', function($rootScope){
$rootScope.$on('remove', function(event, data){
this.contacts.splice(data, 1);
    });    
    this.contacts = [
{
"name": {
"title": "mr",
"first": "liam",
"last": "lawson"
},
"location": {
"street": "1642 church lane",
"city": "dundee",
"state": "south yorkshire",
"zip": "LN6Y 1ZG"
},
"email": "liam.lawson@example.com",
"username": "silverwolf439",
"dob": 924314544,
"phone": "015242 99250",
"cell": "0713-889-058",
"NINO": "BX 12 80 44 W",
"picture": {
"large": "https://randomuser.me/api/portraits/men/74.jpg",
"medium": "https://randomuser.me/api/portraits/med/men/74.jpg",
"thumbnail": "https://randomuser.me/api/portraits/thumb/men/74.jpg"
}
},
....
];
});

If we try to run this code in the browser, the remove functionality won’t work. Can you see why? The answer has to do with the way that scope works in Angular. Let’s take a look at that index.html page again:

<div ng-app="app" class="app">
<div ng-controller="ContactController as ctrl">
<ul>
<li ng-repeat="contact in ctrl.contacts">
<contact id="$index" contact="contact"></contact>
</li>
</ul>
</div>
</div>

$rootScope is the parent scope that is created within the element that has the ng-app directive attached to it. Inside of that div, we have the scope created by the ContactController that exists within the div that has the ng-controller directive specifying the ContactController. Within that div, the ng-repeat that loops through the contacts in the ContactController creates its own scope wherein the variable ‘contact’ represents one of the items in the ‘ctrl.contacts’ array. (Note that the ctrl.contacts array exists within the ContactController scope.) Finally, within the ng-repeat, the call to the custom directive called ‘contact’ creates another nested scope.

It’s easy to see how this could get really complicated. Tracking down which scope is in charge of a particular piece of data or functionality and which scope has access to it can be difficult to grasp.

One of the ways we simplify this is by publishing events to the $rootScope and then subscribing to those events from other scopes that need to interact with that event.

But isn’t that what we did? Well, yes. The issue is that we forgot to keep track of scope within the $rootScope.$on function call.

Fixing the Bug

If we console.log the value of this within the $rootScope.$on function call, we can see that it actually refers to the Window object, not the Contact Controller.

// app/js/controllers/ContactController.js
angular
.module('app')
.controller('ContactController', function($rootScope){
$rootScope.$on('remove', function(event, data){
// this refers to the Window object within the $rootScope
console.log(this)
this.contacts.splice(data, 1);
    });
  ...
  });

When we open index.html in the browser and click on one of the remove links, this is what we’ll get:

We can see that ‘this’ refers to the Window object and that when we try to call .splice() on this.contacts we get a TypeError because contacts is not defined as a property of the Window object, it’s defined as a property of the ContactController object.

So in order to access the list that is a property of the ContactController, we need to store the value of ‘this’ in another variable outside of the $rootScope.$on() function call (but still within the ContactController scope). This way, we’ll have access to the ContactController scope within the $rootScope.$on() function:

// app/js/controllers/ContactController.js
angular
.module('app')
.controller('ContactController', function($rootScope){
var ctrl = this;
$rootScope.$on('remove', function(event, data){
ctrl.contacts.splice(data, 1);
});
    this.contacts = [
...
];
  });

And now it’s time to break into the happy dance!

The main takeaway here is to be aware of where your scope is at any given time. If you need to notify other parts of your app about events trigger by user actions, you’ll want to publish events to the $rootScope and then subscribe to them from the other parts of your app that need to know.

Worth Checking Out