Mastering AngularJS Services Part 2 - Communicating with Directives

This is part two of a two part series on how to effectively use angular services to pass data cleanly around your application.

In Part 1, we discussed the differences between services and factories and the difference in approach for each. In this article, we will build on the previous example, adding more functionality to the directory application we created, and adding a directive that we can reuse elsewhere in our application.

Our goal is to learn how to use services to pass data between controllers and directives and back without polluting the $rootScope or having to broadcast events. A demo of what we will be creating can be found at the bottom of the page. I strongly encourage you to open the Plunkr up and edit the code to get a feel for it.

Building on the previous example, lets first add some functionality to our Directory service. We're going to let our users add people to the directory, and it will be useful to also let them remove people. Whilst we're at it, let's add some validation to make sure everyone in the directory has a unique name. In the real world this may not be useful, but the exercise will let us practice some core principles we've already learnt about variable scope inside services.

Here's how the updated Directory service looks:

/* Directory.js */
angular.module('FactoryServiceDemo').service('Directory', Directory);

function Directory (Person, $q, $log) {  
  var self = this;

  // array of Person objects
  this.people = [];

  // define methods
  this.addPerson = addPerson;
  this.removePerson = removePerson;
  this.reset = reset;

  /**
   * The addPerson method first attempts to find the person in 
   * the directory. If the person is not found, add it.
   */
  function addPerson (person) {
    _find(person.firstname, person.lastname)
      .then(_onPersonFound, _onPersonNotFound);

    function _onPersonFound () {
      $log.info(person.firstname + ' ' + person.lastname + ' already exists');
    }

    function _onPersonNotFound () {
      if (person.firstname && person.lastname) {
        self.people.push(new Person(person.firstname, person.lastname)); 
      }
    }
  }

  /**
   * The removePerson method first attempts to find the person in 
   * the directory. If the person is found, remove it.
   */
  function removePerson (person) {
    _find(person.firstname, person.lastname)
      .then(_onPersonFound, _onPersonNotFound);

    function _onPersonFound (foundPerson) {
      self.people.splice(foundPerson.index, 1);
    }

    function _onPersonNotFound (err) {
      $log.info('Directory Error', err);
    }
  }

  /**
   * The reset method emptys the directory
   */
  function reset () {
    self.people.length = 0;
  }

  /**
   * This private method attempts to find a person in
   * the directory, given a first name and last name.
   * 
   * It returns a promise containing either the person 
   * object and its index in the people array, or a 
   * message if no object is found.
   */
  function _find (firstname, lastname) {
    var d = $q.defer(),
        found = false;

    // loop through the people array and compare
    angular.forEach(self.people, function (person, index) {
      if (person.firstname === firstname && person.lastname === lastname) {
        found = true;
        d.resolve({ person: person, index: index });
      }
    });

    // if not found, set error message
    if (!found) {
      d.reject('Person not found');
    }

    return d.promise;
  }
}

Tip #1: If you haven't done so already, I would encourage you to look through John Papa's excellent Angular Style guide. It's by far the most thorough style guide I've seen and although you may not agree with all the conventions, there's definitely something for every one of us to learn to make our code cleaner and more readable.

There's a lot of code here so let's break it down and digest it method by method. The first thing you will notice is we've updated the addPerson method. As a reminder here's how it looked previously:

function addPerson (firstname, lastname) {  
  self.people.push(new Person(firstname, lastname)); 
}

Here's how it looks now:

function addPerson (person) {  
  _find(person.firstname, person.lastname)
    .then(_onPersonFound, _onPersonNotFound);

  function _onPersonFound () {
    $log.info(person.firstname + ' ' + person.lastname + ' already exists');
  }

  function _onPersonNotFound () {
    if (person.firstname && person.lastname) {
      self.people.push(new Person(person.firstname, person.lastname)); 
    }
  }
}

At first it may look like a big change, but all we've done is add a call to a private _find method, and we only add a new person if they don't already exist. As you'll remember from the last article, if we want to create a private method, we declare it without this.

The purpose of the _find method is to search the directory and return a promise with the result. Although it's beyond the scope of this article, I highly recommend the use of promises to maintain clean code and keep logic encapsulated.

Tip #2: Promises keep your code clean.

We added 2 new methods, removePerson and reset. There shouldn't be any new concepts here. The only thing to note is the way we reset the array. Instead of:

self.people = [];  

We do:

self.people.length = 0;  

There's a good reason for this. The true power of services is in maintaining a single source of truth. We want the ability to directly reference the data in our service, and therefore we have to put some thought in to how we maintain that reference. Setting self.people to [] essentially tells it to point to a new array somewhere else in memory, meaning references to the original array are destroyed. Setting the arrays length to 0 however, clears it out without setting it to a new point in memory, meaning our references are maintained.

Now we've added more functionality to the Directory, let's create a simple DirectoryList directive to output some data. You'll notice all we're doing is rendering a very basic table based on the information in our service. Our controller is kept clean because we've already created all the logic.

/* DirectoryList.js */
angular.module('FactoryServiceDemo').directive('DirectoryList', DirectoryList);

function DirectoryList (Directory) {  
  var DirectoryListDirective = {
    scope: {},
    restrict: 'EA',
    template: '<table class="table table-striped table-hover table-bordered">' +
                '<tr ng-repeat="person in people track by $index">' +
                  '<td>' +
                    '{{person.getFullName()}}' +
                    '<div class="close" ng-click="removePerson(person)">&times;</div>' +
                  '</td>' +
                '</tr>' +
              '</table>',

    controller: DirectoryListController
  };

  function DirectoryListController ($scope, $element, $attrs) {
    $scope.people = Directory.people;
    $scope.removePerson = Directory.removePerson;
  }

  return (DirectoryListDirective);
}

Tip #3: track by increases performance of ng-repeats.

Finally, let's update our controller and view to enable our users to enter some information.

/* MainController.js */
angular.module('FactoryServiceDemo').controller('MainController', MainController);

function MainController ($scope, Directory) {  
  $scope.person = {};
  $scope.addPerson = Directory.addPerson;
  $scope.reset = Directory.reset;
}

$scope.person is an object that holds the users input. We wire up addPerson and reset to some buttons using ng-clicks, and the Directory service handles the rest. The Person service from our previous example hasn't changed.

<div ng-controller="MainController">  
  <form class="form-horizontal">
    <div class="form-group">
      <label for="firstname" class="col-xs-2 control-label">Firstname:</label>

      <div class="col-xs-10">
        <input type="text" id="firstname" class="form-control" ng-model="person.firstname" placeholder="Firstname" />
      </div>
    </div>

    <div class="form-group">
      <label for="lastname" class="col-xs-2 control-label">Lastname:</label>

      <div class="col-xs-10">
        <input type="text" id="lastname" class="form-control" ng-model="person.lastname" placeholder="Lastname" />
      </div>
    </div>

    <div class="form-group">
      <div class="col-xs-offset-2 col-xs-10">
        <button class="btn btn-primary" ng-click="addPerson(person)">Add Person</button>
        <button class="btn btn-danger" ng-click="reset()">Reset</button>
      </div>
    </div>
  </form>

  <hr />

  <directory-list></directory-list>
</div>  

We're already able to see how easy it is to create clean, maintainable and powerful applications using Angular's services and factories correctly. We managed to create services that persist data through the applications lifespan without polluting $rootScope or fragmenting our logic by broadcasting and listening for events, we've created a clean directive that can be reused easily wherever we need, and we've kept the MVC Gods happy by keeping our controllers free of business logic. You can see a working version including all the code in the Plunkr below.


comments powered by Disqus