Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting '$apply is already in progress' error even though I don't explicitly call $apply()

Tags:

angularjs

I am creating a directive that helps with uploading a CSV file. I am using a <input type="file" ng-hide="true"/> and the native JavaScript FileReader() to do this. Everything works fine when I select the file using the native input, but I tried to customize the file control by using my own "Browse" button. The problem is when I try to do it this way I get the $apply is already in progress error.

Here is my directive

app.directive('skUploader', function ($timeout) {
    return {
        restrict: 'A',
        replace: true,
        template: '<div>\
                    <input type="file" class="uploadControl" ng-hide="false" />\
                    <input type="text" ng-model="selectedFile" />\
                    <button type="button" class="btnPlain" ng-click="triggerBrowse()">Browse</button>\
                    <button type="button" class="btnOrange fRight" ng-click="uploadCSV()">Upload</button>\
                    <button type="button" class="btnPlain fRight" ng-click="cancel()">Cancel</button>\
                   </div>',
        link: function (scope, elem, attr, ctrl) {

            var uploadControl = elem.find('input[type=file]');
            var payload = null;

            var type = attr.skUploader;
            scope.selectedFile = null;

            scope.uploadCSV = function () {
                $timeout(function () {
                    alert('Upload Successful');
                }, 3000);
            }

            scope.cancel = function () {
                payload = null;
                scope.selectedFile = null;
                scope.model.showUploadPanel = false;

            }

            uploadControl.on('change',function () {
                var fileHandle = uploadControl[0].files[0];
                var reader = new FileReader();
                var buffer;

                scope.selectedFile = fileHandle.name;

                reader.onload = function(){
                    buffer = reader.result;

                    payload = parseCSV(buffer);

                    if (payload) {
                        // Make API call
                        if (type == 'mapping') {
                            console.log(payload);
                        } else {

                        }
                    }
                }

                if (fileHandle.name.indexOf('.csv') > 0) {
                    reader.readAsText(fileHandle);
                } else {

                }

                });

            function parseCSV(buff){

                var entries = [];
                entries = buff.split(/\n/);

                var json = [];


                if (type == 'mapping') {
                    var formatter = {
                        iRowNumber: null,
                        SourceName: null,
                        DestName: null
                    };
                } else if (type == 'passwords') {
                    var formatter = {
                        iRowNumber: null,
                        SourceCreds: {
                            EmailAddress: null,
                            Username: null,
                            Pwd: null
                        }
                    };
                } else {
                    return null;
                }


                for(var i = 1; i < entries.length - 1; i++){
                    var split = entries[i].split(',');

                    var obj = angular.copy(formatter);

                    var j = 0;
                    angular.forEach(formatter, function (value, key) {
                        if (key == 'iRowNumber') {
                            obj[key] = i;
                        } else if (key == 'SourceCreds') {
                            obj[key] = {
                                EmailAddress: split[0],
                                Username: split[1],
                                Pwd: split[2]
                            };
                        }else{
                            obj[key] = split[j];
                            j++;
                        }
                    });

                    json.push(obj);

                }

                return json;

            }

            scope.triggerBrowse = function () {
                uploadControl.trigger('click'); // THIS IS WHAT CAUSES THE ERROR
            }
        }
    };
});

When I use the "Browse" button to trigger('click') the uploadControl, Angular spits out the error

Error: [$rootScope:inprog] $apply already in progress

But I am not using scope.$apply() anywhere so I cannot figure out the problem. It should simply trigger the click event on the <input type="file"/> which should bring up the OS native file explorer.

Update

Could it be something related to this issue?

Update 2

After some more research I found this Git issue that vojta (a core AngularJS dev) responded to saying we need to wrap the event in a $timeout. I did this and it actually worked! Could someone please explain to me why this is needed in detail? Thank you

like image 798
Matt Hintzke Avatar asked Sep 12 '14 19:09

Matt Hintzke


People also ask

How do you resolve $Digest already in progress?

There are a few ways to deal with this. The easiest way to deal with this is to use the built in $timeout, and a second way is if you are using underscore or lodash (and you should be), call the following: $timeout(function(){ //any code in here will automatically have an apply run afterwards });

How use $apply in AngularJS?

In AngularJS, $apply() function is used to evaluate expressions outside of the AngularJS context (browser DOM Events, XHR). Moreover, $apply has $digest under its hood, which is ultimately called whenever $apply() is called to update the data bindings. We will take an example to give a better understanding.


1 Answers

When you make changes to the scope without using Angular's functions (using a jQuery click event handler instead of ngClick, for example) it gets very grumpy. Sometimes it'll just ignore your changes until it runs another apply, and sometimes you'll catch it at a particularly grumpy moment when it's already busy with other things and it'll give you the error you got.

To be Angular friendly, you need to use its event system to update the scope. If you can't do that, you have a way to tell it you're making changes so that it can deal with those changes on its own schedule. That way is $timeout - an Angular-friendly version of JavaScript's timeout that is integrated with the update loop. Calling it without a duration parameter just means "The next time you're ready for changes to the scope, do this stuff."

Calling $apply yourself is not a good idea, by the way, just use $timeout.

like image 121
Carson Myers Avatar answered Nov 12 '22 17:11

Carson Myers