Introduction
Probably because there are so many ways to create services and differences in the various techniques can appear subtle, developers sometimes get confused by the various options. We’ll look at all the different ways we can create our own services in Angular.
Creating our own services is great, but we also need to understand how to properly use them.
Throughout the post, we’ll be adding features to a very simple app I’ve named Book Logger. The idea behind this is that it allows students to be able to use to log what books they read, and for how long they read them. We’ll add enough features to allow the adding and editing of books and the viewing of basic information about the readers in the app. I’ve tried to keep the app and the code required to run it as simple as possible. The one requirement is that you have NodeJS installed on your machine.
Why Services?
Services are a large a part of an Angular application. Constructing robust Angular services and taking full advantage of the various built-in services are vital abilities for any Angular Developer.
Developing super Angular apps means making the maximum of the built-in services. It means to take an app from simplistic to state-of-the-art.
The framework consists of lots of brilliant capability to assist us with networking, caching, logging, promises, and plenty extra. Writing our very own services is a great way to put in force reusable code and a good way to capture the logic unique to our application. Organizing our code into services leads to cleaner, better-defined components, which means we finish our project with more maintainable code.
Services Overview
There are five functions that are used to create an Angular service. The most fundamental service creation function is the provider function. Creating services with it allows us to explicitly create a configurable provider object.
The provider knows how to create the resulting service. Three of the remaining four functions all internally call the provider function. The first of those wrapper functions is the factory function. It’s a very easy to use wrapper around provider, and will probably become our primary service creation function.
The next service creation function is actually named service and is just a simple wrapper around the factory function. When we call the service function, it will internally call the factory function, which will then call the provider function. They are each small abstraction on top of one another.
Similarly, the fourth function i.e. the value function is just a thin wrapper around the factory function. We use it in much the same way we use the constant service which is the fifth function available for service creation. Value and constant look similar, but actually behave fundamentally different in a couple of important ways. The constant function is the one function in the list that’s quite different from all the rest. It doesn’t call the factory function or even a provider function. It is its own unique kind of service. It’s very simple but quite useful.
Provider Function
Services are designed to be injected into the other components in the application, therefore, their creation is very much a part of Angular’s dependency injection system. Objects that know how to create injectable services are known as providers, so before a new service can be created there must first be a provider that knows how to create that service. It’s the $provide service that we use to do that. It’s one of the services that ships with Angular as denoted by the leading $ in its name. It has several methods that register components with the Angular injector. Once registered, the injector knows how to find the correct instance and pass it as a parameter to other components needing it. All of that largely happens behind the scenes once we have created a service using one of the five methods discussed in this module. The basic process is that the $provide service creates a provider which contains a function that is used to create a service. The first function on the &provide service we’ll look at is the provider function. It’s the most fundamental way to create a service. All of the other methods of creating services which we’ll see are just wrappers around the provider function. The constant function, again, is the one exception. To use the provider function directly, we simply call the function and pass it a name and a function that will define the underlying provider. The provider created for each service, regardless of the technique we use, will be given a name that is the name we specify for the service with the word provider appended to it. So in this example, the name of the service we will ultimately inject into our other components, will be ‘books’, and the name of the provider will be booksProvider. The number one rule for using a provider function is that the function we pass to it must contain a property named $get. The function assigned to that property is the function that will be called by Angular to create our service. The service will then be represented by the return value of that function. Creating services with the provider function is a little more complicated than the other techniques we’ll look at. The benefit it offers in exchange for that added complexity is the ability to configure the underlying provider for our service. None of the other service creation functions allow us to do this. Let’s see how to create and configure a service with the provider function in a demo.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
var app = angular.module('app', []); | |
app.config(function($provide)){ | |
$provide.provider('books',function(){ | |
this.$get=function(){ | |
var appName = 'Book Logger'; | |
var appDesc = 'Track which books you read'; | |
return { | |
appName: appName, | |
appDesc: appDesc | |
}; | |
}; | |
}); | |
}); | |
}()); |
Factory Function
Using the factory function on the provide service is usually a much simpler option than using provider, if we don’t need to configure the underlying provider object. All it really does is call the provider function and assign the function we pass to the factory function as the value of the get property on the provider.
To use the factory function, we just pass it a name as the first parameter, like we did with the provider function, and the second parameter is a function that will return an object that represents the service instance. If we don’t need to configure the provider, like we did in the last demo, then using the factory function will be a much simpler and readable way to create our services.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
angular.module('app') | |
.factory('dataService', dataService); | |
function dataService(logger) { | |
return { | |
getAllBooks: getAllBooks, | |
getAllReaders: getAllReaders | |
}; | |
function getAllBooks() { | |
logger.output('getting all books'); | |
return [ | |
{ | |
book_id: 1, | |
title: 'Harry Potter and the Deathly Hallows', | |
author: 'J.K. Rowling', | |
yearPublished: 2000 | |
}, | |
{ | |
book_id: 2, | |
title: 'The Cat in the Hat', | |
author: 'Dr. Seuss', | |
yearPublished: 1957 | |
}, | |
{ | |
book_id: 3, | |
title: 'Encyclopedia Brown, Boy Detective', | |
author: 'Donald J. Sobol', | |
yearPublished: 1963 | |
} | |
]; | |
} | |
function getAllReaders() { | |
logger.output('getting all readers'); | |
return [ | |
{ | |
reader_id: 1, | |
name: 'Marie', | |
weeklyReadingGoal: 315, | |
totalMinutesRead: 5600 | |
}, | |
{ | |
reader_id: 2, | |
name: 'Daniel', | |
weeklyReadingGoal: 210, | |
totalMinutesRead: 3000 | |
}, | |
{ | |
reader_id: 3, | |
name: 'Lanier', | |
weeklyReadingGoal: 140, | |
totalMinutesRead: 600 | |
} | |
]; | |
} | |
} | |
dataService.$inject = ['logger']; | |
}()); |
Service Function
It’s just a thin wrapper around the factory function. The only difference is that the function we passed to the service method will be treated as a constructor function and called with the JavaScript “new” operator.
It uses the instantiate method of the injector to call the function we pass to the service method. The instantiate method will then use the “new” operator to execute the function that creates the service. We would use the service method instead of the factory method if we specifically needed our function to be treated as a constructor and called with the “new” operator. There are several reasons why we may want that behavior. One is if we have defined an inheritance hierarchy in our code. Creating an instance with “new” will make sure that our instantiated object properly inherits from its prototypes. Let’s now go add two new services using the factory and service functions to the bookLogger app.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
angular.module('app') | |
.service('logger', BookAppLogger); | |
function LoggerBase() { } | |
LoggerBase.prototype.output = function(message) { | |
console.log('LoggerBase: ' + message); | |
}; | |
function BookAppLogger() { | |
LoggerBase.call(this); | |
this.logBook = function(book) { | |
console.log('Book: ' + book.title); | |
} | |
} | |
BookAppLogger.prototype = Object.create(LoggerBase.prototype); | |
}()); |
Value and Constant Functions
The two remaining service types are value and constant services. They look alike and are the two simplest types of services. The syntax for both types of services is very similar and straightforward.
The value function is just shorthand for calling the factory function with no parameters. If we don’t need to inject anything into the factory function, we can use the value function instead.
The constant function is not a shorthand version of anything. It allows us to register an object literal function or some other constant value with the injector, but it doesn’t call the service, factory or provider methods behind the scenes.
A value service can’t be injected into a module configuration function, but a constant service can.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function () { | |
angular.module('app') | |
.value('badgeService', { | |
retrieveBadge: retrieveBadge | |
}); | |
function retrieveBadge(minutesRead) { | |
var badge = null; | |
switch (true) { | |
case (minutesRead > 5000): | |
badge = 'Book Worm'; | |
break; | |
case (minutesRead > 2500): | |
badge = 'Page Turner'; | |
break; | |
default: | |
badge = 'Getting Started'; | |
} | |
return badge; | |
} | |
}()); |
The final difference is related to decorators, which we’ll cover in the last module of the course. Decorators are a way to modify or override the behavior of an existing service.
Value services can be overridden by decorators, but constant services can’t as we wouldn’t be able to change the behavior of something created as a constant.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function () { | |
angular.module('app') | |
.constant('constants', { | |
APP_TITLE: 'Book Logger', | |
APP_DESCRIPTION: 'Track which books you read.', | |
APP_VERSION: '1.0' | |
}); | |
}()); |
So basically, with regard to Angular Services there are different ways we can create our own services. Some of them may look similar, but there are very good reasons for using each of them. Understanding how and why to use each of the available techniques will allow us to implement the best design possible in your application. I think it’s helpful to think of these functions, and really any API, like a set of tools. Before we can choose the best one for the job, we need to understand all of our tools and how to use them.