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
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 });
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.
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With