Luuk de Bruin Luuk de Bruin - 2 months ago 10
Javascript Question

Problems with an angular foreach waiting for http calls

I'm building an ionic project where users can play a tour (which the data is from an API)

Every tour has an amount of parts that users can play at a certain point on the map. This app must be able to be a 100% offline app, so when the user enters the code of a tour, the data must be retrieved from the API before the user can proceed (so the app will put all the data of the tour offline). Every part has an image, video, audio which is getting downloaded at start of the app.

The problem is that the function call, who is downloading all the data, is not synchronous. The console.log's are saying that the function already ends before all data is downloaded. Pieces of code below:

function getAndFillFullTour() {
vm.showLoader = true;
// load data
TourFactory.getFullTour(vm.tourData.number, function(data){
if(data.state == 'success'){
vm.tourData = data;
var test = downloadData(function(){
// hide loader and continue tour
});
} else {
console.log('error');
}
});
}


This function calls the factory who is getting the full tour including paths of images of each part which is needed to get downloaded on the users device. The downloadData function is the following function:

function downloadData(callback) {
angular.forEach(vm.tourData.parts, function(value, key){
var part = value;
var i = key;

if(part.image !== "") {
TourFactory.getPartImage(part, tourId, function(data){
vm.tourData.parts[i].partImage = data;
console.log('executed with picture ' + i);
});
}

});

if(callback)
callback();
}


Unfortunately, the forloop itself is performing synchronous but it is not waiting for the factory call to complete. I tried a lot of alternatives with promises, but without luck. Could anyone help? I need to wait for the http call to be finished in order to get a response from the downloadData call.

the getPartImage() is just an example, there are five functions like this each for loop which need to be completed first before I get a response in the downloadData call.

Answer

Take a look at $q.all or here- it is a promise helper function that can wait for multiple promises to complete. It's result is a promise as well, so you can chain it with other promises.

// Promise function that knows how to download a single part
function downloadPart(myurl) {
  // return http promise
  return $http({
    method: 'GET',
    url: myurl
  });
};

// Aggregat epromise that downloads all parts
function downloadAllParts(parts) {
  var defer = $q.defer(); // Setup return promise
  var partsPromises = []; // Setup array for indivudual part promises
  angular.forEach(parts, function(part) { // Iterate through each part
    // Schedule download of a single
    partsPromises.push(downloadPart(part));
  });
  // Wait for all parts to resolve
  $q.all(partsPromises)
    .then(function(data) {
      // Returned data will be an array of results from each individual http promise
      resData = [];
      angular.forEach(data, function(partData) {
        //handle each return part
        resData.push(partData.data);
      })
      defer.resolve(resData); // Notify client we downloaded all parts
    }, function error(response) { // Handle possible errors
      console.log('Error while downloading parts'
        response);
      defer.reject('Error while downloading parts');
    });
  return defer.promise;
};

Then, in your client you can simply wait for the downloadAllParts to complete:

downloadAllParts(myParts)
 .then(function(data) {
   alert('Success!');
 }, function(error) {
   alert(error);
 })

Since $q.all is a promise as well, you can get rid of defers all together:

// Aggregat epromise that downloads all parts
function downloadAllParts(parts) {
  var partsPromises = []; // Setup array for indivudual part promises
  angular.forEach(parts, function(part) { // Iterate through each part
    // Schedule download of a single
    partsPromises.push(downloadPart(part));
  });
  // Wait for all parts to resolve
  return $q.all(partsPromises)
    .then(function(data) {
      // Returned data will be an array of results from each individual http promise
      var resData = [];
      angular.forEach(data, function(partData) {
        //handle each return part
        resData.push(partData.data);
      })
      return resData;
    });
};

Here is a working jsfiddle: link

Comments