What, if anything, does it have to do with the factory pattern?
Or if nothing, then why is it called a factory? It seems more like a singleton to me.
My guess is that a factory can be used to create factory functions
app.factory('MyFactory', function() {
// The returned function is available by injecting MyFactory
// into other components
return function() {
return 'Something created by the factory';
};
});
This can be used, for example, in a controller
app.controller('MyController', function(MyFactory) {
var myObj = MyFactory();
// myObj is 'Something created by the factory'
});
(You can do something like this using service as well. I think you just have to live with the fact there is almost complete overlap between what you can do with factory and service)
Related
I have a controller that calls a Service which is a wrapper for a Resource. Like this:
app.factory("Service", ["$resource", function ($resource) {
return $resource("/api/Service/get");
}]);
Return value of the service's method is assigned to a variable within the controller. Normally, the variable is of type Resource and it contains a promise. When the promise is resolved, the variable is populated with all values returned from the backend.
I track then on the promise in order to modify the model received from the backend. Like so:
this.model = Service.get();
this.model.$promise.then(function(data) {
// do something with data
});
I need to test the value of the resulting model variable in my controller.
The only way I found to do this, is to use $httpBackend with a real implementation of my Service. However, this is ugly because then, testing my controller, I have to pass request path "api/Service/get" to the httpBackend.when() in order for it to respond with some value.
An excerpt form my test:
// call Controller
$httpBackend.when('GET', '/api/Service/get').respond(someData);
$httpBackend.flush();
expect(scope.model.property).toBe(null);
This seems and feels utterly wrong. The whole point of using a separate service to deal with resource is for the controller to not know anything about the url and http method name. So what should I do?
In other words, what I want to test is that then gets called and does what I need it to do.
I guess I could probably create a separate service that gets called in then and do what I need to do with the model but it feels a bit overkill if all I want to do is, for example, set one field to null depending on a simple condition.
You are correct, you shouldn't have to use $httpBackend unless you are using $http in the controller you are testing.
As you wrote, the controller shouldn't need to know anything about the implementation of Service. What the controller knows is that Service has a get method that returns an object with a $promise property that is a promise.
What you want to do is to use a fake implementation of Service in your test. There are multiple ways to do this via mocks, spies, stubs etc, depending on your use case and which testing framework(s) you are using.
One way is to create a fake implementation like this:
var Service = {
get: function() {
deferred = $q.defer();
return {
$promise: deferred.promise
};
}
};
You want to be able to access deferred from the tests, so you can either resolve or reject the promise based on what you want to test.
Full setup:
var $rootScope,
scope,
createController,
$q,
deferred;
var Service = {
get: function() {
deferred = $q.defer();
return {
$promise: deferred.promise
};
}
};
beforeEach(function() {
module('App');
inject(function(_$rootScope_, $controller, _$q_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
createController = function() {
$controller('MyController', {
$scope: scope,
Service: Service
});
};
$q = _$q_;
});
});
Controller implementation:
app.controller('MyController', function($scope, Service) {
$scope.property = false;
$scope.model = Service.get();
$scope.model.$promise.then(function(data) {
if (data) {
$scope.property = true;
}
});
});
You can then spy on the fake implementation to assert that it is called correctly.
Example with Jasmine:
spyOn(Service, 'get').and.callThrough();
You need and.callThrough() or the call will be interrupted and your fake implementation will not be used.
You now have full control by manually creating the controller, resolving the promise and triggering the digest loop and can test the different states:
it('Should work', function() {
spyOn(Service, 'get').and.callThrough();
expect(Service.get).not.toHaveBeenCalled();
createController();
expect(Service.get).toHaveBeenCalled();
expect(scope.property).toBeFalsy();
deferred.resolve('some data');
$rootScope.$digest();
expect(scope.property).toBeTruthy();
});
Demo: http://plnkr.co/edit/th2pLWdVa8AZWOyecWOF?p=preview
I have an AngularJS service which uses it's own methods (bad practice?). When I go to test this, and mock out the internally used method, I get conflicting context.
Here's an example service
angular.module('myModule', []).service('cardWarsService', function() {
return {floopThePig:floopThePig,
winAtCardWars:winAtCardWars};
function winAtCardWars(cards){
....
floopThePig(pigCard)
....
};
function floopThePig(card){
// flooping happens here
}
});
However when I try to mock out floopThePig I get told it's not called. This is how I'm mocking it.
spyOn(cardWarsService, 'floopThePig').and.callThrough();
If I change the call in the service to use this or with var self = this then it works in the test frame work but not in the application.
angular.module('myModule', []).service('cardWarsService', function() {
var self = this;
return {floopThePig:floopThePig,
winAtCardWars:winAtCardWars};
function winAtCardWars(cards){
....
self.floopThePig(pigCard)
....
};
function floopThePig(card){
// flooping happens here
}
});
Place your return object as a stand alone variable.
The issue is that the methods on either side of your return object are different. Your test only mocks the interface. As your application is not using the interface the spy doesn't pick up that it's being used.
This will guarantee our service will work in and out of the test framework
angular.module('myModule', []).service('cardWarsService', function() {
var self = {floopThePig:floopThePig,
winAtCardWars:winAtCardWars};
return self;
function winAtCardWars(cards){
....
floopThePig(pigCard)
....
};
function floopThePig(card){
// flooping happens here
}
});
Lets say I have a service which queries some data and sets it in the controller, a little similar to:
(Method on controller)
DogService.query(function(data)){
if(data.isSuccess){
$scope.IloveDogs = true;
$scope.dogLovers += 1;
}
})
It is highly simplified, but how would I in my controller test that when calling a mocked dogService, that it sets the correct data?
If for simplicity we say that the function isn't asynchronous and deals with promises, I would create and inject a mock to the controller. The mock could look like:
var DogService = {
query: function(){
return true;
}
}
This unfortunately doesn't run the code where the $scope.IloveDogs is set to true, and the dogLovers is incremented by one.
Any ideas, since I would rather not have to duplicate the code in my controller from the service to the mocked service?
This is how I would normally mock a service in a unit test.
(You didn't mention which testing framework you use, so I am going to assume Jasmine as it's the most popular one at the moment).
I just create a dumb object to act as my mock and then just Jasmine's built-in spy functionality to dictate what it returns. Note that this is syntax for Jasmine 2.0.
I use $q to create a promise, and make sure I am able to reference it from my tests so I can resolve it.
describe('Spec', function() {
var scope;
var catServiceMock;
var deferredCatCall;
beforeEach(module('myModule'));
beforeEach(inject(function($controller, $rootScope, $q) {
scope = $rootScope;
//Create a mock and spy on it to return a promise
deferredCatCall = $q.defer();
catServiceMock = {
query: function() {}
};
spyOn(catServiceMock, 'query').and.returnValue(deferredCatCall.promise);
//Inject the mock into the controller
$controller('MyCtrl', {
$scope: scope,
catService: catServiceMock
});
}));
it('proves that cats are better than dogs', function() {
//resolve the promise that was returned by the mock
deferredCatCall.resolve({
isSuccess: true
});
//Need to trigger a $digest loop so angular process the resolved promise
scope.$digest();
//Check that the controller callback did something
expect(scope.iLoveCats).toBeTruthy();
});
});
For a service that does not use promises, I would possibly do something like this:
describe('Spec', function() {
var scope;
var catServiceMock;
beforeEach(module('myModule'));
beforeEach(inject(function($controller, $rootScope, $q) {
scope = $rootScope;
//Create a mock and spy on it to return a value
catServiceMock = {
query: function() {}
};
spyOn(catServiceMock, 'query').and.returnValue({
isSuccess: true
});
//Inject the mock into the controller
$controller('MyCtrl', {
$scope: scope,
catService: catServiceMock
});
}));
it('proves that cats are better than dogs', function() {
//Check that the controller callback did something
expect(scope.iLoveCats).toBeTruthy();
});
});
The main problem with this approach is that you're forced to dictate what the service will return before you instantiate the controller. This means that if you want to test how the controller behaves to different data received from the service you're going to have to have multiple beforeEach blocks nested in different describe blocks and while it looks at a glance like it's less boilerplate in the test you will end up with a lot more.
This is one of the reasons why I prefer my services to return promises even if they are not asynchronous.
I wonder if I can call controller method from service.
I know that Service is singleton and I can't inject $scope to the service.
In my case I manage Google Maps in service and want to open modal Dialog when user right clicks on Polygon.
As I know, to open/create new instance of dialog, somehow Service must notify controller to do that.
This is a template with controller + method and service: Template
var myApp = angular.module('myApp', []);
function MyCtrl($scope, gridService, $timeout) {
// how to call "foo" method from service?
$scope.foo = function(){
alert('called from service');
};
}
myApp.service('gridService', ['$timeout', function ( $timeout) {
var grid = {
fetching: false,
pristine: true,
pageType: 'Edit'
}
return {
gridSetup: function () {
return grid;
},
setGridSetup: function (newGrid) {
}
}
}]);
Thanks,
The answer is simple: you don't.
The service exists to manipulate data, nothing else. And it really shouldn't care "why" it's doing what it's doing. You ask the service to do something and wait for a response.
Personally I prefer using promises to resolve async operations (i.e. to notify the controller about a state change) since it's heavily supported by many angular services like $http.
But feel free to use callbacks of you wish.
Usually you do not need to call controller from the service - in general the single service could be used by any controller so service shall know nothing about them. In most cases controller calls to the service in react to some user's action and then you need to update view from controller somehow when service did its work (get response from server, etc.). I see following general ways how to do it.
1. Use callbacks.
//controller
$scope.onButtonClick = function() {
$scope.label = "wait, loading...";
function onSuccess(result) {
$scope.label = "done! result is " + result;
}
myService.doSomeWork(param1, param2, onSuccess);
}
//service
doSomeWork: function(param1, param2, onSuccess) {
$.get({...}, onSuccess);
}
So you provide a callback for each action.
2. Subscribe on events
You may use jQuery for events subscribing/triggering
//controller
$(myService).on('update', function() {
$scope.lastUpdateTime = new Date();
});
$scope.onButtonClick = function() {
myService.doUpdate();
}
//service
doUpdate: function() {
$.get({...}, function onOk() {
$(this).trigger('update');
});
}
3. Use promises
A lot of built-in angular services return promise objects, you may use them too:
//controller
myService.doSomething(param1).then(function(result) {
$scope.result = result;
});
//service
doSomething: function(param1) {
return $http.get({...});
}
4. Share some data
An example is $resource service - for example when you call query method it returns empty array-like object that could be safely put to scope and then fills it with values when http request is done.
//controller
$scope.options = myService.options;
$scope.onClick = function() { myService.update() }
//service
options: [],
update: function() {
var self = this;
$http.get({...}).success(function(res) {
self.options.splice(0, self.options.length); //to keep same array
self.options.push.apply(self.options, res.data.options);
});
}
In all these cases services and controllers are separated, services could be used with any controller and you may easily write unit-tests on services that will not break if you change your controller/view part somehow.
A possible solution would be to have a dialog service which you can inject into the grid service. So when the user right clicks on the polygon the handler would call open on the dialog service.
Take a look at the modal service on angular ui as an example.
All I need to do is to download a json file and assign it to OCategories in PCategory provider after I set the path. However I get an error that $http doesnt exist. How can I inject it into my provider and download inside of the setPath function?
var app = angular.module('NSApp',
[
'ui.bootstrap',
'MDItem',
'MDUser',
'MDNotification',
'MDUpload'
]
);
app.config(function(PCategoriesProvider)
{
PCategoriesProvider.setPath('data/csv/categories.json');
});
MDItem/provider/category.js
angular.module('MDItem').provider('PCategories',function(){
var OCategories;
var OPath;
return{
setPath: function(d){
OPath = d;
console.log('Path is set. Trying to download categories.');
OCategories = $http.get(oPath);
},
$get : function() {
return {
categories : OCategories
}
}
}
});
You can never inject service instances into config functions or providers, since they aren't configured yet. Providers exist to configure specific services before they get injected. Which means, there's always a corresponding provider to a certain service. Just to clarify, here's a little example configuring $location service using $locationProvider:
angular.module('myModule').config(function ($locationProvider) {
$locationProvider.html5Mode(true);
});
So what happens here, is that we configure $location service to use its html5mode. We do that by using the interfaces provided by $locationProvider. At the time when config() is executed, there isn't any service instance available yet, but you have a chance to configure any service before they get instantiated.
Later at runtime (the earliest moment ist the run() function) you can inject a service. What you get when injecting a service is what its providers $get() method returns. Which also means, each provider has to have a $get() function otherwise $injector would throw an error.
But what happens, when creating custom services without building a provider? So something like:
angular.module('myModule').factory('myService', function () {
...
});
You just don't have to care about, because angular does it for you. Everytime you register any kind of service (unless it is not a provider), angular will set up a provider with a $get() method for you, so $injector is able to instantiate later.
So how to solve your problem. How to make asynchronous calls using $http service when actually being in configuration phrase? The answer: you can't.
What you can do, is run the $http call as soon as your service gets instantiated. Because at the time when your service get instantiated, you're able to inject other services (like you always do). So you actually would do something like this:
angular.module('myModule').provider('custom', function (otherProvider, otherProvider2) {
// some configuration stuff and interfaces for the outside world
return {
$get: function ($http, injectable2, injectable3) {
$http.get(/*...*/);
}
};
});
Now your custom provider returns a service instance that has $http as dependency. Once your service gets injected, all its dependencies get injected too, which means within $get you have access to $http service. Next you just make the call you need.
To make your this call is getting invoked as soon as possible, you have to inject your custom service at run() phrase, which looks like this:
angular.module('myModule').run(function (custom, injectable2) {
/* custom gets instantiated, so its $http call gets invoked */
});
Hope this makes things clear.
Since all services are singletons in angular you could simply store a variable in a factory with the $http promise. And then when the factory is called at startup it will download the json.
You can then also expose a method on the factory that refreshes the data.
I know this is not the exact answer to your question, but I thought I'd share how I would do it.
angular.module('MDItem').factory('PCategories', function ($http, PCategoriesPath) {
var service = {
categories: [],
get: function () {
if (angular.isUndefined(PCategoriesPath)) {
throw new Error('PCategoriesPath must be set to get items');
}
$http.get(PCategoriesPath).then(function (response) {
service.categories = response.data;
});
}
};
// Get the categories at startup / or if you like do it later.
service.get();
return service;
});
// Then make sure that PCategoriesPath is created at startup by using const
angular.module('MDItem').const('PCategoriesPath', 'data/csv/categories.json');
angular.module('app').controller('myCtrl', function ($scope, PCategories) {
$scope.categories = PCategories.categories;
// And optionally, use a watch if you would like to do something if the categories are updated via PCategories.get()
$scope.$watch('categories', function (newCategories) {
console.log('Look maa, new categories');
}, true); // Notice the true, which makes angular work when watching an array
})
You have to inject $http in the function $get, because that's the function called by the injector.
However, to download the categories you would be better off using promises:
angular.module('MDItem').provider('PCategories',function(){
var OCategories;
var OPath;
return{
setPath: function(d){
OPath = d;
console.log('Path is set');
},
$get : function($http) {
return {
fetch: function () {
var deferred = $q.defer();
$http.get(oPath).then(function (value) {
deferred.resolve(value);
}
return deferred.promise;
}
}
}
}
});
I implemented what I wanted with a diffrent approach which is quite simple and effective. Just add a dummy controller in the main index.html(NOT PARTIAL). Data is now shared between all my modules and controllers and everything is downloaded once. :) Oh I love AJ.
...
<div ng-controller="initController" hidden></div>
...
initController:
angular.module('NSApp',[]).controller("initController",function($scope, $http, FCategory, FLocation){
$http.get('data/json/categories.json').then(function (response) {
FCategory.categories = response.data;
});
$http.get('data/json/cities.json').then(function (response) {
FLocation.cities = response.data;
});
$http.get('data/json/regions.json').then(function (response) {
FLocation.regions = response.data;
});
});
And now you can access it:
angular.module('MDTest', []).controller("test",function($scope, FCategory, FLocation){
$scope.categories = FCategory.categories;
FCategory factory
angular.module('MDItem').factory('FCategory', function ($http) {
var service = {
categories: [],
....
};
return service;
});