Step 4 - Bonus: Isolating Todos Access Into a Service

A client side framework like Angular JS is based on the concept of components which allow you to isolate logic into distinct chunks of functionality so you don't end up creating monolithic JavaScript files.

Just as it's common in back end applications to have classes and business objects that isolate functionality into logical units, you should also break up client side functionality into small, manageable units of work.

In Angular there are many types of components that can be created:

  • Services - general purpose classes that provide logic
  • Factories - same as services that are singletons (1 instance)
  • Directives - UI based components that need to interact with the DOM
  • Providers - Interfaces the affect behavior of other services
  • Controllers - the main logic that interfaces the Page/Container with your code

To demonstrate how you can break out functionality let's isolate the $http based logic that is used to retrieve the Todo items from the server into a Service. In the process you'll see how create a new module, and use dependency injection to access the new service in your controller.

There are two kinds of services that Angular supports - Services and Factories. The most common service is a factory which is a singleton, which means it's instantiated once and then kept alive for the duration of the page in the browser. Until you navigate to a new HTML page this factory's data stays intact. This is more efficient and makes it easy to carry application state around your application - you can update the state of the service in multiple controllers and each controller can see the changes. It's and effective way to have 'shared state' across components.

Creating a ToDo Factory Service

So, let's create a TodoService that wraps all the 'business logic' associated with retrieving and managing Todos. This includes making the Http calls and updating the Todo array we've been using in the model.

To keep things inline with our simple example, I add the service to the bottom of the existing todo.js file like this:

app.factory('todoService', todoService);

todoService.$inject = ['$http','$timeout'];

function todoService($http,$timeout) {
    var service = {
        todos: [],

        getTodos: getTodos,
        addTodo: addTodo,
        deleteTodo: deleteTodo            
    };


    return service;

    function getTodos() {
    }

    function addTodo(todo) {
        
    }

    function deleteTodo(todo) {
        
    }
}

Note: you can and usually should create services and components in their own separate files, but because this sample is so simple we'll keep everything in a single file. If you use separate files each file has to have its own module definition:

(function () {
    'use strict';

    angular
        .module('app')
        .factory('todoService', todoService);

    todoService.$inject = ['$http'];

    function todoService($http) {
        var service = {
           ...
        };
        return service;
    }
})();

A service needs to return an object, which provides the service interface. This service can contain properties and methods that define the service behavior.

The layout should be familiar - I like to define a declaration section at the top which just shows the interface, and the actual implementation of functionality in plain functions below. This makes it easy to see what functionality the service provides at a glance.

When we created the controller we already isolated the retrieval methods so as it turns out it's actually pretty straight forward to port the code.

Getting ToDo Items - Take 2

Here's the implementation of the Service's getTodos() function:

function getTodos() {
    return $http.get('todos.csvc')
        .success(function(todos) {
            service.todos = todos;
        })
        .error(function() {
            var error = parseHttpError(arguments);
            return error; // passed to other .error() handlers
        });
}

The code makes the HTTP call and stores the todos on the service.todos property. Remember the service stays alive after the request, so this value remains accessible.

Also notice the .error() which parses the HTTP error into an object. What's nice is that you can now return the resulting error object, which the next .error() handlers in the promise chain will now receive. In other words I can handle the HTTP error here and turn it into a clean error object that I can pass up the chain to the application.

To use this service method in the controller we can now change the loadTodos() function we created in the controller.

First we need to inject the service into the controller's parameter list:

pageController.$inject = ['$scope', '$http', '$timeout','todoService'];

function pageController($scope, $http, $timeout,todoService) {

Then we can change the loadTodos() function to:

function loadTodos() {            
    todoService.getTodos()
        .success(function (todos) {                    
            vm.todos = todos;
            
            // this also works:
            // vm.todos = todoService.todos
        })
        .error(function (error) {    
            // set the message to update the alert box
            vm.message = error.message;
        });
}

The logic is very similar, except we now have access to the todoService to retrieve additional information. Notice that the .error() method now receives an error parameter which is an already parsed HTTP error message so the application code doesn't have to know how the error was generated or had to be parsed.

If you run the code now, you should once again see the list loading.

Finishing up

So let's apply the same principles to the adding and deleting todos.

Here are the two method that implement these tasks in the service:

function addTodo(todo) {
    // copy the tod
    var td2 = $.extend({}, todo);

    return $http.post('todo.csvc', todo)
       .success(function (todo) {
           // and add it to the model array
            service.todos.splice(0, 0, td2);
        })                
       .error(function () {
            return ww.angular.parseHttpError(arguments);
       });
}

function deleteTodo(todo) {            
    return $http.delete('todo.csvc?title=' + encodeURIComponent(todo.title))
       .success(function() {
           // find the tdo and then remove it
           var index = service.todos.indexOf(todo);
           if (index > -1)
               service.todos.splice(index, 1);
       })
       .error(function () {
           return ww.angular.parseHttpError(arguments);
       });            
}

Both of these methods handle two tasks:

  • Make the HTTP call
  • Update the service.todos array

As items get added and deleted the service manages the state of the todos list which is now removed from the code in the controller.

This makes the job of the controller that's calling these methods much simpler as all the controllers has to do is call the method and rebind the list to the service's list.

Here's what the relevant pageController looks like:

function addTodo() {
    var todo = $.extend({}, vm.activeTodo);

    todoService.addTodo(todo)
        .success(function() {
            // just rebind the list of todos
            // which already has the merged todo in it
            vm.todos = todoService.todos;
        })
        .error(function(error) {
            vm.message = error.message;
        });
}

function removeTodo(todo, ev) {
    todoService.deleteTodo(todo)
        .success(function() {
                vm.todos = todoService.todos;
            })
            .error(function(error) {
                vm.message = "Couldn't delete Todo: " + error.message;
            });
}

As you can see both of these methods have gotten a lot simpler from the previous counter parts. Gone is any dependency on the $http service and gone is the code that needs to parse the list and add or remove the todo item - all of that is now encapsulated in the service, as is the error handling. The only thing this code needs to know about the error is that an object with a .message property is passed when an error occurs which is much simpler than the parse code we have now shifted to the service.

One key thing to look at is this assignment in both methods:

vm.todos = todoService.todos;

which rebinds the service list to the controller's todo items. The service updates the list of todos and by rebinding the service list to the local model value, the list rebinds in the UI with the updated data.

Sweet - we've now moved all of the todo based 'business logic' into a separate service where it's easier to maintain in one place. The code is reusable and - if necessary - can be replaced or mocked with another service that has the same interface.

Why a Service?

Because this todo applet is such a simple example, the benefit of using a service is not immediately obvious. After all it doesn't reduce the code size much. However, in a more complex application you are likely to have more 'business logic' wrapped up in the service. For example, you might have to filter the data after it arrives or strip out certain fields or validate the data before you allow it to be passed into the application. The service provides the intermediary hook where you can apply this logic. Even in this simple example a few tasks like updating the array are nicely encapsulated inside of the service.

The real benefit of this comes the first time you need to reuse the service in another part of the same application. In most applications services are used in multiple components and by having the logic in a separate component it's easy to reuse it. Likewise if there is a problem with the code in the service, there's only one place where you need to fix it.

Even in our simple example, the service abstracts away the entire $http interface in our controller. In fact once we move the other functions to use the service we can remove the $http dependency altogether. This essentially separates the service interface/logic and the implementation where the low level HTTP access is the responsibility of the service. if you decide you want to retrieve the data from some other source (say WebSockets, or a static file) you can simply re-implement the service and use the other implementation without changing the controller application logic.

This is very similar to the way we think of business objects in a desktop or server application, except here we're isolating client logic. You can create services large and small and you can use those services to effectively separate discreet components and behavior into easily reusable pieces that you can inject into your code.

Summary

In this step you learned:

  • How to create a Factory Service
  • How to isolate 'business logic' into a service
  • How to make $http calls and handle errors consistently
  • How to inject a custom service into your controller
  • How to use the service in your controller code

And this concludes this tutorial. I've taken you through a very small and simple Todo application that nevertheless demonstrates many of the key features of Angular JS. I hope you've gotten a feel for how a framework like Angular makes it much easier to build user interfaces in the browser, based on the two-way databinding features, the module based architecture that lets you break up complex applications into modules, and the rich support features like the $http subsystem, the form validation and more.

Keep in mind this is a small self-contained example that is managed in a single JavaScript code file. More complex applications require separation of each module into its own file and setting up more full features application configuration, routing and using Views to swap content in and out for each view you need to display. If you want a more full featured sample, please check out the MusicStore sample that ships with Web Connection.


© West Wind Technologies, 1996-2019 • Updated: 02/07/16
Comment or report problem with topic