Question
I have implemented the $q.all in angularjs, but I can not make the code work. Here is my code :
UploadService.uploadQuestion = function(questions){
var promises = [];
for(var i = 0 ; i < questions.length ; i++){
var deffered = $q.defer();
var question = questions[i];
$http({
url : 'upload/question',
method: 'POST',
data : question
}).
success(function(data){
deffered.resolve(data);
}).
error(function(error){
deffered.reject();
});
promises.push(deffered.promise);
}
return $q.all(promises);
}
And here is my controller which call the services:
uploadService.uploadQuestion(questions).then(function(datas){
//the datas can not be retrieved although the server has responded
},
function(errors){
//errors can not be retrieved also
})
I think there is some problem setting up $q.all in my service.
Answer
In javascript there are no block-level scopes
only function-level scopes
:
Read this article about javaScript Scoping and Hoisting.
See how I debugged your code:
var deferred = $q.defer();
deferred.count = i;
console.log(deferred.count); // 0,1,2,3,4,5 --< all deferred objects
// some code
.success(function(data){
console.log(deferred.count); // 5,5,5,5,5,5 --< only the last deferred object
deferred.resolve(data);
})
- When you write
var deferred= $q.defer();
inside a for loop it's hoisted to the top of the function, it means that javascript declares this variable on the function scope outside of thefor loop
. - With each loop, the last deferred is overriding the previous one, there is no block-level scope to save a reference to that object.
- When asynchronous callbacks (success / error) are invoked, they reference only the last deferred object and only it gets resolved, so $q.all is never resolved because it still waits for other deferred objects.
- What you need is to create an anonymous function for each item you iterate.
- Since functions do have scopes, the reference to the deferred objects are preserved in a
closure scope
even after functions are executed. - As #dfsq commented: There is no need to manually construct a new deferred object since $http itself returns a promise.
Solution with angular.forEach
:
Here is a demo plunker: http://plnkr.co/edit/NGMp4ycmaCqVOmgohN53?p=preview
UploadService.uploadQuestion = function(questions){
var promises = [];
angular.forEach(questions , function(question) {
var promise = $http({
url : 'upload/question',
method: 'POST',
data : question
});
promises.push(promise);
});
return $q.all(promises);
}
My favorite way is to use Array#map
:
Here is a demo plunker: http://plnkr.co/edit/KYeTWUyxJR4mlU77svw9?p=preview
UploadService.uploadQuestion = function(questions){
var promises = questions.map(function(question) {
return $http({
url : 'upload/question',
method: 'POST',
data : question
});
});
return $q.all(promises);
}