/mar 4, 2015

Smart Syncing Angular Services on $scope

By Kyle Clark

AngularJS is a MVW (Model-View-Whatever) framework, which means despite Angular's opinionated nature, it's largely the wild, wild west in how an Angular application is constructed. Whatever works for you. If your Angular program is a weekend warrior project, you can get by with a hodgepodge structure. However, for any serious or large Angular applications, services are not optional. They are a way of life in an Angular ecosystem, and learning how to effectively sync services on $scope is critical to your application's success.

Before we show how to sync data from a service model onto $scope, let's examine why we need services in the first place. There needs to be a clear separation of concerns. Controllers are great for one night stands, if that's your thing, but they aren't designed for long-term relationships. Not in their nature. Controllers are stateful, yet short-lived -- meaning data stored in a controller is transitory and destroyed as we navigate throughout our application away from the controller view. Here one moment; gone the next.

Controllers of course still have their place in an Angular application. They support our Angular views (templates) by defining which data is accessible in the view via $scope and governing presentation logic -- displaying data, how to handle user interactions, etc.

/* Home Controller */

$scope.characters = ['Frank Underwood', 'Selina Meyer', 'Don Draper'];

$scope.addCharacter = function (character) {
  // add character logic goes here...
};
<!-- Home View -->

<!-- Iterate list of characters via ng-repeat -->

<p>List of Characters:</p>
<ul>
 <li ng-repeat="character in characters">{{ character }}</li>
</ul>

<!-- Add new character user interactions -->

<input type="text" ng-model="character">
<button ng-click="addCharacter(character)">Add New Character</button>

In our associated Home view we can use the $scope values defined in the controller to display a list of characters and bind a click event to an element for adding a new character. However, what happens when we need to create a Characters view with the same content? We end up repeating ourselves in the Characters controller:

$scope.characters = ['Frank Underwood', 'Selina Meyer', 'Don Draper'];
$scope.addCharacter = function (character) {
  // add character logic goes here...
};

Same code all over again. In allusion to the Angular tagline "Superheroic JavaScript MVW Framework", services come to the rescue!


Services are a practical option to use in conjunction with controllers if one of the following or more criteria are met:

  • Code needs to be reusable
  • Store application state
  • Independent of the view
  • Caching objects
  • Model CRUD
  • Utility functions
  • 3rd party integration with a library

By creating a Characters service, we are able to hold permanent state on our characters list and addCharacter function once and only once!

/* Characters Service */

this.collection = ['Frank Underwood', 'Selina Meyer', 'Don Draper'];

this.add = function (character) {
  // add character logic goes here...
};

Now we can simplify our controllers and avoid unnecessary code repetition:

/* Home Controller */

// inject Characters service dependency

$scope.characters = Characters.collection;
$scope.addCharacter = function (character) {
  Characters.add(character);
}

--

/* Characters Controller */

// inject Characters service dependency

$scope.characters = Characters.collection;
$scope.addCharacter = Characters.add.bind(Characters); // Alternative method to define $scope.addCharacter

The benefits to utilizing services do not stop at code succinctness. Creating and maintaining a large data-driven application can get tedious and cumbersome. Services, which as a general term in Angular include factories, services, and providers, drive the application independent of views. As we've shown, services are reusable and store business logic.

A service is essentially a model in the traditional MVC sense with data and functionality stored perpetually for the duration of the user's application instance, unlike controllers. In the scenario of our Characters service, the list of characters and function for adding a character persists. For example, a user may add a character in the Home view and the new character will display instantly in Home as well as appear when we navigate to the Characters view.

-- user input: 'Walter White'
-- user clicks add new character passing the input value

On click of the Add Character button, the character input value will be passed into the Characters.add method via $scope.addCharacter.

/* Characters Service */

this.add = function (character) {
  // push the character onto the collection list
  this.collection.push(character);
};

Immediately following, our Home view DOM is instantaneously re-rendered and includes the new character added...

List of Characters:

  • Frank Underwood
  • Selina Meyer
  • Don Draper
  • Walter White

Walter White (or should we say his name "Heisenberg") is added onto Characters.collection. Then, once Characters.collection changes the $scope.characters value is immediately updated, since $scope.characters is set to equal the value of Characters.collection, and the Home view DOM is refreshed to display the updated characters list on scope. All done in the blink of an eye.

Try it out in a Plunker.

The example thus far has been trivialized in complexity to demonstrate the fundamentals of syncing Angular services on $scope. Let's take it one step further and introduce a backend request into adding a new character.


/* Characters Service */

// self to reference this pointing to the Characters service instance
var self = this;

// add character http post
this.add = function (character) {
  var apiEndpoint = '/api/characters';

  $http
    .post(apiEndpoint, character)
    .then(self.addSuccessHandler, self.addFailHandler)
    .catch(self.addErrorHandler);
}

// Handle add character post success
this.addSuccessHandler = function (response) {
  // presumes the backend returns a response which contains the full list of characters in response.data

  self.collection = response.data;
}

// insert fail, error handlers

When a user adds a new character, the add method in the Characters service is called. An $http POST call to the backend API is initiated to add a new character to our backend characters data. If the POST is successful, the addSuccessHandler method is called where we accept the backend response as a parameter. And just like that, Characters.collection is updated.

In the Home and Characters controllers, $scope.characters is revised to equal the new value of Characters.collection when a user navigates to one of those views. The characters list in our views will be updated instantly -- without a page refresh from the server. Our backend data in this process is synchronized with our local Characters service model which syncs with $scope in our Home & Characters controllers along with their associated views.

Data flows from user interaction in a view to its controller up to a service and over the wire to a backend and back down the pipe through the synchronization chain of Angular services, controllers, and views. There is certainly far more to discuss, such as $scope.$watch for specific changes, about smart syncing services on $scope than we can cover in a single blog post.

Suffice to say services are vital to architecting a large Angular application. The front-end architecture of our single-page Angular application at SRC:CLR relies on the power and resourcefulness of services for our rapid development pace, code maintenance and flexibility.

Related Posts

By Kyle Clark