Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Getting Started with Angular: Dealing with Scopes/Controllers

A common source of confusion among new Angular developers is scopes. The parent-child scope model is confusing, and it isn’t straightforward how to share data between scopes. It’s common in applications to see nested controllers and heavy use of scope inheritance. I don’t think this these are good patterns to follow, because they make it difficult to understand where data lives, and they can lead to bloated, highly-coupled controllers.

TLDR: It’s better to keep controllers flat and independent. Use service objects to share data among scopes.

Lets say you are building an app to keep track of ping-pong tournament. You have a PeopleCtrl, used on a part of your app that displays all competitors. You also have a MatchCtrl, which keeps track of all current matches. Both controllers need to know who is competing in the tournament, so they need to share the list of all the tournament competitors.

At first glance, it may seem like a good idea to store the list of competitors on the root scope.

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

/* ... */

app.controller('PeopleCtrl', function($rootScope) {
  $scope.people = $rootScope.people;
});

app.controller('MatchCtrl', function($rootScope) {
  $scope.people = $rootScope.people;

  $scope.createMatch = function createMatch(personIndex1, personIndex2) {
    var person1 = $scope.people[personIndex1];
    var person2 = $scope.people[personIndex2];

    /* ... */
  };
});

This doesn’t look terrible right now; however, if you start repeating this pattern you’re root scope is going to get real big real quick. It can also lead to lots of crazy data conflicts.

Similarly, you could create a parent controller that wraps PeopleCtrl and MatchCtrl, and passes data to them via scope inheritance:

app.controller('ParentCtrl', function() {
  $scope.people = [ /* ...*/ ];
});

app.controller('PeopleCtrl', function() {
  /* No need to do anything here, because people is already defined in ParentCtrl */
});

app.controller('MatchCtrl', function() {
  /* people is already defined in ParentCtrl */

  $scope.createMatch = function createMatch(personIndex1, personIndex2) {
    var person1 = $scope.people[personIndex1];
    var person2 = $scope.people[personIndex2];
    /* ... */
  };
});
<div ng-controller='ParentCtrl'>
  <div ng-controller='PeopleCtrl'>
    <!-->...<-->
  </div>
  <div ng-controller='MatchCtrl'>
    <!-->...<-->
  </div>
</div>

The problem with this example is all of the hidden dependencies. First of all, you’ve created restrictions for your html: PeopleCtrl and MatchCtrl DOM elements must always lie nested within a ParentCtrl DOM element – otherwise they won’t function properly. In addition, PeopleCtrl and MatchCtrl use the “people” property from their parent scope without explicitly calling out the dependency. This makes the code more confusing and harder to test, while making it very likely that the property will be overwritten.

Here’s how I would approach the situation:

app.factory('peopleService', function() {
  return {
    people: [ /* ... */ ]
  };
});

app.controller('PeopleCtrl', function(peopleService) {
  $scope.people = peopleService.people;

  $scope.addPerson = function addPerson(person) {
    peopleService.people.push(person)
  };
});

app.controller('MatchCtrl', function(peopleService) {
  $scope.people = peopleService.people;

  /* ... */
});

What I love about this data-sharing strategy boils down to basic design principles: separation of concerns, and well-managed dependencies. Our data is nicely encapsulated in a service that does nothing else but maintain the data. In addition, the controllers explicitly define their PeopleService dependency, and we no longer have any html restrictions. Testing is awesome as well – you could choose to use a simple PeopleService mock in the controller unit tests, or the controllers could use the actual service in a more integration-style test.

Even though your data lives in a service, two-way bindings still work. If you call the “addPerson” method on PeopleCtrl, the “people” variable on both scopes will update. This is because service objects in Angular are singletons and are guaranteed to never be re-instantiated.

To summarize, here’s two heuristics I try to follow when dealing with controllers/scopes:

  1. Keep your controller structure as flat as possible. Avoid controller nesting and scope inheritance.
  2. Use services to store and share data. Avoid having data live in controllers.

That being said, I am not an Angular expert by any means. If you disagree with me or have counterexamples, please share!

Comments
  1. lucas says:

    Thanks for writing this!
    I also struggled with Data sharing between Controllers and the Angular docs were not much help.

    QQ: In your final example you wrote:
    app.controller(‘PeopleCtrl’, function(peopleService) {
    $scope.people = PeopleService.people;

    was it meant to be:
    app.controller(‘PeopleCtrl’, function(peopleService) {
    $scope.people = peopleService.people; // <—— Casing

    thanks!
    Luke

  2. Geoff Pleiss says:

    Thanks Luke for catching that typo! I updated the example.

  3. Tom says:

    Hi,

    Thanks for the example. Are we not supposed to be including $scope as a parameter of the Controllers?

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *