Services, providers, and factories are all just fancy ways of saying "library". A broad description of a library would be some collection of behavior that is reusable across multiple codebases. Services provide this by acting as an instantiated, singleton object, likely full of many instance methods. Factories can provide a new object every time, perhaps full of functions, or perhaps just a single function, or even object. Providers act much like factories, but they are configurable before AngularJS hits its "run" phase.
Controllers, filters, directives etc., can all use these via DI (dependency injection). You can share functions, models, singleton objects, classes (or just about anything) between disparate components. These controllers, filters, and directives can be considered the codebases your libraries provide behavior to.
You must understand the differences between services, factories, and providers. You must also understand when to use them. Here's a table to oversimplify the situation:
Component | Instantiated | Needs a $get method | Explicitly returns something | Present during config() |
Service | Yes | No | No | No |
Factory | No | No | Yes | No |
Provider | Yes | Yes | No | Yes |
Our blog could also use a service, Posts, to handle our data. Here it is:
// Define our first service; Posts
// This service simply uses the built-in $http service to retrieve data from
// a static JSON store.
blog.service('Posts', function ($http) {
this.getPosts = function () {
return $http.get('data.json');
};
});
Within the Posts service, there's a getPosts()
method which returns a promise from an $http call. We could have just put the $http call within the controller itself, but it's good practice to put potentially reusable code into a service, since controllers cannot talk to one another unless they share the same scope. Now, any controller that injects Posts into itself can use the method here; we don't have to write separate $http calls.
Of note, $http itself is a service. It's built in to AngularJS, and is one of a number of prerolled services for common functionality. We will discuss Ajax further in a bit.
I don't know. I can tell you, though, that $http does not care about what backend it talks to and treats them all the same, so if you are having problems using $http when migrating from say, a PHP server to a Python server, the problem is not within AngularJS. The nice folks on Freenode's #python can help you. Just don't ask to ask. My first guess would be to check your headers and make sure your server is getting what it expects.
Here's what the three look like:
// Service
some_module.service('SomeService', function() {
this.someMethod = function() { ... };
});
// Factory
some_module.factory('SomeFactory', function() {
return {
someMethod: function() { ... }
};
});
// Provider
some_module.provider('SomeProvider', function() {
// some setup here
// required method $get; acts as factory
this.$get = function() {
return {
someMethod: function() { ... }
}
};
});
Services are singletons. That is to say, you only get one, ever. Every time you inject a service into your controller or wherever, you get the same object. In my experience, I try to use a service first, then consider the other two. The reason being is that a service simply takes up less memory and processing power than the others. It is also probably the most dead-simple to write since you can stuff everything into the service's this and don't have to return anything. Think of the service as a constructor function that gets called only once, and the resulting object can be used just about anywhere.
When what you want to provide is not an object--perhaps it's just a function, or a class definition--use a factory. If you want to have new objects each time they are injected, you would not use a service. Say you had a filter-like function that you wanted to provide to your controllers, but you know you'd never use it in a view, and you don't want to murk up some namespace by declaring it outside of AngularJS' context. Factories to the rescue!
var someFunction = function() { ... };
// becomes...
some_module.factory('someFunction', function() { // injectable too!
return function() { ... };
});
I find this pattern to be extremely handy, and it keeps my namespaces pristine. Don't make that global function!!! Put it in a factory. Here's a simple example of creating and using a class, which we'll use in our blog. This class will describe a blog post; a Post object. We'll need to define our factory, then update our controller to use it:
// returns a class Post which represents a blog post
blog.factory('Post', function () {
// set date, author, and temporary title and body
// date can be a Date object or a JS timestamp or anything else Date objects accept
var Post = function (title, body, date, author, autosave) {
this.date = new Date(date);
this.author = author;
if (autosave) {
this.title = title;
this.body = body;
this.temp = {};
} else {
this.temp = {title: title, body: body};
}
};
// update the date to NOW
Post.prototype.updateDate = function () {
this.date = new Date(); // js Date object
};
// save the temp info to the real info, then update the date to now.
Post.prototype.save = function () {
this.title = this.temp.title;
this.body = this.temp.body;
this.updateDate();
this.stopEditing();
};
// prepares this post for editing
Post.prototype.beginEditing = function () {
this.editing = true;
this.temp.title = this.title;
this.temp.body = this.body;
};
// takes the post out of 'editing' mode
Post.prototype.stopEditing = function () {
this.editing = false;
this.temp = {};
};
// handiness to convert this Post to a string, for debugging purposes
Post.prototype.toString = function() {
return angular.toJson(this);
};
return Post;
});
In this Post pseudoclass, we've created a constructor that will set up our object. We created an updateDate()
function, which sets the "date" property of the Post
object to be now. We have a save()
function that replaces much of the "save" functionality from our previous version of ContentCtrl
. Our stopEditing()
function simply flips the "editing" flag and resets our temporary object. Finally, if we want to view a string representation of our Post
object (using toString()
), it's pretty handy to simply dump a JSON representation of it.
What follows is a controller actually using this service.
// Controls the blog content including all methods related to posts
blog.controller('ContentCtrl', function ($scope, Posts, Post) {
// util function to stop editing all of the posts
$scope._stopEditingAllPosts = function (posts) {
var i;
if (angular.isArray(posts)) {
i = posts.length;
while (i--) {
posts[i].stopEditing();
}
}
};
// Retrieve the posts from our "server". If this succeeds, we'll
// put the posts into our $scope and do some post-processing.
Posts.getPosts().success(function (data) {
var posts = data.posts, i, post;
$scope.posts = [];
// Create Post objects and put them into the list of posts.
i = posts.length;
while (i--) {
post = posts[i];
// convert to millisecond precision
$scope.posts.push(new Post(post.title, post.body, post.date * 1000,
post.author, true));
}
});
// This closes all editors if we happen to click the "new post" button
$scope.$watch('posts', function (new_val, old_val) {
if (new_val !== old_val) {
$scope._stopEditingAllPosts(old_val);
}
});
// Begins editing a post by making a temporary copy of the title and body,
// and setting the "editing" flag for that post to be true.
$scope.edit = function (post) {
$scope._stopEditingAllPosts($scope.posts);
post.beginEditing();
};
// Saves a post, sets its editing flag to false, puts it in the list of posts
// and eliminates the 'new_post' from the scope.
$scope.save = function (post) {
post.save();
$scope.posts.unshift(post);
delete $scope.new_post;
};
// Cancels a post edit. Does not copy temp data, and sets the editing flag
// to false. In addition we set the controller-wide "new_post" model to be
// false, so the "New Post" button appears.
$scope.cancel = function (post) {
post.stopEditing();
delete $scope.new_post;
};
// instantiate new Post object provided by Post factory
$scope.newPost = function () {
$scope._stopEditingAllPosts($scope.posts);
$scope.new_post = new Post('Enter Title Here', 'Enter Body Here', undefined, 'me');
$scope.new_post.beginEditing();
};
});
Procedurally, what this controller does, is ask the Posts service for posts whenever it is instantiated. The controller is instantiated when the ngController
directive is encountered, compiled and linked. So if you had two ContentCtrl
s hanging out in your markup, the request for Posts would be made twice.
The functions we place into the scope are mainly straightforward. However, we do have a $watch
that detects if the posts
object has changed. If it has, that means we've gotten new data from the server. And if we've got new data from the server, then we want to stop editing stuff (throw it away) because we may have new stuff to edit. A smart system would detect collisions, but we're the only person writing on the blog and this is not necessary.
What follows are unit tests for ContentCtrl
:
describe('ContentCtrl', function () {
describe('basic methods', function () {
// all this stuff happens whenever we instantiate the controller
beforeEach(inject(function (Posts, Post, $routeParams, $httpBackend, $http) {
$http.defaults.transformRequest = []; // remove any fancy request transforms
$httpBackend.expectGET('data.json').respond({posts: []});
$controller('ContentCtrl', {$scope: scope, Posts: Posts,
Post: Post, $routeParams: $routeParams});
$httpBackend.flush();
}));
describe('_stopEditingAllPosts', function () {
it('should call stopEditing() on all members of posts array',
inject(function (Post) {
var posts = [
new Post(),
new Post(),
new Post()
];
spyOn(Post.prototype, 'stopEditing');
scope._stopEditingAllPosts(posts);
expect(Post.prototype.stopEditing).toHaveBeenCalled();
expect(Post.prototype.stopEditing.calls.length).toBe(3);
}));
});
describe('edit', function () {
it('should call _stopEditingAllPosts', inject(function (Post) {
var post = new Post();
spyOn(scope, '_stopEditingAllPosts');
scope.edit(post);
expect(scope._stopEditingAllPosts).toHaveBeenCalled();
}));
it('should call Post.prototype.beginEditing', inject(function (Post) {
var post = new Post();
spyOn(Post.prototype, 'beginEditing');
scope.edit(post);
expect(Post.prototype.beginEditing).toHaveBeenCalled();
}));
});
describe('save', function () {
it('should call save on the post', inject(function (Post) {
var post = new Post();
spyOn(Post.prototype, 'save');
scope.save(post);
expect(Post.prototype.save).toHaveBeenCalled();
}));
it('should prepend to list of posts', inject(function (Post) {
var post = new Post();
scope.$apply(function () {
scope.save(post);
});
expect(scope.posts.length).toBe(1);
expect(scope.posts[0]).toBe(post);
}));
it('should delete new_post', inject(function (Post) {
var post = new Post();
spyOn(Post.prototype, 'save');
scope.$apply('new_post = "foo"');
scope.$apply(function () {
scope.save(post);
});
expect(scope.new_post).toBeUndefined();
}));
});
describe('cancel', function () {
it('should call Post.prototype.stopEditing', inject(function (Post) {
var post = new Post();
spyOn(Post.prototype, 'stopEditing');
scope.$apply(function () {
scope.cancel(post);
});
expect(Post.prototype.stopEditing).toHaveBeenCalled();
}));
it('should delete new_post', inject(function (Post) {
var post = new Post();
spyOn(Post.prototype, 'save');
scope.$apply('new_post = "foo"');
scope.$apply(function () {
scope.cancel(post);
});
expect(scope.new_post).toBeUndefined();
}));
});
describe('newPost', function() {
it('should call _stopEditingAllPosts', function() {
spyOn(scope, '_stopEditingAllPosts');
scope.newPost();
expect(scope._stopEditingAllPosts).toHaveBeenCalled();
});
it('should create a new post', inject(function(Post) {
spyOn(Post.prototype, 'beginEditing'); // blasts our data
scope.$apply(function() {
scope.newPost();
});
expect(scope.new_post.temp.title).toBe('Enter Title Here');
expect(scope.new_post.temp.body).toBe('Enter Body Here');
expect(scope.new_post.author).toBe('me');
}));
it('should call Post.prototype.beginEditing', inject(function(Post) {
spyOn(Post.prototype, 'beginEditing'); // blasts our data
scope.$apply(function() {
scope.newPost();
});
expect(Post.prototype.beginEditing).toHaveBeenCalled();
}));
});
});
Nine times out of ten, you are going to want to use a service or a factory. But what if you have a service and need to configure it before it's instantiated? This is when you use a provider.
Use a provider if you need to configure a service before it runs. This is the only reason you'd ever use a provider, because they are the most complex of the three to use. For example, it's common to want to use $httpProvider
to set up default headers before the $http service is instantiated.
But what mainly brings us here is the fact that you cannot do this:
some_module.service('SomeService', function() {
this.someMethod = function() { ... };
});
some_module.config(function(SomeService) {
// do something with SomeService
});
SomeService
has not yet been instantiated and it simply doesn't exist. But you CAN do this:
function SomeProvider() {
// some setup code goes here
this.last_name = 'Hiller';
// required method $get; acts as factory or service
this.$get = function() {
var that = this;
return {
someMethod: function() { return that.first_name + ' ' + that.last_name }
};
};
}
some_module.provider('SomeProvider', SomeProvider)
// YES, it tacks "Provider" on to the end of any provider you create.
some_module.config(function(SomeProviderProvider) {
SomeProviderProvider.first_name = 'Chris';
});
// run() just runs arbitrary code immediately after bootstrapping
some_module.run(function(SomeProvider) {
SomeProvider.someMethod(); // returns 'Chris'
});
And some unit tests showing this works:
describe('SomeProvider', function () {
it('should be injectable', inject(function (SomeProvider) {
expect(SomeProvider).not.toBeUndefined();
}));
it('should return configured name from someMethod', inject(function (SomeProvider) {
expect(SomeProvider.someMethod()).toBe('Chris Hiller');
}));
it('should provide a provider', function () {
module(function (SomeProviderProvider) {
expect(SomeProviderProvider.last_name).toBe('Hiller');
});
});
});
The $get()
function is required; it is called to retrieve the SomeProvider service _after_ config()
has been called. When using the provider as a provider, if you have a provider named "Foo", you will inject "FooProvider", which is automatically created for you. It's like two services in one! Note that you cannot inject SomeProviderProvider into anything after bootstrapping has finished. This means you must only use your provider ("FooProvider") in the config()
function, before the services have initialized.
A real-world example of a useful provider would be a generic "dialog" or "pop-up" provider ("Dialog" or "DialogProvider"), which you could configure during the config()
stage to use any number of different implementations. For example, you may want to tell "DialogProvider" to use jQuery UI, or Bootstrap, or some hand-rolled thing. That way, each module or application using the provider (living in some common module) will be able to declare what implementation to use, and the service ("Dialog") will act accordingly. You should probably even allow this service to be configurable on-the-fly if, for some reason, you need to use a Bootstrap modal here and a jQuery UI dialog there (ew).
Unit testing factories, services and providers is incredibly easy. Let's write tests for our Posts service and our Post factory:
describe('Blog Services', function () {
var Posts, httpBackend;
// I want to use the variable name Posts for my service, but if I use DI here to inject it
// the variable name will already be taken! A fun way around this limitation isto inject the $injector service,
// then simply ask it for what you want.
beforeEach(inject(function ($injector, $httpBackend) {
Posts = $injector.get('Posts');
httpBackend = $httpBackend;
}));
describe('Posts', function () {
it('should initiate the http call', function () {
var promise;
// you will need to use expectGET, expectPOST, etc., when unit testing functions
// that use the $http service. They are assertions about what call you are making and allow
// you to mock a response easily.
httpBackend.expectGET('data.json').respond('foo');
promise = Posts.getPosts();
// make sure our promise is really a then-able object
expect(angular.isFunction(promise.then)).toBeTruthy();
promise.then(function (res) {
expect(res.data).toBe('foo');
});
httpBackend.flush();
});
});
});
Next is a relatively long bit of testing code to assert all of our Post class' methods are functioning as expected:
describe('Blog Factories', function () {
var Post;
beforeEach(inject(function ($injector) {
Post = $injector.get('Post');
}));
describe('Post', function () {
// covers all of the constructor's behavior, which is very little.
describe('the constructor', function () {
it('should store information temporarily when autosave flag is not truthy', function () {
var post = new Post('cashews', 'peanuts', new Date(), 'Mr. Almond');
expect(post.temp.title).toBe('cashews');
expect(post.temp.body).toBe('peanuts');
expect(post.title).toBeUndefined();
expect(post.body).toBeUndefined();
expect(post.date).not.toBeUndefined();
expect(post.author).toBe('Mr. Almond');
});
it('should not store information temporarily when autosave flag is truthy', function () {
var post = new Post('cashews', 'peanuts', new Date(), 'Mr. Almond', true);
expect(post.title).toBe('cashews');
expect(post.body).toBe('peanuts');
expect(post.temp.title).toBeUndefined();
expect(post.temp.body).toBeUndefined();
expect(post.date).not.toBeUndefined();
expect(post.author).toBe('Mr. Almond');
});
});
describe('the updateDate method', function () {
it('should reset the date', function () {
var old_date, post = new Post();
old_date = post.toString();
post.updateDate();
// it would be kind of weird/difficult to try to assert it updates the date object
// to NOW; so just assert it changed.
expect(old_date).not.toBe(post.date.toString());
});
});
describe('the save method', function () {
it('should copy information from temp object', function () {
var post = new Post('foo', 'bar');
post.save();
expect(post.title).toBe('foo');
expect(post.body).toBe('bar');
});
it('should call updateDate method', function() {
var post = new Post();
// we just need to assert that save() happens to call the updateDate() method
spyOn(post, 'updateDate');
post.save();
expect(post.updateDate).toHaveBeenCalled();
});
it('should call the stopEditing method', function() {
var post = new Post();
spyOn(post, 'stopEditing');
post.save();
expect(post.stopEditing).toHaveBeenCalled();
});
});
describe('the beginEditing method', function() {
it('should rearrange some data in the post', function() {
var post = new Post('foo', 'bar', new Date(), 'baz', true);
post.beginEditing();
// we assert the state changes and the information has been copied out of temp
expect(post.editing).toBeTruthy();
expect(post.temp.title).toBe(post.title);
expect(post.temp.body).toBe(post.body);
});
});
describe('the stopEditing method', function() {
it('should set editing flag to false and empty temp object', function() {
var post = new Post('foo', 'bar', new Date(), 'baz', true);
post.beginEditing();
post.stopEditing();
expect(post.editing).toBeFalsy();
// note that you cannot use "toBe" here, because {} !== {}
expect(post.temp).toEqual({});
});
});
describe('toString method', function() {
it('should return a JSON string', function() {
var post = new Post('foo', 'bar', new Date(), 'baz', true);
expect(post.toString()).toBe(angular.toJson(post));
});
});
});
});
These services, providers and factories make up the backbone (no pun intended) of your AngularJS app. Do the heavy lifting here; make it reusable and testable. You'll end up with less spaghetti on your plate. Keep your controller from thinking too hard. Your directive only has one function in it--make that function readable by injecting services into it.
In the next chapter, we'll take a look at the runt of the litter, filters.
There has been error in communication with Booktype server. Not sure right now where is the problem.
You should refresh this page.