Getting Started with Angular: Dealing with Scopes/Controllers

May 11, 2014 Geoff Pleiss

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!

About the Author

Biography

More Content by Geoff Pleiss
Previous
Golang Memory Benefit for Cloud Foundry BOSH
Golang Memory Benefit for Cloud Foundry BOSH

Several different areas of the Cloud Foundry codebase have recently been using Go for some new components o...

Next
Setting up a FreeBSD Server on Hetzner, Part 5: PHP, SSI, SSL, Redirects
Setting up a FreeBSD Server on Hetzner, Part 5: PHP, SSI, SSL, Redirects

In this blog post we describe the procedure to configure nginx on a FreeBSD VM to use PHP, SSI (Server Side...

Enter curious. Exit smarter.

Learn More