AngularJS: Sharing data between controllers

Even though you should try to keep things decoupled and your directives, controllers and services should rather be self contained, you sometimes do need to share data between controllers. There are basically two main scenarios:

  1. Sharing data between a parent and a child controller
  2. Sharing data between two mostly unrelated controllers e.g. two siblings

Sharing data between a parent and a child controller

Let’s assume you have two controllers, one controller being the parent controller and a child controller:

<div ng-controller="ParentCtrl">
  <div ng-controller="ChildCtrl as vm">
  </div>
</div>

Where the controllers are defined as:

var app = angular.module('sharing', []);

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

app.controller('ChildCtrl', function($scope) {
});

Let’s now define a user name in the parent controller:

app.controller('ParentCtrl', function($scope) {
  $scope.user = {
    name: "Henri"
  };
});

Note that you shouldn’t define your variable as a primitive in the scope since this will cause shadow property to appear in the child scope hiding the original property on the parent scope (caused by the JavaScript prototype inheritance). So you should define an object in the parent scope and defined the shared variable as a property of this object.

There are now three ways to access this shared variable:

  1. using $parent in HTML code
  2. using $parent in child controller
  3. using controller inheritance

Using $parent in HTML code

You can directly access variables in the parent scope by using $parent:

Hello {{$parent.user.name}}

Using $parent in child controller

Of course, you could also reference the shared variable using $parent in the controller and expose it in the scope of the child controller:

app.controller('ChildCtrl', function($scope) {
  $scope.parentUser = $scope.$parent.user;
});

Then, you can use this scope variable in your HTML code:

Hello {{parentUser.name}}

Using controller inheritance

But since the scope of a child controller inherits from the scope of the parent controller, the easiest way to access the shared variable is actually not to do anything. All you need to do is:

Hello {{user.name}}

Sharing data between two mostly unrelated controllers e.g. two siblings

If you need to share data between two controllers which do not have a parent-child relationship, you can neither use $parent nor rely on prototype inheritance. In order to still be able to share data between such controllers, you have three possibilities:

  1. Holding the shared data in a factory or service
  2. Holding the shared data in the root scope
  3. Using events to notify other controller about changes to the data

Holding the shared data in a factory or service

AngularJS factories (and services) can contain both methods (business logic) and properties (data) and can be injected in other components (e.g. your controllers). This allows you to define a shared variable in a factory, inject it in both controllers and thus bind scope variables in both controllers to this factory data.

The first step is to define a factory holding a shared value:

app.factory('Holder', function() {
  return {
    value: 0
  };
});

Then you inject this factory in your two controllers:

app.controller('ChildCtrl', function($scope, Holder) {
  $scope.Holder = Holder;
  $scope.increment = function() {
    $scope.Holder.value++;
  };
});

app.controller('ChildCtrl2', function($scope, Holder) {
  $scope.Holder = Holder;
  $scope.increment = function() {
    $scope.Holder.value++;
  };
});

In both controllers, we bind the Holder factory to a scope variable and define a function which can be called from the UI and updates the vale of the shared variable:

<div>
  <h2>First controller</h2>
  <button>+</button>{{Holder.value}}
</div>
<div>
  <h2>Second controller</h2>
  <button>+</button>{{Holder.value}}
</div>

No matter which “+” button you press, both values will be incremented (or rather the shared value will be incremented and reflected in both scopes).

Holding the shared data in the root scope

Of course, instead of using a factory or a service, you can also directly hold the shared data in the root scope and reference it from any controller. Although this actually works fine, it has a few disadvantages:

  1. Whatever is present in the root scope is inherited by all scopes
  2. You need to use some naming conventions to prevent multiple modules or libraries from overwriting each other’s data

In general, it’s much cleaner to encapsulate the shared data in dedicated factories or services which are injected in the components which require access to this share data than making these data global variables in the root scope.

Using events to notify other controller about changes to the data

In case, you do not want to bind both scopes through factory data (e.g. because you only want to propagate changes from one scope to another one on some condition), you can also rely on event notifications between the controllers to sync the data. There are three functions provided by AngularJS to handle events:

  • $emit is used to trigger an event and propagate it to the current scope and recursively to all parent scopes
  • $broadcast is used to trigger an event and propagate it to the current scope and recursively to all child scopes
  • $on is used to listen to event notification on the scope
Using $emit

Since $emit is propagating events up in the scope hierarchy, there are two use cases for it:

  • Propagating events to parent controllers
  • Efficiently propagating events to unrelated controllers through the root scope

In the first scenario, you emit an event on the child controller scope:

$scope.$emit("namechanged", $scope.name);

And listen to this event on the parent controller scope:

$scope.$on("namechanged", function(event, name) {
  $scope.name = name;
});

In the second scenario, you emit an event on the root scope:

$rootScope.$emit("namechanged", $scope.name);

And listen to this event on the root scope as well:

$rootScope.$on("namechanged", function(event, name) {
  $scope.name = name;
});

In this case there is effectively no further propagation of the event since the root scope has no parent scope. It is thus the preferred way to propagate events to unrelated scopes (and should be preferred to $broadcast in such scenarios).

There is one thing you need to consider when registering to events on the root scope: in order to avoid leaks when controllers are created and destroyed multiple times, you need to unregister the event listeners. A function to unregistered is returned by $emit. You just need to register this function as a handler for the $destroy event in your controller, replacing the code above by:

var destroyHandler = $rootScope.$on("namechanged", function(event, name) {
  $scope.name = name;
});

$scope.$on('$destroy', destroyHandler);
Using $broadcast

Theoretically, you could also use $broadcast to cover two scenarios:

  • Propagating events to child controllers
  • Propagating events to unrelated controllers through the root scope

Effectively, the second use case doesn’t make much sense since you would basically trigger an event on the root scope and propagate it to all child scopes which is much less efficient than propagating and listening to events on the root scope only.

In the first scenario, you broadcast an event on the parent controller scope:

$scope.$broadcast("namechanged", $scope.name);

And listen to this event on the child controller scope:

$scope.$on("namechanged", function(event, name) {
  $scope.name = name;
});
Similarities and differences between $emit and $broadcast

Both $emit and $broadcast dispatch an event through the scope hierarchy notifying the registered listeners. In both cases, the event life cycle starts at the scope on which the function was called. Both functions will pass all exceptions thrown by the listeners to $exceptionHandler.
An obvious difference is that $emit propagates upwards and $broadcast downwards (in the scope hierarchy). Another difference is that when you use $emit, the event will stop propagating if one of the listeners cancels it while the event cannot be canceled when propagated with $broadcast.

Demo

You can see the code from this post in action on plunker:

Leave a Reply

Your email address will not be published. Required fields are marked *