$http promise in angular service - javascript

I am having a problem with promises in an angular service. I have a service with a method getArea which is supposed to return the name of a service-area. The service gets the service-areas from the API. When getArea gets the service-areas, it finds the name of the requested area, and should return it. However, my code does not work - I get into an infinite loop. I guess I have misunderstood how to use promises?
SupplierService:
var servicePromise;
var getServices = function(){
if( !servicePromise ){
servicePromise = $http.get('/api/services')
.then(function(res){
return res.data.data;
});
}
return servicePromise;
};
var myService = {
getServices : getServices,
getArea : function(questionnaireId){
getServices().then(function(services){
// ...
return "hello world";
});
}
};
return myService;
Controller:
$scope.supplierService = SupplierService;
View:
<div>
<b>Area:</b> {{ supplierService.getArea(r.questionnaireId) }}
</div
I expect the view to show "Area: hello world", but gets into an infinite loop.
Update 1: I have added getServices as a public function in the service, and can access it from the controller like this:
SupplierService.getServices().then(function(d){
$scope.services = d;
});
Therefore I guess the problem is in the getArea method?
Update 2: I was inspired by this answer https://stackoverflow.com/a/12513509/685352. I want to cache the result.
Update 3: Here is a plunker. If you try accessing supplierService.getArea(100) from the view - the browser will not respond.

Your service should look more like this:
var getServices = function(){
var deferred = $q.deferred();
$http.get('/api/services')
.then(function(res){
deferred.resolve(res.data)
});
return deferred.promise;
};
Notice when you create a deferred you must return the deferred.promise (the actual promise) and then when you're async call returns you must call deferred.resolve or deferred.rejected as appropriate (to trigger the success or error functions respectively)
Minor addition I have a plunkr showing a few ways of getting data from a service into your controllers since this is a common issue for devs coming into Angular
http://plnkr.co/edit/ABQsAxz1bNi34ehmPRsF?p=info
It's not absolute best practices since I tried to keep it as simple as possible, but basically showing three different ways to "share" your data keep in mind some of these methods rely on angular.copy which means the property of the service you store the data on must be an Object or an Array (primitive types won't work since the reference can't be shared).
Here's a rewrite including the function inline:
var myService = {
var dataLoaded = false;
var data = {}; //or = [];
getServices : function(){
var deferred = $q.defer();
if( !dataLoaded ){
$http.get('/api/services').then(function(res){
angular.copy(res.data, myService.data);
deferred.resolve(myService.data);
}, function(err){
deferred.reject("Something bad happened in the request");
});
}
else
{
deferred.resolve(myService.data);
}
return deferred.promise;
}
};
return myService;
To explain, I create a new promise using the $q service which you'll need to inject to the service function. This allows me to either resolve that promise with data I already have or to make the call to the service and resolve that data but in both cases when this is being used it's assumed you will just get a promise back and are therefore dealing with an async operation. If you have multiple data sets to load you can use an object to store the flags instead of a single boolean.

i think if you return the $http callback?
//$http.get('/someUrl').success(successCallback);
var getServices = function(){
return $http.get('/api/services');
};
getServices.success(function(services){
// ...
return "hello world";
});
}

Related

Why is $rootScope's property undefined?

I have an angular application that implements factory functions to handle some API requests for a global object that is implemented in almost all controllers.
factory.loadCart = function() {
var deferred;
deferred = $q.defer();
httpService.get({
service: 'cocacola',
param1: 'userCart',
guid: sessionStorage.token
}, function(r) {
if (r.error == 0) {
$rootScope.user.cart = r.result;
deferred.resolve(r.result);
} else {
deferred.reject("Error al cargar el carrito.")
}
}, function(errorResult) {
deferred.reject(errorResult);
});
return deferred.promise;
}
In the code I set the value of user.cart property as the result of the request. When I go to another controller that also implements this factory method (in this way)...
CartFactory.loadCart().then(function(response) {
$rootScope.user.cart = response;
$scope.cart = $rootScope.user.cart;
if ($rootScope.user.cart.productos.length == 0) {
$state.go('main.tienda');
} else {
getCards();
$rootScope.showCart = false;
}
}, function(error) {
$scope.loading = false;
$scope.showMe = false;
$state.go('main.tienda');
console.log(error);
});
... and go back to the first controller, the user.cart property is undefined and I can't proceed to execute the other functions that are defined as factory methods since the $rootScope.user.cart property is undefined and required as a parameter to these other functions. Also, the $rootScope.user.cart property gets its value after I refresh the browser (but I can't keep this as a solution), I'm very new to Angular so any help will be really appreciated, this is driving me nuts!
I've always found that $rootScope was somewhat difficult to work with, and something of an antipattern in AngularJS... it's like putting variables on the global scope in vanilla JS.
Is there any reason you wouldn't avoid the whole $rootScope.user.cart issue by just keeping cart in CartFactory and then putting an API in CartFactory to getCart, then return whatever cart is to any interested controller?
where you setting 'user' property on $rootScope? can you please put the whole code and always before reading the nested properties from object, check for undefined, in your case put if($rootScope.user) { // your logic }

Use $timeout to wait service data resolved

I am trying to pass data from directive to controller via service, my service looks like this:
angular
.module('App')
.factory('WizardDataService', WizardDataService);
WizardDataService.$inject = [];
function WizardDataService() {
var wizardFormData = {};
var setWizardData = function (newFormData) {
console.log("wizardFormData: " + JSON.stringify(wizardFormData));
wizardFormData = newFormData;
};
var getWizardData = function () {
return wizardFormData;
};
var resetWizardData = function () {
//To be called when the data stored needs to be discarded
wizardFormData = {};
};
return {
setWizardData: setWizardData,
getWizardData: getWizardData,
resetWizardData: resetWizardData
};
}
But when I try to get data from controller it is not resolved (I think it waits digest loop to finish), So I have to use $timeout function in my controller to wait until it is finished, like this:
$timeout(function(){
//any code in here will automatically have an apply run afterwards
vm.getStoredData = WizardDataService.getWizardData();
$scope.$watchCollection(function () {
console.log("getStoredData callback: " + JSON.stringify(vm.getStoredData));
return vm.getStoredData;
}, function () {
});
}, 300);
Despite of the fact that it works, what I am interested in is, if there is a better way to do this, also if this is bug free and the main question, why we use 300 delay and not 100 (for example) for $timeout and if it always will work (maybe for someone it took more time than 300 to get data from the service).
You can return a promise from your service get method. Then in your controller, you can provide a success method to assign the results. Your service would look like this:
function getWizardData() {
var deferred = $q.defer();
$http.get("/myserver/getWizardData")
.then(function (results) {
deferred.resolve(results.data);
}),
function () {
deferred.reject();
}
return deferred.promise;
}
And in your ng-controller you call your service:
wizardService.getWizardData()
.then(function (results) {
$scope.myData = results;
},
function () { });
No timeouts necessary. If your server is RESTFULL, then use $resource and bind directly.
Use angular.copy to replace the data without changing the object reference.
function WizardDataService() {
var wizardFormData = {};
var setWizardData = function (newFormData) {
console.log("wizardFormData: " + JSON.stringify(wizardFormData));
angular.copy(newFormData, wizardFormData);
};
From the Docs:
angular.copy
Creates a deep copy of source, which should be an object or an array.
If a destination is provided, all of its elements (for arrays) or properties (for objects) are deleted and then all elements/properties from the source are copied to it.
Usage
angular.copy(source, [destination]);
-- AngularJS angular.copy API Reference
This way the object reference remains the same and any clients that have that reference will get updated. There is no need to fetch a new object reference on every update.

How to pass arguments to promise in AngularJS

I've go a small angular app with directive.
For retriving data from serverside I use ngRoute. After retriving data I bind result to a scope property and parse the result with ng-repeat like so:
<div class="col-xs-12" ng-repeat="clsRow in classificatorData">
<span>{{clsRow.code}}</span>
</div>
This function that retrievs data from resource
var getClassificatorDataScope = function (criteria, predicate) {
references.initialize('classificators');
references
.getRefereces(null, $scope.classificatorType, criteria, predicate == null ? "NONE" : predicate, $scope.limitLevel, null)
.$promise.then(function (result) {
$scope.classificatorData = result.Data;
});
};
Everything works fine. But if I try to implement passing result data container (dataScope) like so
var getClassificatorDataScope = function (criteria, predicate, dataScope) {
references.initialize('classificators');
references
.getRefereces(null, $scope.classificatorType, criteria, predicate == null ? "NONE" : predicate, $scope.limitLevel, null)
.$promise.then(function (result) {
dataScope = result.Data;
});
};
And use it in controller like so
getClassificatorDataScope("CODE", null, $scope.classificatorData);
I've got no data at all. Please help me to understand such behaviour.
There's 2 problems here.
dataScope = result.Data;
The first one is this. This doesn't act like how you'd expect it would. It doesn't replace the $scope.classificatorData. All it does is replace the local dataScope variable in getClassificatorDataScope to result.Data (yes, it's not "passed by reference").
Second, you're using promises incorrectly. You return the promise for listening, not pass the scope to who-knows-where. Your data layer should not be aware of $scope or the UI in general. Return the promise to the controller, and have it listen for the data from it.
// In your function
var getClassificatorDataScope = function(criteria, predicate) {
references.initialize('classificators');
return references
.getRefereces(null, $scope.classificatorType, criteria, predicate == null ? "NONE" : predicate, $scope.limitLevel, null)
.$promise
};
// In your controller
getClassificatorDataScope("CODE", null).then(function(result){
$scope.classificatorData = result.Data;
});
Seems that in second example you're trying to assign data retrieved from server to dataScope but since AJAX data loading is asynchronoys so $promise is resolved later than your template with ng-repeat is drawn.
There's not enough code provided in question to write whole example - how it should be implemented. But basically you should return $promise from your service and in controller change $scope variables upon
promise.then(function() {
//do stuff with $scope variables
})
The problem might be in your references.getRefereces method. It should return a promise and later resolve it with the proper result(I see you try to access "Data" attribute from result.). something like this:
reference.getReferences = function() {
var deferred = $q.defer();
someAsyncOperations(function callback(){
deferred.resolve({Data: result}) // make sure you have the "Data" attr
})
return deferred.promise;
// or if someAyncOperations already return a promise
// return someAsyncOperations.then(function(result) {
// return {Data: result};
// });
}

How to make controller wait for promise to resolve from angular service

I have a service that is making an AJAX request to the backend
Service:
function GetCompaniesService(options)
{
this.url = '/company';
this.Companies = undefined;
this.CompaniesPromise = $http.get(this.url);
}
Controller:
var CompaniesOb = new GetCompanies();
CompaniesOb.CompaniesPromise.then(function(data){
$scope.Companies = data;
});
I want my service to handle the ".then" function instead of having to handle it in my controller, and I want to be able to have my controller act on that data FROM the service, after the promise inside the service has been resolved.
Basically, I want to be able to access the data like so:
var CompaniesOb = new GetCompanies();
$scope.Companies = CompaniesOb.Companies;
With the resolution of the promise being handled inside of the service itself.
Is this possible? Or is the only way that I can access that promise's resolution is from outside the service?
If all you want is to handle the response of $http in the service itself, you can add a then function to the service where you do more processing then return from that then function, like this:
function GetCompaniesService(options) {
this.url = '/company';
this.Companies = undefined;
this.CompaniesPromise = $http.get(this.url).then(function(response) {
/* handle response then */
return response
})
}
But you'll still have use a promise in the controller, but what you get back will have already been handled in the service.
var CompaniesOb = new GetCompanies();
CompaniesOb.CompaniesPromise.then(function(dataAlreadyHandledInService) {
$scope.Companies = dataAlreadyHandledInService;
});
There is no problem to achieve that!
The main thing you have to keep in mind is that you have to keep the same object reference (and in javascript arrays are objects) in your service.
here is our simple HTML:
<div ng-controller = "companiesCtrl">
<ul ng-repeat="company in companies">
<li>{{company}}</li>
</ul>
</div>
Here is our service implementation:
serviceDataCaching.service('companiesSrv', ['$timeout', function($timeout){
var self = this;
var httpResult = [
'company 1',
'company 2',
'company 3'
];
this.companies = ['preloaded company'];
this.getCompanies = function() {
// we simulate an async operation
return $timeout(function(){
// keep the array object reference!!
self.companies.splice(0, self.companies.length);
// if you use the following code:
// self.companies = [];
// the controller will loose the reference to the array object as we are creating an new one
// as a result it will no longer get the changes made here!
for(var i=0; i< httpResult.length; i++){
self.companies.push(httpResult[i]);
}
return self.companies;
}, 3000);
}}]);
And finally the controller as you wanted it:
serviceDataCaching.controller('companiesCtrl', function ($scope, companiesSrv) {
$scope.companies = companiesSrv.companies;
companiesSrv.getCompanies();
});
Explanations
As said above, the trick is to keep the reference between the service and the controller. Once you respect this, you can totally bind your controller scope on a public property of your service.
Here a fiddle that wraps it up.
In the comments of the code you can try uncomment the piece that does not work and you will see how the controller is loosing the reference. In fact the controller will keep having a reference to the old array while the service will change the new one.
One last important thing: keep in mind that the $timeout is triggering a $apply() on the rootSCope. This is why our controller scope is refreshing 'alone'. Without it, and if you try to replace it with a normal setTimeout() you will see that the controller is not updating the company list.
To work around this you can:
don't do anything if your data is fetched with $http as it calls a $apply on success
wrap you result in a $timeout(..., 0);
inject $rootSCope in the service and call $apply() on it when the asynchronous operation is done
in the controller add a $scope.$apply() on the getCompanies() promise success
Hope this helps!
You can pass the $scope into GetCompanies and set $scope.Companies to the data in the service
function GetCompaniesService(options,scope)
{
this.url = '/company';
this.Companies = undefined;
this.CompaniesPromise = $http.get(this.url).then(function(res) {
scope.Companies = res;
});
}
You have to be careful about the order in which you then use the data. That's kind of the reason behind a promise to begin with.

Add methods to a collection returned from an angular resource query

I have a resource that returns an array from a query, like so:
.factory('Books', function($resource){
var Books = $resource('/authors/:authorId/books');
return Books;
})
Is it possible to add prototype methods to the array returned from this query? (Note, not to array.prototype).
For example, I'd like to add methods such as hasBookWithTitle(title) to the collection.
The suggestion from ricick is a good one, but if you want to actually have a method on the array that returns, you will have a harder time doing that. Basically what you need to do is create a bit of a wrapper around $resource and its instances. The problem you run into is this line of code from angular-resource.js:
var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
This is where the return value from $resource is set up. What happens is "value" is populated and returned while the ajax request is being executed. When the ajax request is completed, the value is returned into "value" above, but by reference (using the angular.copy() method). Each element of the array (for a method like query()) will be an instance of the resource you are operating on.
So a way you could extend this functionality would be something like this (non-tested code, so will probably not work without some adjustments):
var myModule = angular.module('myModule', ['ngResource']);
myModule.factory('Book', function($resource) {
var service = $resource('/authors/:authorId/books'),
origQuery = service.prototype.$query;
service.prototype.$query = function (a1, a2, a3) {
var returnData = origQuery.call(this, a1, a2, a3);
returnData.myCustomMethod = function () {
// Create your custom method here...
return returnData;
}
}
return service;
});
Again, you will have to mess with it a bit, but that's the basic idea.
This is probably a good case for creating a custom service extending resource, and adding utility methods to it, rather than adding methods to the returned values from the default resource service.
var myModule = angular.module('myModule', []);
myModule.factory('Book', function() {
var service = $resource('/authors/:authorId/books');
service.hasBookWithTitle = function(books, title){
//blah blah return true false etc.
}
return service;
});
then
books = Book.list(function(){
//check in the on complete method
var hasBook = Book.hasBookWithTitle(books, 'someTitle');
})
Looking at the code in angular-resource.js (at least for the 1.0.x series) it doesn't appear that you can add in a callback for any sort of default behavior (and this seems like the correct design to me).
If you're just using the value in a single controller, you can pass in a callback whenever you invoke query on the resource:
var books = Book.query(function(data) {
data.hasBookWithTitle = function (title) { ... };
]);
Alternatively, you can create a service which decorates the Books resource, forwards all of the calls to get/query/save/etc., and decorates the array with your method. Example plunk here: http://plnkr.co/edit/NJkPcsuraxesyhxlJ8lg
app.factory("Books",
function ($resource) {
var self = this;
var resource = $resource("sample.json");
return {
get: function(id) { return resource.get(id); },
// implement whatever else you need, save, delete etc.
query: function() {
return resource.query(
function(data) { // success callback
data.hasBookWithTitle = function(title) {
for (var i = 0; i < data.length; i++) {
if (title === data[i].title) {
return true;
}
}
return false;
};
},
function(data, response) { /* optional error callback */}
);
}
};
}
);
Thirdly, and I think this is better but it depends on your requirements, you can just take the functional approach and put the hasBookWithTitle function on your controller, or if the logic needs to be shared, in a utilities service.

Categories

Resources