Creating and Using a AngularJS Service

The AngularJS Module defines three methods for defining services: factory, service, and provider. The result of using these methods is the same—a service object that provides functionality that can be used throughout the AngularJS application—but the way that the service object is created and managed by each method is slightly different, as I explain and demonstrate in the sections that follow.

1. Using the Factory Method

The simplest way to create a service is to use the Module.factory method, passing as arguments the name of the service and a factory function that returns the service object. To demonstrate how this works, I have created a new file called services.js in the angularjs folder and used it to create a new module that defines a service. You can see the contents of the services.js file in Listing 18-6.

Listing 18-6. The Contents of the services.js File

angular.module(“customServices”, [])

.factory(“logService”, function () {

var messageCount = 0; return {

log: function (msg) {

console.log(“(LOG + ” + messageCount++ + “) ” + msg);

}

};

});

I have defined a new module called customServices and called the factory method to create a service called logService. My service factory function returns an object that defines a log function, which accepts a message as an argument and writes it to the console.

Tip I am creating a custom logging service, but there is a built-in one that I could have used instead. The built-in service is called $log, and I describe it in Chapter 19.

The object returned by the factory function is the service object and will be used by AngularJS whenever the logService is requested. The factory function is called only once because the object it creates and returns is used whenever the service is required within the application. A common error is to assume that each consumer of the service will receive a different service object and assume that variables like counters will be modified by only one AngularJS component.

I have defined a variable called messageCount that is included in the messages written to the JavaScript console to emphasize the fact that services objects are singletons. The variable is a counter that is incremented each time a message is written to the console, and it will help demonstrate that only one instance of the object is created. You will see the effect of this counter when I test the service shortly.

Tip Notice that I defined the messageCount variable in the factory function, rather than as part of the service object. I don’t want consumers of the service to be able to modify the counter, and placing it outside of the service object means that it isn’t accessible to service consumers.

Having created the service, I can now apply it to the main application module, as shown in Listing 18-7.

Listing 18-7. Consuming the Service in the example.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Services and Modules</title>

<script src=”angular.js”></script>

<script src=”directives.js”></script>

<script src=”services.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [“customDirectives”, “customServices”])

.controller(“defaultCtrl”, function ($scope, logService) {

$scope.data = {

cities: [“London”, “New York”, “Paris”],

totalClicks: 0

};

$scope.$watch(‘data.totalClicks’, function (newVal) {

logService.log(“Total click count: ” + newVal);

});

});

</script>

</head> <body ng-controller=”defaultCtrl”>

<div class=”well”>

<div class=”btn-group” tri-button

counter=”data.totalClicks” source=”data.cities”>

<button class=”btn btn-default”

ng-repeat=”city in data.cities”>

{{city}}

</button>

</div>

<h5>Total Clicks: {{data.totalClicks}}</h5>

</div>

</body>

</html>

I have added a script element to import the services.js file into the HTML document, which ensures that the service is available for use. After that, it is simply a matter of adding an argument to the factory function of the controller to declare its dependency on the service. The name of the argument must match the name used to create the service because AngularJS inspects the arguments of factory functions and uses them to perform dependency injection. That means you can define the argument in any order, but it does prevent you from picking your own argument names. I can also consume the service in my custom directive, as shown in Listing 18-8.

Listing 18-8. Consuming the Service in the directives.js File

angular.module(“customDirectives”, [“customServices”])

.directive(“triButton”, function (logService) {

return {

scope: { counter: “=counter” },

link: function (scope, element, attrs) {

element.on(“click”, function (event) {

logService.log(“Button click: ” + event.target.innerText);

scope.$apply(function () {

scope.counter++;

});

});

}

}

});

Once the dependencies on the module and the service have been declared, I call the logService.log method to access the simple functionality provided by the service. If you load the example HTML file into the browser and click the buttons, you will see output like this in the JavaScript console:

(LOG + 0) Total click count: 0

(LOG + 1) Button click: London

(lOG + 2) Total click count: 1

(lOG + 3) Button click: New York

(lOG + 4) Total click count: 2

You might be wondering why using a service is better than the original example where I called console.log directly. There are a couple of benefits. The first is that I can disable logging throughout the entire application by commenting out one line in the services.js file, rather than having to search through the application looking for console.log calls. This is not a big deal in my simple example application, but it is a big deal in a real project made up of many large and complex files.

The second benefit is that the consumers of the service have no insight into or dependency on its implementation. The controller and directive in this example know that there is a logService, and they know that it defines a method called log, but that’s all—and that means I can completely change the way that logging is performed without having to make any changes outside of the service object.

The final benefit is that I can isolate and test the logging functionality separately from the rest of the application, using the techniques I describe in Chapter 25.

In short, services let you build common functionality without breaking the MVC pattern—something that becomes increasingly important as your projects grow in scale and complexity. And, as you will learn, some important AngularJS features are provided through a set of built-in services.

2. Using the Service Method

The Module.service method also creates service objects, but in a slightly different way. When AngularJS needs to satisfy a dependency for a service defined by the factory method, it simply uses the object returned by the factory function, but for a service defined with the service method, AngularJS uses the object returned by the factory function as a constructor and uses the JavaScript new keyword to create the service object.

The new keyword isn’t widely used in JavaScript development, and when it is used, it causes a lot of confusion because most developers are familiar with the class-based inheritance used by languages such as C# and Java and not the prototype-based inheritance used by JavaScript. A demonstration will help explain what the new keyword does and how it is used by the Module.service method. In Listing 18-9, I have updated the contents of the services.js file to take advantage of the service method.

Listing 18-9. Using the service Method in the service.js File

var baseLogger = function () {

this.messageCount = 0; this.log = function (msg) {

console.log(this.msgType + “: ” + (this.messageCount++) + ” ” + msg);

}

};

var debugLogger = function () { };

debugLogger.prototype = new baseLogger();

debugLogger.prototype.msgType = “Debug”;

var errorLogger = function () { };

errorLogger.prototype = new baseLogger();

errorLogger.prototype.msgType = “Error”;

angular.module(“customServices”, [])

.service(“logService”, debugLogger)

.service(“errorService”, errorLogger);

The first thing I have done is create a constructor junction, which is essentially a template for defining functionality that will be defined on new objects. My constructor function is called baseLogger, and it defines the messageCount variable and the log method you saw in the previous section. The log method passes an undefined variable called msgType to the console.log method, which I’ll set when I use the baseLogger constructor function as a template.

The next step I take is to create a new constructor function called debugLogger and set its prototype to a new object created using the new keyword and the baseLogger keyword. The new keyword creates a new object and copies

the properties and functions defined by the constructor function to the new object. The prototype property is used to alter the template. I call it once to ensure that the debugLogger constructor inherits the property and method from the baseLogger constructor and again to define the msgType property.

The whole point of using constructors is that you can define functionality in the template once and then have it applied to multiple objects—and to that end, I have repeated the process to create a third constructor function called errorLogger. The use of the new keyword in both cases means that I define the messageCount property and the log method once but have it apply to both to objects that are created by the debugLogger and errorLogger constructors and the objects that are created from them. To finish the example, I register the debugLogger and errorLogger constructors as services, like this:

angular.module(“customServices”, [])

.service(“logService”, debugLogger)

.service(“errorService”, errorLogger);

Notice that I pass the constructors to the service method. AngularJS will call the new method to create the service objects. To test the new service, simply load the example.html file into the browser. I don’t need to make any changes to the controller or directive because AngularJS presents all service objects to consumers in the same way, hiding the details of how they were created. If you click the city buttons, you will see output like the following:

Debug: 0 Total click count: 0

Debug: 1 Button click: London

Debug: 2 Total click count: 1

Debug: 3 Button click: New York

Debug: 4 Total click count: 2

As I said, the new keyword isn’t widely used, prototype-based inheritance can be confusing, and I am just touching the surface of what’s possible. The advantage of this approach is that I defined my log method in one place but was able to use it in two services. The disadvantage is that the code is verbose and won’t be readily understood by many JavaScript programmers.

You don’t have to use prototypes with the service method. You can treat it as being equivalent to the factory method, and I recommend you do exactly that when you are new to AngularJS because you have enough to keep track of without remembering which methods use a specific technique to create service objects. In Listing 18-10, you can see how I have updated the services.js file to define a service with the service method, but without the use of the JavaScript prototype features.

Listing 18-10. Using the service Method Without Prototypes in the services.js File

angular.module(“customServices”, [])

.service(“logService”, function () {

return {

messageCount: 0, log: function (msg) {

console.log(“Debug: ” + (this.messageCount++) + ” ” + msg);

}

};

});

This isn’t as flexible, and AngularJS will still use the new keyword behind the scenes, but the overall effect is to allow the service method to be used as an interchangeable replacement for the factory method but with a more immediately meaningful name.

3. Using the Provider Method

The Module.provider method allows you to take more control over the way that a service object is created or configured. In Listing 18-11, you can see how I have updated my logging service so that it is defined using the provider method.

Listing 18-11. Using the provider Method to Define a Service in the services.js File

angular.module(“customServices”, [])

.provider(“logService”, function() {

return {

$get: function () {

return {

messageCount: 0, log: function (msg) {

console.log(“(LOG + ” + this.messageCount++ + “) ” + msg);

}

};

}

}

});

The arguments to the provider method are the name of the service that is being defined and a factory function. The factory function is required to return a provider object that defines a method called $get, which in turn is required to return the service object.

When the service is required, AngularJS calls the factory method to get the provider object and then calls the $get method to get the service object. Using the provider method doesn’t change the way that services are consumed, which means that I don’t need to make any changes to the controller or directive in the example. They continue to declare the dependence on the logService service and call the log method of the service object they are provided with.

The advantage of using the provider method is that you can add functionality to the provider method that can be used to configure the service object. This is best explained with an example, and in Listing 18-12 I have added a function to the provider object that controls whether the message counter is written out as part of the log message and another to control whether messages are written at all.

Listing 18-12. Adding Functions to the provider Object in the services.js File

angular.module(“customServices”, [])

.provider(“logService”, function () {

var counter = true;

var debug = true;

return {

messageCounterEnabled: function (setting) {

if (angular.isDefined(setting)) {

counter = setting; return this;

} else {

return counter;

}

},

debugEnabled: function(setting) {

if (angular.isDefined(setting)) {

debug = setting; return this;

} else {

return debug;

}

},

$get: function () {

return {

messageCount: 0, log: function (msg) {

if (debug) {

console.log(“(LOG”

+ (counter ? ” + ” + this.messageCount++ + “) ” : “) “)

+ msg);

}

}

};

}

}

});

I have defined two configuration variables, counter and debug, that are used to control the output from the log method. I expose these variables through two functions called messageCounterEnabled and debugEnabled, which I added to the provider object. The convention for provider objects methods is to allow them to be used to set the configuration when an argument is provided and query the configuration when there is no argument. When the configuration is set, the convention is to return the provider object as the result from the method in order to allow multiple configuration calls to be chained together.

AngularJS makes the provider object available for dependency injection, using the name of the service combined with the word Provider, so for the example the provider object can be obtained by declaring a dependency on logServiceProvider. The most common way to obtain and use the provider object is in a function passed to the Module.config method, which will be executed when AngularJS has loaded all of the modules in the application, as described in Chapter 9. In Listing 18-13, you can see how I have used the config method to obtain the provider object for the logging service and change the settings.

Listing 18-13. Configuring a Service via Its Provider in the example.html File

<script>

angular.module(“exampleApp”, [“customDirectives”, “customServices”])

.config(function (logServiceProvider) {

logServiceProvider.debugEnabled(true).messageCounterEnabled(false);

})

.controller(“defaultCtrl”, function ($scope, logService) {

$scope.data = {

cities: [“London”, “New York”, “Paris”],

totalClicks: 0

};

$scope.$watch(‘data.totalClicks’, function (newVal) {

logService.log(“Total click count: ” + newVal);

});

});

</script>

You don’t have to configure services using the Module.config method, but it is sensible to do so. Remember that service objects are singletons and any changes you make once the application has started will affect all of the components that are consuming the service—something that often causes unexpected behaviors.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *