0

In an AngularJS app I am developing, I'm attempting to process multiple uploaded files asynchronously in order to prepare them for a routine that adds them to a SuiteCRM instance. Overall, the code is bug free but I'm finding that the deferred calls are getting executed one after another and not passing the result to the awaiting script until all promises are resolved. Perhaps someone could help me out with how to chain these in order to get the result I want?

  function fileReader(file) { // perform async operation
      var deferred = $q.defer();
      var reader = new FileReader();
      reader.onload = function () {
          deferred.resolve(reader.result.replace(/^(.*),/g, ""));
      };
      reader.readAsDataURL(file);
      return deferred.promise;
  }

  $scope.uploadFiles = function () {
        console.log('Starting uploads');
        $scope.uploadClicked = true;
        $scope.uploadProgress = {};
        $applicationID = $('#application_id').val();
        var tmpMerchData = SessionService.getMerchantData();
        var isoID = SessionService.getISOID();
        if ($scope.fileList && $scope.fileList.length) {
            var $myFiles = $scope.fileList;
            for (var f = 0; f < $myFiles.length; f++) {
                var thisFile = $myFiles[f];
                console.debug(thisFile);
                fileReader(thisFile.file).then(function (result) {
                    ApplicationFactory.createNewUploadedDocument(thisFile, thisFile.templatetype, result, thisFile.description, $scope.userID, tmpMerchData.id, $applicationID, isoID).then(
                            function successCallback(response) {
                                if (response.data.status === 1) {
                                    console.log("Document creation succeeded");
                                    $scope.uploadProgress = {
                                        'progress': 100,
                                        'index': f
                                    };
                                } else {
                                    console.log("Document creation failed");
                                }
                            }, function errorCallback(response) {
                        console.debug("Error returned from createNewUploadedDocument", response);
                    });
                });
            }
        }
    };

So, you see, I need the FileReader to process the file, return the file's contents in order to pass it on to the createNewUploadedDocument function for each of the f (files) in my $scope.fileList. NGF's multiple files option is not available to me because each of these files has a specific template type and description.

Any help you can provide or suggest would be greatly appreciated.

EDIT - I have read this Chaining multiple promises created through for loop and this angular $q, How to chain multiple promises within and after a for-loop but neither are providing any usable feedback for my specific situation. I understand that promises can be nested and combined using $q.all but can they still provide resulting data to one another? My inner method is a call to a REST API - not that it makes any difference - but I do need the result of the FileReader promise in order to call it.

Community
  • 1
  • 1
DevlshOne
  • 8,357
  • 1
  • 29
  • 37
  • What's the part that isn't working? The code is a bit messy but should work, but since you're not super clear on what your desired outcome is it's hard to help out. – Daniel B Jan 25 '17 at 14:13
  • It loops through all of the fileReader promises before calling the underlying function. – DevlshOne Jan 25 '17 at 16:15
  • That's not possible. It should continue into the first `.then()` when the first promise is resolved from `fileReader`. There is nothing stopping it from continuing so the case that it would await all promises before entering any of the `.then()` is just illogical. – Daniel B Jan 25 '17 at 16:41
  • I've console'd the output from this and it does exactly that - iterates through each `fileReader` and then provides only the last `result` to the `.then()`. Illogical as it may seem (and I thought the code should work as it's written, as well)! – DevlshOne Jan 25 '17 at 16:54

2 Answers2

0

ANSWER - this updated version of the original code fixes the problem.

Notice that an angular.forEach has been implemented as well as some cleaned up variable names and reuse.

    $scope.uploadFiles = function () {
        console.log('Starting uploads');
        $scope.uploadClicked = true;
        $scope.uploadProgress = {};
        $applicationID = $('#application_id').val();
        var tmpMerchData = SessionService.getMerchantData();
        var isoID = SessionService.getISOID();
        if ($scope.fileList && $scope.fileList.length) {
            angular.forEach($scope.fileList, function (thisFile, f) {
                console.log('Starting upload #', f);
                fileReader(thisFile.file).then(
                        function (fileContents) {
                            ApplicationFactory.createNewUploadedDocument(thisFile, thisFile.templatetype, fileContents, thisFile.description, $scope.userID, tmpMerchData.id, $applicationID, isoID).then(
                                    function successCallBack(response) {
                                        if (response.data.status === 1) {
                                            console.log('Document creation succeeded');
                                            $scope.uploadProgress = {
                                                'progress': 100,
                                                'index': f
                                            };
                                        } else {
                                            console.log('Document creation failed');
                                        }
                                    }, function errorCallback(response) {
                                console.debug('Error returned from createNewUploadedDocument ', response);
                            });
                        });
            });
        }
    };
DevlshOne
  • 8,357
  • 1
  • 29
  • 37
  • Although `f` is now trapped in a closure, `$scope.uploadProgress` will probably (depending on its nature) still be an unreliable indicator of actual progress. Due to `ApplicationFactory.createNewUploadedDocument()` being asynchronous, there's no guarantee that its responses will arrive back in the original order. If it is a thermometer style indicator, it could jump around all over the place and finish at somwhere other than 100%. – Roamer-1888 Jan 26 '17 at 13:09
  • 1
    Therefore, instead of `'index': f`, use `'index': ++count`, where `var count = 0` is declared outside the `$scope.fileList.forEach()` loop. With that in place, you no longer need the closure, so could revert to a `for` loop. – Roamer-1888 Jan 26 '17 at 13:09
  • Actually, I'm not relying on `$scope.uploadProgress` to be an actual indicator of the upload progress, it jumps directly to 100% as soon as the file is done. I do see what you mean, however. – DevlshOne Jan 27 '17 at 17:40
  • OK, as I feared, there's a lot about the design that I don't understand. Good luck with it. – Roamer-1888 Jan 27 '17 at 21:53
-1

You can effectively use $q service. When you use $q.all(), you pass an array of promises and in result you get an array of the results of each promise in the same order. So we can rewrite your code like this :

if ($scope.fileList && $scope.fileList.length) {
   var $myFiles = $scope.fileList;
   var promisesFileReader = [];
   var promisesUploadedDocument = [];
   for (var f = 0; f < $myFiles.length; f++) {
      var thisFile = $myFiles[f];
      console.debug(thisFile);
      promisesFileReader.push(fileReader(thisFile.file));
   }

   $q.all(promisesFileReader)
      .then(function(results) {

         //We loop on results and push all the UploadDocument promises with the correct result 
         for (var i = 0; i < results.length; i++) {
            promisesUploadedDocument.push(ApplicationFactory.createNewUploadedDocument(thisFile, thisFile.templatetype, results[i], thisFile.description, $scope.userID, tmpMerchData.id, $applicationID, isoID));
         }

         $q.all(promisesUploadedDocument)
            .then(
               function successCallback(response) {
                  if (response.data.status === 1) {
                     console.log("Document creation succeeded");
                     $scope.uploadProgress = {
                        'progress': 100,
                        'index': f
                     };
                  } else {
                     console.log("Document creation failed");
                  }
            },
              function errorCallback(response) {
                 console.debug("Error returned from createNewUploadedDocument", response);
            });
       });
}
  • 3
    This won't work since you're invoking `$q.all` each iteration in the `for` loop. You should move the `$q.all` outside the loop and invoke it once when you have the full array of promises from `fileReader`. – Daniel B Jan 25 '17 at 14:11
  • Sorry, this doesn't work. It returns "results" `f` number of times. Each time as an increasing larger object. – DevlshOne Jan 25 '17 at 17:46
  • I'm sorry. You have to put the $q.all resquests out of the for loops. The loops allow to push all promises to an array and after that you can call every promise with $q.all. – Emeline Esteves Jan 27 '17 at 09:14