Async/await with FileReader issue - javascript

I have this code to read an Excel sheet into an array of objects. Everything seems to work fine except that readed.onload is performed only when I try to access the data that are not yet loaded (i.e., I suspect that reader.readAsArrayBuffer(file) did not have the time to fire the .onload event, am I right?). Then data are correctly loaded but the program stops at the end of loadFile(file) (i.e., after the resolve statement) seemingly because the call stack is empty.
To be complete, loadFile(file) is called by launchAll() which was called by a .onclick event.
I searched for similar but none reported such a program stop.
I cannot figure out what is going on!
function launchAll() {
var files = document.getElementById('file_upload').files;
if (files.length == 0) {
alert("Please choose any file...");
return;
}
loadFile(files[0]
createEmptyTree() // Creates forefather and foremother
createTree() // Creates DAGs using mappedData (a global variable)
}
async function loadFile(file) {
try {
let fileLoaded = await readFileAsync(file)
console.log("File loaded !!", fileLoaded)
} catch (err) {
console.log("Error during loading ", err)
}
};
function readFileAsync(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.onload = (event) => {
var data = event.target.result;
var workbook = XLSX.read(data, {
type: 'binary'
});
var roa = XLSX.utils.sheet_to_row_object_array(workbook.Sheets[workbook.SheetNames[0]]);
if (roa.length > 0) {
for (i = 0; i < roa.length; i++) {
mappedData.push(mapNode(roa[i], i))
}
}
resolve(event)
}
reader.onerror = (error) => {
reject(error)
};
reader.readAsArrayBuffer(file)
})
}

You need to await loadFile:
// Add async
async function launchAll() {
// ...
// Await loadFile
await loadFile(files[0])
createEmptyTree() // Creates forefather and foremother
createTree() // Creates DAGs using mappedData (a global variable)
}

Related

Check array after async task finishes

Today I'm having an issue with async task while using JSZIP.
I want to check the array content after the async task executed by JSZip ends.
I have a zip which contains one XML file which I read and get a specific node to store them in another list to later do some stuffs. Well, my issue is that the checking array is called before the XML file is read and, as it is executed before XML file is read, array is empty.
I tried some ways to make it work, but, without success yet.
fileElement.addEventListener('change', (e) => {
try {
var zip = new JSZip();
zip.loadAsync( fileElement.files[0])
.then(function(zip) {
let xmlfiles = []
const _ziptask = async () => {for(let [filename, file] of Object.entries(zip.files)) {
if (filename.includes("file.xml")) {
file.async("string").then(function (data) {
let xmlDoc = new DOMParser().parseFromString(data,"text/xml");
let metaInputs = [...xmlDoc.querySelectorAll("file")];
xmlfiles = metaInputs.filter(_node => null != _node.getAttribute('src'));
console.log("FILE.XML LOOP ENDED")
});
}
}}
async () => {
await _ziptask().then(() => {
console.log("CHECKING FILE.XML ARRAY ")
console.log(xmlfiles)
})
}
}, function() {console.error("ERROR: NOT ZIP FILE")});
} catch (error) {
restoreFileInput("Something went wrong, try it again later")
}
});
Well, basically after testing different things, I reached the goal by using an array of promises and using Promise.all, which basically check that all the promises were resolved successfully.
Its curious that where I read this, the promises are stored in a const declaration instead var or let.
Anyway, if someone want to see the result:
fileElement.addEventListener('change', (e) => {
try {
var zip = new JSZip();
zip.loadAsync( fileElement.files[0])
.then(function(zip) {
let xmlfiles = []
const promises = [];
for(let [filename, file] of Object.entries(zip.files)) {
if (filename.includes("file.xml")) {
promises.push(file.async("string").then(function (data) {
let xmlDoc = new DOMParser().parseFromString(data,"text/xml");
let metaInputs = [...xmlDoc.querySelectorAll("file")];
xmlfiles = metaInputs.filter(_node => null != _node.getAttribute('src'));
console.log("FILE.XML LOOP ENDED")
}));
}
}
Promise.all(promises).then(function () {
console.log("CHECKING FILE.XML ARRAY ")
console.log(xmlfiles)
});
}, function() {console.error("ERROR: NOT ZIP FILE")});
} catch (error) {
restoreFileInput("Something went wrong, try it again later")
}
});
Thanks for the help to the guys who commented previously.
Best regards.

How to use async/await with FileReader in for-loop function in google-apps-script web app?

I have created a Google Apps Script web application for uploading images to my Google Drive. It has a lot of rows, each row has an input tag for uploading file. I created an only one submit button to upload all chosen images in one time. However, I would like each row to upload each image in order and then delete that row when it was uploaded successfully in order as well. The problem is I can't find the right way to use async/await function to upload the images to Drive with FileReader because when I run the script, It's still work as asynchronous function.
async function uploadImage() {
var row = document.getElementsByClassName('row');
var file = document.getElementsByClassName('img-file');
var name = document.getElementsByClassName('img-name');
for (let i=0; i<row.length; i++) {
var image = file[i].files[0];
if (image) {
var reader = new FileReader();
reader.readAsDataURL(image);
reader.onloadend = async (event) => {
await new Promise((resolve, reject) => {
google.script.run.withSuccessHandler(r => resolve())
.uploadImgToDrive(name[i].value, event.target.result)
}).then(() => row[i].innerHTML='');
}
}
}
}
If I understood what your goal is, the following code should work. Only one image at a time will be uploaded. Let me know if it fit your needs.
Please note that your function will always be asynchronous though because you have two asynchronous tasks inside it (FileReader and API call). The only thing you can decide is how many operations you want to handle "at the same time".
Finally, remember that anytime you use an async function it will immediately return an unresolved promise that will resolve with the value that the function returns when it finishes running.
Inside async functions, await is used to "wait" for a promise to resolve before continuing (in this case, the promise that you are creating with new Promise()), so it is similar to using .then() directly on the promise (you don't need both, that is why I removed the .then() part).
function uploadImages() {
var row = document.getElementsByClassName('row');
var file = document.getElementsByClassName('img-file');
var name = document.getElementsByClassName('img-name');
(function nextImg(i) {
var image = file[i].files[0];
if (image) {
var reader = new FileReader();
reader.readAsDataURL(image);
reader.onloadend = async (event) => {
await new Promise((resolve, reject) => {
google.script.run.withSuccessHandler(r => resolve())
.uploadImgToDrive(name[i].value, event.target.result);
});
row[i].innerHTML='';
if (i < row.length - 1) {
nextImg(i + 1);
}
};
}
})(0);
}
Optimised version (not tested):
Avoids using innerHTML (important) and tries to reuse FileReader() instance (not sure if it will work).
function uploadImages() {
let row = document.getElementsByClassName('row');
let file = document.getElementsByClassName('img-file');
let name = document.getElementsByClassName('img-name');
let reader = new FileReader();
(function nextImg(i) {
if (file[i].files[0]) {
reader.onloadend = async function onloadend(e) {
await new Promise((resolve) => {
google.script.run.withSuccessHandler(r => resolve(r)).uploadImgToDrive(name[i].value, e.target.result);
});
while (row[i].firstChild) {
row[i].removeChild(row[i].firstChild);
}
if (i < row.length - 1) {
nextImg(i + 1);
}
};
reader.readAsDataURL(file[i].files[0]);
}
})(0);
}
Another way to do this would be to hook up the loadend event of reader to a new promise and chain it:
async function uploadImage() {
var row = document.getElementsByClassName('row');
var file = document.getElementsByClassName('img-file');
var name = document.getElementsByClassName('img-name');
for (let i=0; i<row.length; i++) {
var image = file[i].files[0];
if (image) {
var reader = new FileReader();
reader.readAsDataURL(image);
let promiseOfAllDone = new Promise(res=>reader.addEventListener('loadend',res))
.then(event=>new Promise((resolve, reject) => {
google.script.run.withSuccessHandler(resolve)
.uploadImgToDrive(name[i].value, event.target.result)
}).then(() => row[i].innerHTML='')
.catch(e=>console.error(e));
await promiseOfAllDone;//wait for all promises to be fulfilled
}
}
}

call then only after method returning promise is finished

submitTCtoDB() {
console.log("this.selectedFileList is: " + this.selectedFileList)
this.readFile().then(() => {
alert("ReadFile Finished now submit TC");
this.submitTC()
});
}
readFile() {
return new Promise((resolve, reject) => {
for (let i = 0; i < this.selectedFileList.length; i++) {
let file = this.selectedFileList[i];
alert("file in redafile" + file.name)
let fileReader = new FileReader();
fileReader.onload = () => {
this.fileContent = fileReader.result;
if (this.fileContent.indexOf("END DATA | BEGIN RESULTS") != -1) {
alert("Multiple testcases found in " + file.name + " file. Please separate/save testcases in Calc Builder. Then reimport");
const index: number = this.selectedFileList.indexOf(file);
if (index > -1) {
this.selectedFileList.splice(index, 1);
}
console.log(this.fileContent);
}
resolve(this.fileContent);
}
fileReader.readAsText(file);
}
});
}
I want to run the submitTC() method only after the readFile method is completely finished but .then(inside submitTCtoDB) is getting invoked early .
I think .then or promise is not used properly.
Desired functionality is to call the submitTC method only when readFile method is completed reading/splicing the files.
Kindly help.
You have a resolve call in a loop, but resolve only has an effect when called the first time: once a promise resolves, that is its final state, and the then callbacks are triggered. So that happens when the first file has been read, without waiting for any other files to be processed.
What you could do:
Promisify the FileReader without adding specific logic (your if check): keep that outside of it, so it remains generic
Use Promise.all to map the file list to a new promise that will give the list of file contents.
Process the list of contents for the specific checks
Return the new promise (Promise.all or the one chained on it) to the caller.
Code:
submitTCtoDB() {
console.log("this.selectedFileList is: " + JSON.stringify(this.selectedFileList))
this.readFileList(this.selectedFileList).then((validList) => {
alert("ReadFile Finished now submit TC");
this.selectedFileList = validList;
this.submitTC()
});
}
readFileList(list) {
return Promise.all(list.map(file => this.readFile(file))).then(contents => {
return list.filter((file, i) => {
const fileContent = contents[i];
if (fileContent.indexOf("END DATA | BEGIN RESULTS") != -1) {
console.log("Multiple testcases found in " + file.name + " file. Please separate/save testcases in Calc Builder. Then reimport");
console.log(fileContent);
return false; // exclude this file
}
return true; // include this file
});
});
}
readFile(file) {
return new Promise(resolve => {
console.log("file in promiseFile: " + file.name);
const fileReader = new FileReader();
fileReader.onload = () => resolve(fileReader.result);
fileReader.readAsText(file);
});
}

HTML 5 File Reader reading javaScript files

I'm trying to allow users to drag and drop a folder containing JavaScript files into an html5 page. This is what I currently have:
$scope.files = [];
//Establish dropzone
var dropbox;
dropbox = document.getElementById("fileDragAndDrop");
dropbox.addEventListener("dragenter", dragenter, false);
dropbox.addEventListener("dragover", dragover, false);
dropbox.addEventListener("drop", drop, false);
//Events
function dragenter(e) {
e.stopPropagation();
e.preventDefault();
};
function dragover(e) {
e.stopPropagation();
e.preventDefault();
};
function drop(e) {
e.stopPropagation();
e.preventDefault();
var items = e.dataTransfer.items;
for (var i = 0, item; item = items[i]; i ++) {
var entry = item.webkitGetAsEntry();
if(entry) {
traverseFileTree(entry);
}
}
};
//resursive file walker
function traverseFileTree(item) {
if(item.isFile) {
$scope.$apply(function () {
$scope.files.push(item);
});
} else if (item.isDirectory) {
var dirReader = item.createReader();
dirReader.readEntries(function(entries) {
for (var i = 0; i < entries.length; i++) {
traverseFileTree(entries[i]);
}
});
}
};
So the dragging and dropping works, but I'm having problems reading the file content.
$scope.parse = function () {
for(var i = 0; i < $scope.files.length; i++) {
var fileReader = new FileReader();
fileReader.onload = function (e) {
console.log(fileReader.result);
};
fileReader.onerror = function(err) {
console.log(err);
};
fileReader.readAsBinaryString($scope.files[i]);
}
};
I am not getting any error messages, which makes it hard to debug. Am I doing something wrong? has anyone had any issues doing similar tasks?
Not sure what your $scope is but giving it a go.
As you use webkitGetAsEntry() I assume this is for Chrome. From the looks of it your code should give you an error. If it does not, there is likely something you have omitted. You should typically get something like:
Uncaught TypeError: Failed to execute 'readAsBinaryString' on 'FileReader': The argument is not a Blob.
in your $scope.parse function.
There is a few issues. One is that you probably would read files as text and not as binary string. Secondly readAsBinaryString() is deprecated, use readAsArrayBuffer() if you want to read binary data.
Further, the webkitGetAsEntry() returns a FileEntry, hence why you should get the error mentioned above. To read the file you could typically do:
$scope.files[i].file(success_call_back, [error_call_back]);
For example:
function my_parser(file) {
var fileReader = new FileReader();
fileReader.onload = function (e) {
console.log(fileReader.result);
};
fileReader.onerror = function(err) {
console.log(err);
};
console.log('Read', file);
// Here you could (should) switch to another read operation
// such as text or binary array
fileReader.readAsBinaryString(file);
}
$scope.files[0].file(my_parser);
This will give you a File object as argument to my_parser(). Then you could typically check .type and use appropriate read function. (Though be aware of the slackness in MIME type. As in: do not rely on it, but use it as a hint.)
if (file.type.match(/application\/javascript|text\/.*/)) {
// Use text read
} else if (file.type.match(/^image\/.*/)) {
// Use binary read, read as dataURL etc.
} else ...
$scope.parse = function () {
for(var i = 0; i < $scope.files.length; i++) {
var fileReader = new FileReader();
fileReader.onload = function (e) {
console.log(fileReader.result);
};
fileReader.onerror = function(err) {
console.log(err);
};
fileReader.readAsText($scope.files[i]);
}
};
Below is working on mine, this is typescript
You should convert FileEntry To Standard File, before passing to FileReader.
const convertFileEntryToStandardFile = async (files: any[]) => {
if (files) {
let plainFiles = [];
files.forEach(f=> {
let plainFile = readFile(f);
plainFiles.push(plainFile);
});
return plainFiles;
}
}
const readFile = async (fileEntry: any) => {
return await new Promise((resolve, reject) => fileEntry.file(resolve, reject));
}

JavaScript Exception/Error Handling Not Working

This might be a little hard to follow.
I've got a function inside an object:
f_openFRHandler: function(input) {
console.debug('f_openFRHandler');
try{
//throw 'foo';
DragDrop.FileChanged(input);
//foxyface.window.close();
}
catch(e){
console.error(e);
jQuery('#foxyface_open_errors').append('<div>Max local storage limit reached, unable to store new images in your browser. Please remove some images and try again.</div>');
}
},
inside the try block it calls:
this.FileChanged = function(input) {
// FileUploadManager.addFileInput(input);
console.debug(input);
var files = input.files;
for (var i = 0; i < files.length; i++) {
var file = files[i];
if (!file.type.match(/image.*/)) continue;
var reader = new FileReader();
reader.onload = (function(f, isLast) {
return function(e) {
if (files.length == 1) {
LocalStorageManager.addImage(f.name, e.target.result, false, true);
LocalStorageManager.loadCurrentImage();
//foxyface.window.close();
}
else {
FileUploadManager.addFileData(f, e.target.result); // add multiple files to list
if (isLast) setTimeout(function() { LocalStorageManager.loadCurrentImage() },100);
}
};
})(file, i == files.length - 1);
reader.readAsDataURL(file);
}
return true;
LocalStorageManager.addImage calls:
this.setItem = function(data){
localStorage.setItem('ImageStore', $.json_encode(data));
}
localStorage.setItem throws an error if too much local storage has been used. I want to catch that error in f_openFRHandler (first code sample), but it's being sent to the error console instead of the catch block. I tried the following code in my Firebug console to make sure I'm not crazy and it works as expected despite many levels of function nesting:
try{
(function(){
(function(){
throw 'foo'
})()
})()
}
catch(e){
console.debug(e)
}
Any ideas?
var reader = new FileReader();
reader.onload = (function(f, isLast) {
Likely that's your problem right there - the FileReader probably calls onload asynchronously, at a time when your try/catch is no longer in scope. There may be a separate error handler function available on the FileReader interface, or you might need to move the try/catch into the anonymous function you're passing to onread().
I think the problem is that the error is happening later, after your f_openFRHandler function has completed. Note that the function where LocalStorageManager.addImage is being called is being set as the onload handler on the reader, not being called immediately. It gets called later, asynchronously, when the data is loaded.
You'll need to put your try..catch inside the anonymous function being created and assigned to that event, e.g.:
this.FileChanged = function(input) {
// FileUploadManager.addFileInput(input);
console.debug(input);
var files = input.files;
for (var i = 0; i < files.length; i++) {
var file = files[i];
if (!file.type.match(/image.*/)) continue;
var reader = new FileReader();
reader.onload = (function(f, isLast) {
return function(e) {
try { // ADDED
if (files.length == 1) {
LocalStorageManager.addImage(f.name, e.target.result, false, true);
LocalStorageManager.loadCurrentImage();
//foxyface.window.close();
}
else {
FileUploadManager.addFileData(f, e.target.result); // add multiple files to list
if (isLast) setTimeout(function() { LocalStorageManager.loadCurrentImage() },100);
}
}
catch (err) { // ADDED
// YOUR HANDLING HERE
}
};
})(file, i == files.length - 1);
reader.readAsDataURL(file);
}
return true;
};
Your (excellent) test case makes the call synchronously, which is why the error is caught when you try it. This is a closer model to what's actually happening:
try{
(function(){
setTimeout(function(){ // Use setTimeout to model asynchronicity
throw 'foo'
}, 100);
})()
}
catch(e){
console.debug(e)
}

Categories

Resources