Multiple controllers and factories

At this point, we are able to make a webpage with a controller where we can create new objects and display those objects in our index.html view. Let's say that the teacher using our student-roster application wants to be able to keep a separate list on the same page to keep track of which students have turned in their field trip permission slip, so that a phone call can be made to the parents of those who haven't.

At this point we're adding new functionality, so it's appropriate to add another controller. We want to build each of our controllers to handle one specific functionality. This prevents our controllers from becoming too large to understand as we develop more complex apps.

Let's create a new controller to handle the permission slip functionality. We'll call it FieldTripsCtrl. Let's also change the js folder to be named controllers to be clearer about what that folder contains. Don't forget to change the <script> tag in the index.html page too.

Let's add our FieldTripsCtrl now:

controllers/FieldTripsController.js

studentRoster.controller('FieldTripsCtrl', function FieldTripsCtrl($scope) {
  $scope.studentsWithPermission = [{ name: "Jane Doe" }, { name: "John Doe" }];
});

We've added some sample students who have turned in their permission slips. Now we'll need to update our index.html to display this list.

index.html

//This code can go below the form to enter a student
 <div class="row permission">
    <div class="col-md-6">
      <h2>Permission Slips</h2>
      <ul>
        <li ng-repeat="item in studentsWithPermission">
          {{item.name}}
        </li>
      </ul>
    </div>
  </div>

Now let's refresh and give it a whirl. Uh-oh, you'll notice it successfully loads our page with the <h2>, but there are no students showing! There are also no errors in the console. What's going on?

Right now this <div class="row permission"> section is still under the power of the StudentsCtrl, so it can't access the studentsWithPermission property in the FieldTripsCtrl controller.

Let's fix that. Add in ng-controller = "FieldTripsCtrl" into the <div class="row permission">. Now everything contained in that <div> will be under the control of the FieldTripsCtrl. Let's move the ng-controller = "StudentsCtrl" to a new <div> that wraps the old part of the page:

index.html

<body>
  <div class="container">
    <div  ng-controller="StudentsCtrl">
      <div class="row" ng-show="students.length">
        <div class="col-md-12">
          <h4>Search Students</h4>
          <form class="form-inline" role="form">
            <div class="form-group">
              <input ng-model="query" type="text" class="form-control" id="student-name" placeholder="Search">
            </div>
          </form>
        </div>
      </div>
      <div class="row">
        <div class="col-md-6">
          <h2 ng-show="students.length && filtered.length">Student List</h2>
          <h2 ng-show="students.length && !filtered.length">No Matches</h2>
          <ul>
            <li ng-repeat="item in filtered = (students | filter:query)">
              <span ng-click="editing = true" ng-hide="editing">
                {{item.name}} <a ng-click="StudentsFactory.deleteStudent(item)">Delete</a>
              </span>
              <span class="form-group" ng-show="editing" ng-submit="editing = false">
                <form class="form-inline" role="form">
                  <input type="text" class="form-control input-sm" ng-model="item.name" placeholder="Name" required/>
                  <button class="btn btn-default btn-sm" type="submit">Save</button>
                </form>
              </span>
            </li>
          </ul>
        </div>
        <div class="col-md-6">
          <h2>New Student</h2>
          <form ng-submit="StudentsFactory.addStudent()" class="form-inline" role="form">
            <div class="form-group">
              <input type="text" ng-model="StudentsFactory.studentName" class="form-control" id="student-name" placeholder="Enter name">
            </div>
            <button type="submit" class="btn btn-default">Submit</button>
          </form>
        </div>
      </div>
    </div>
   <div class="row permission" ng-controller="FieldTripsCtrl">
      <div class="col-md-6">
        <h2>Permission Slips</h2>
        <ul>
          <li ng-repeat="item in studentsWithPermission">
            {{item.name}}
          </li>
        </ul>
      </div>
    </div>
  </div>
</body>

Now if you refresh the page, it should load our pre-made list of students who have turned in their permission slips.

Our pre-made list of students is great, but it isn't linked to our list of actual students. What we really need is a single place to store information about the students. In this case, that will include their name, and well as whether they've turned in their permission slip or not.

You might think that this code would work:

controllers/FieldTripsController.js

studentRoster.controller('FieldTripsCtrl', function FieldTripsCtrl($scope) {
  $scope.addStudentWithPermissionSlip = function(student) {
    student.permissionSlip = true;
  };
});

The problem is that this FieldTripsCtrl controller can't access the $scope.students array that currently resides in the StudentsCtrl controller. The $scope variable is scoped to its controller.

Because any one <div> can only have one ng-controller directive active at a time we need a place where any controller can access data.

Angular has us covered. In order to allow controllers to share data, we need to use a factory. A factory is one of many services that Angular offers to organize and share code across your app. A factory gets called once at the start of the application and is available for use by any controller throughout the session. It's the well from which any controller can draw water.

Let's create a folder called services and put our factory inside of it:

services/StudentsFactory.js

studentRoster.factory('StudentsFactory', function StudentsFactory() {
  var factory = {};
  factory.students = [];

  factory.addStudent = function() {
    var student = { name: factory.studentName, permissionSlip: false };
    factory.students.push(student);
    factory.studentName = null;
  };

  factory.deleteStudent = function(student) {
    var index = factory.students.indexOf(student)
    factory.students.splice(index, 1);
  };
  return factory;
});

We've taken all the properties and methods that used to be in the StudentsCtrl and moved them to the factory, so that multiple controllers can have access to them. We have also changed our addStudent method so that when we create a new student it has permissionSlip set to false by default. Here's what our StudentsCtrl should look like now:

controllers/StudentsController.js

studentRoster.controller('StudentsCtrl', function StudentsCtrl($scope, StudentsFactory) {
  $scope.students = StudentsFactory.students;
  $scope.StudentsFactory = StudentsFactory;
});

Notice we need to pass in StudentsFactory as an argument to our controller in order to use it. This tells our controller where to find data. We've also created a $scope.students property. That's a shortcut for the array of students, so that we can simply call students instead of StudentsFactory.students every time.

Lastly, we need to update the index.html file by requiring the StudentsFactory script and reflecting our changes in the body:

index.html

...
<div class="row">
  <div class="col-md-6">
    <h2 ng-show="students.length && filtered.length">Student List</h2>
    <h2 ng-show="students.length && !filtered.length">No Matches</h2>
    <ul>
      <li ng-repeat="item in filtered = (students | filter:query)">
        <span ng-click="editing = true" ng-hide="editing">
          {{item.name}} <a ng-click="StudentsFactory.deleteStudent(item)">Delete</a>
        </span>
        <span class="form-group" ng-show="editing" ng-submit="editing = false">
          <form class="form-inline" role="form">
            <input type="text" class="form-control input-sm" ng-model="item.name" placeholder="Name" required/>
            <button class="btn btn-default btn-sm" type="submit">Save</button>
          </form>
        </span>
      </li>
    </ul>
  </div>
  <div class="col-md-6">
    <h2>New Student</h2>
    <form ng-submit="StudentsFactory.addStudent()" class="form-inline" role="form">
      <div class="form-group">
        <input type="text" ng-model="StudentsFactory.studentName" class="form-control" id="student-name" placeholder="Enter name">
      </div>
      <button type="submit" class="btn btn-default">Submit</button>
    </form>
  </div>
</div>
...

All we've done here is prepend 'StudentsFactory' to the methods that we're calling from the factory, and to the studentName model, since student names are now being saved in the StudentsFactory and not the StudentsCtrl.

Now we need to tell our FieldTripsCtrl how to find StudentsFactory and how to change data inside of it. Here's what it should look like:

controllers/FieldTripsController.js

studentRoster.controller('FieldTripsCtrl', function FieldTripsCtrl($scope, StudentsFactory) {
  $scope.students = StudentsFactory.students;
  $scope.addStudentWithPermissionSlip = function(student) {
    student.permissionSlip = true;
  };
});

Finally we need to update the FieldTripsCtrl section of our HTML to use this method and show the correct lists of students.

index.html

<div class="row" ng-controller="FieldTripsCtrl">
  <div class="col-md-6" ng-show="studentsWithoutPermission.length">
    <h3>Without Permission Slips</h3>
    <ul>
      <li ng-repeat="item in studentsWithoutPermission = (students | filter:{permissionSlip: false})">
        {{item.name}} <a ng-click="addStudentWithPermissionSlip(item)"> - received permission slip -</a>
      </li>
    </ul>
  </div>
  <div class="col-md-6" ng-show="studentsWithPermission.length">
    <h3>With Permission Slips</h3>
    <ul>
      <li ng-repeat="item in studentsWithPermission = (students | filter:{permissionSlip: true})">
        {{item.name}}
      </li>
    </ul>
  </div>
</div>

By adding the directive ng-show="studentsWithoutPermission.length" we are only showing the permission slip information if there are actually students in the studentsWithoutPermission array. You might be wondering when we actually created the studentsWithoutPermission array. We do just below with the line <li ng-repeat="item in studentsWithoutPermission = (students | filter:{permissionSlip: false})">. This sets the studentsWithoutPermission array equal to the students array with the filter permissionSlip: false.

When the user clicks on the - received permission slip - link in that list, it will set the permissionSlip status to true, and then the student's name will be displayed in the studentsWithPermission array below.

We have two kinds of functionality on our page now, but only one type of object - a student - which is stored in the StudentsFactory. We also have two controllers updating different properties of that object type. And we know how to use multiple controllers and create factories in our Angular apps.

Summary

index.html

<!doctype html>
<html lang="en" ng-app="studentRoster">
<head>
  <meta charset="UTF-8">
  <title>Student App</title>
  <script src="lib/angular.js"></script>
  <script src="app.js"></script>
  <script src="controllers/StudentsController.js"></script>
  <script src="controllers/FieldTripsController.js"></script>
  <script src="services/StudentsFactory.js"></script>
  <link rel="stylesheet" href="css/bootstrap.min.css">
</head>
<body>
  <div class="container">
    <div ng-controller="StudentsCtrl">
      <div class="row" ng-show="students.length">
        <div class="col-md-12">
          <h4>Search Students</h4>
          <form class="form-inline" role="form">
            <div class="form-group">
              <input ng-model="query" type="text" id="student-name" class="form-control" placeholder="Search">
            </div>
          </form>
        </div>
      </div>
      <div class="row">
        <div class="col-md-12">
          <h2 ng-show="students.length && filtered.length">Student List</h2>
          <h2 ng-show="students.length && !filtered.length">No Matches</h2>
          <ul>
            <li ng-repeat="item in filtered = (students | filter:query)">
              <span ng-click="editing = true" ng-hide="editing">
                {{item.name}} <a ng-click="StudentsFactory.deleteStudent(item)">Delete</a>
              </span>
              <span class="form-group" ng-show="editing" ng-submit="editing = false">
                <form class="form-inline" role="form">
                  <input type="text" class="form-control input-sm" ng-model="item.name" placeholder="Name" required/>
                  <button class="btn btn-default btn-small" type="submit">Save</button>
                </form>
              </span>
            </li>
          </ul>
        </div>

        <div class="col-md-4">
          <h2>New Student</h2>
          <form ng-submit="StudentsFactory.addStudent()" class="form-inline" role="form">
            <div class="form-group">
              <input type="text" ng-model="StudentsFactory.studentName" class="form-control" id="student-name" placeholder="Enter name">
            </div>
            <button type="submit" class="btn btn-default">Submit</button>
          </form>
        </div>
      </div>
    </div>

      <div class="row" ng-controller="FieldTripsCtrl">
        <div class="col-md-12" ng-show="studentsWithoutPermission.length">
          <h3>Without Permission Slips</h3>
          <ul>
            <li ng-repeat="item in studentsWithoutPermission = (students | filter:{permissionSlip: false})">
              {{item.name}} <a ng-click="addStudentWithPermissionSlip(item)"> - received permission slip -</a>
            </li>
          </ul>
        </div>
        <div class="col-md-12" ng-show="studentsWithPermission.length">
          <h3>With Permission Slips</h3>
          <ul>
            <li ng-repeat="item in studentsWithPermission = (students | filter:{permissionSlip: true})">
              {{item.name}}
            </li>
          </ul>
        </div>
      </div>

  </div>
</body>
</html>

StudentsFactory.js

studentRoster.factory('StudentsFactory', function StudentsFactory() {
  var factory = {};
  factory.students = [];

  factory.addStudent = function() {
    var student = { name: factory.studentName, permissionSlip: false };
    factory.students.push(student);
    factory.studentName = null;
  };

  factory.deleteStudent = function(student) {
    var index = factory.students.indexOf(student);
    factory.students.splice(index, 1);
  };
  return factory;
});

StudentsController.js

studentRoster.controller('StudentsCtrl', function StudentsCtrl($scope, StudentsFactory) {
  $scope.students = StudentsFactory.students;
  $scope.StudentsFactory = StudentsFactory;
});
FieldTripsController.js
studentRoster.controller('FieldTripsCtrl', function FieldTripsCtrl($scope, StudentsFactory) {
  $scope.students = StudentsFactory.students;
  $scope.addStudentWithPermissionSlip = function(student) {
    student.permissionSlip = true;
  };
});