Wait for forEach to complete before returning valu

2019-06-14 13:33发布

问题:

Overview: I'm creating an app using ionic framework and AngularJS. The App has 2 forms where user can upload images. In first form, image upload field allows to upload only one image and second form image upload field allows to upload more than one images. After user uploads images I'm showing preview to user right below image field. Then user click on "Save", which calls Web Service (defined into factory) to upload images to site. The Service function implements $q, so that form submission can continue only after uploading images. This is working fine.

Problem: When uploading multiple images, I'm calling another helper function in controller which loop (forEach) over file data and call file save function for each file. But the execution in this helper function doesn't wait for forEach loop to complete. This is allowing me to get first file details of file saved, but not remaining saved files details.

Here is code I'm using for uploading file:

.controller('AppCtrl', function($q) {
  // Upload single file.
  $scope.fileUpload = function(fileData) {
    var deferred = $q.defer();
    if (angular.isUndefined(fileData) || angular.isUndefined(fileData.dataURL)) {
      deferred.resolve("No new upload found.");
    }
    else {
      var filedata = {
        "file" : {
          file: (fileData.dataURL).replace(/^data:image\/[A-Za-z]{3,4};base64,+/g, ''),
          filename: fileData.file.name,
        },
      };
      AuthServiceContent.uploadNewFile(filedata).then(function(response) {
        deferred.resolve(response);
      }, function(response) {
        deferred.reject(response);
      });
    }
    return deferred.promise;
  };

  // Upload multiple files.
  $scope.fileUploadMultiple = function(data) {
    var deferred = $q.defer();
    var fileUploadStatus = [];
    if (angular.isUndefined(data) || angular.isUndefined(data[0])) {
      deferred.reject("No new upload found.");
    }
    else {
      angular.forEach(data, function(fileData, index) {
        $scope.fileUpload(fileData).then(function(response) {
          fileUploadStatus[index] = response;
        }, function(response) {
        });
      });
    }

    return (!angular.isUndefined(fileUploadStatus) ? deferred.resolve(fileUploadStatus) : deferred.reject(fileUploadStatus));
  };

  $scope.createContent = function(formData) {
    $scope.fileUploadMultiple(formData.image).then(function(response) {
      if (angular.isObject(response)) {
        angular.forEach(response, function(fileData, index) {
          console.log(fileData);
          formData.image.und = [{'fid' : fileData.fid}];
        });
        console.log(formData);
      }
      else {
      }
    }, function(err) {
      console.log("updates failed!!!");
    });
    return;
  };
})

.factory('AuthServiceContent', function($q, $http, DEFAULT) {
  var service_content = {
    uploadNewFile: function(fileData) {
      var deferred = $q.defer();
      $http.post(DEFAULT.serviceURL + 'file', JSON.stringify(fileData), {
        headers : {
          'Content-Type': 'application/json',
          'Cookie': 'cookieData',
        }
      })
      .success(function (data, status, headers, config) {
        deferred.resolve(data);
      })
      .error(function (data, status, headers, config) {
        deferred.reject(data);
      });
      return deferred.promise;
    }
  };

  return service_content;
});

I've been trying this for more than 2 days and find similar issue, but its not working.

Updates: I got this working by adding extra check in loop, here is updated function in controller to upload multiple images.

  $scope.fileUploadMultiple = function(data) {
    var deferred = $q.defer();
    var fileUploadStatus = [];
    if (angular.isUndefined(data) || angular.isUndefined(data[0])) {
      deferred.reject("No new upload found.");
    }
    else {
      var dataLength = (data.length - 1);

      angular.forEach(data, function(fileData, index) {
        $scope.fileUpload(fileData).then(function(response) {
          fileUploadStatus[index] = response;
          // Check if we are at last element. If yes, then return status
          // Return deffered status on last element.
          if (dataLength == index) {
            deferred.resolve(fileUploadStatus);
          }
        }, function(response) {
          // Check if we are at last element. If yes, then return status
          // Return deffered status on last element.
          if (dataLength == index) {
            deferred.reject(fileUploadStatus);
          }
        });
      });
    }

    return deferred.promise;
  };

回答1:

The problem is that you have only one promise and are resolving it as soon as the first call uploaded file returns its results.

Save all the promises in an array and then try $q.all:

var promises = [];
angular.forEach(data, function(fileData, index) {
    promises.push($scope.fileUpload(fileData));
});

$q.all(promises).then(function(results) {
   // loop through the results, one for each promise, and do what you need
})

or just return $q.all(promises) and let the application code handle the results.

The big problem with $q.all is that it just gives and error if ANY of the promises is rejected. If you still want to handle the results for each promise, I use an implementation of $q.allSettled (I think I use this implementation. That returns a response for each promise -- either success or failure -- with the error message or the returned data so that I can then handle the results of each promise separately.

Hope this helps



回答2:

The updated code in the answer is not working. If the last file is the smallest, it fires the resolve immediately after it's been uploaded. I use instead a fileIsUplaoded counter.

  $scope.fileUploadMultiple = function(data) {
    var deferred = $q.defer();
    var fileUploadStatus = [];
    if (angular.isUndefined(data) || angular.isUndefined(data[0])) {
      deferred.reject("No new upload found.");
    }
    else {
      var uploadCount = data.length;

      angular.forEach(data, function(fileData, index) {
        $scope.fileUpload(fileData).then(function(response) {
          // if a file is uploaded reduce the uploadCount by 1
          uploadCount --;
          fileUploadStatus[index] = response;
          // Check if we are at last element. If yes, then return status
          // Return deffered status on last element.
          if (uploadCount == 0) {
            deferred.resolve(fileUploadStatus);
          }
        }, function(response) {
          // if a file is not uploaded reduce the uploadCount by 1
          uploadCount --;
          // Check if we are at last element. If yes, then return status
          // Return deffered status on last element.
          if (uploadCount == 0) {
            deferred.reject(fileUploadStatus);
          }
        });
      });
    }

    return deferred.promise;
  };