Callback function after reading multiple files - javascript

I am doing something similar to http://www.html5rocks.com/en/tutorials/file/dndfiles/
What I'm doing is Im reading the contents of the selected files one at a time to validate that their lines pass some regex test. After done validating all files, I need to update (enable / disable) some buttons accordingly hence the call back function
Is it possible to have a call back function which will do something after everything is read?
HTML:
<input type="file" id="files" name="files[]" multiple />
Javascipt:
<script>
function handleFileSelect(evt) {
var files = evt.target.files; // FileList object
// files is a FileList of File objects. List some properties.
var validArray = [];
for (var i = 0, f; f = files[i]; i++) {
//Create new file reader
var r = new FileReader();
//On load call
r.onload = (function (f) {
return function (e) {
var contents = e.target.result;
var lines = contents.split('\n');
for(var i=0; i<lines.length; i++){
//Validate regex of line here
//If line does not pass, append file name to validArray and break
}
};
})(f);
r.readAsText(f);
}
}
document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

Came here looking for a similar answer. I wanted to call a function after all files were loaded and processed. The solution provided by #Snuffleupagus did not work for me because the function was called after all the files were read, but before they had finished being processed in the onload function. I found a solution around this as follows (not sure if it is the 'cleanest' but it works for me).
var processedCount=0; // global variable
var totalFiles = 0; // global variable
function handleFileSelect(evt) {
var files = evt.target.files; // FileList object
totalFiles = files.length; // important
// files is a FileList of File objects. List some properties.
for (var i = 0, f; f = files[i]; i++) {
//Create new file reader
var r = new FileReader();
//On load call
r.onload = (function(theFile){
return function(){
onLoadHandler(this,theFile);
onLoadEndHandler();
};
})(f);
r.readAsText(f);
}
}
function onLoadEndHandler(){
processedCount++;
if(processedCount == totalFiles){
// do whatever - this code will run after everything has been loaded and processed
}
}
I tried to use r.onloadend but it was called too soon. I believe because my function 'onLoadHandler' takes a few seconds to process each file and onloadend is called when the file is done being loaded but before the code within 'onload' has finished running.

Absolutely. Callbacks are just passed as any other normal argument would be, so we'll end up adding another argument to handleFileSelect and changing the event listener to an anonymous function that calls handleFileSelect with the extra argument.
I set up a fiddle to give you a quick working demo.
function handleFileSelect(evt, cb) {
var files = evt.target.files; // FileList object
// files is a FileList of File objects. List some properties.
var output = [];
for (var i = 0, f; f = files[i]; i++) {
output.push('<li><strong>'+ escape(f.name) + '</strong>');
}
document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
if(cb) cb();
}
document.getElementById('files').addEventListener('change', function(e){handleFileSelect(e, function(){alert('all done');})}, false);​
Breaking it down - added an extra argument to handleFileSelect and at the end added if(cb) cb();. That just checks to see if cb exists, if it does, run it as a function.
Then when we go to bind the event handler instead of passing a reference to handleFileSelect we use an anonymous function - this lets us pass our extra argument.
The anonymous function inside of the anonymous function is just our callback, it could be a reference to a function if you'd rather.

A really clean way to do this is to use async.js reduce method. Async.js gives many nice ways to deal with multiple callbacks. You could use reduce to iterate through the array of file names, and build a reduced value which is an array of the valid lines:
<input type="file" id="files" name="files[]" multiple />
<script type='text/javascript' src='https://github.com/caolan/async/raw/master/lib/async.js'/>
<script>
var isValidLine = function(text){
// todo: implement
}
function handleFileSelect(evt) {
var files = evt.target.files; // FileList object
// reduce by starting with empty array in second argument -
// this gets built up with the valid array lines
async.reduce(files, [], function(validLinesSoFar, file, callback){
var r = new FileReader();
// Read file here:
r.onload = function (f) {
var contents = f.target.result;
var lines = contents.split('\n');
for(var i=0; i<lines.length; i++){
if isValidLine(lines[i])
validLinesSoFar.push(lines[i]);
}
callback(null, validLinesSoFar);
};
r.readAsText(file);
}, function(err, validLines){
// gets called after every file iterated through
// result is entire valid array
// do something here with valid array
});
}
document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

I would take a look at jQuery's deferred object
Also a very relevant question that might be applicable to you.
How to fire a callback function after a for-loop is done in Jquery?

Related

Add tsv file upload button to Jupyter Notebook using JavaScript

So I am using Jupyter 4.x and python 3.5, trying to "upload" a .tsv, but really just trying to capture it as a string and then use
setTimeout(function(){IPython.notebook.kernel.execute("stringData=StringIO(+"fr.result")");},5000);
to allow time for the FileReader() object to complete the binary-string conversion then save the string into a python variable. I tested that the Filereader() was working by printing the entire .tsv file in the console. For some reason, though, stringData remains undefined on the python side.Here is the javascript cell:
%%HTML
<input type="file" id="CSVFileInput" onchange="handleFiles(this.files)" value="upload csv">
<script>
var inputElement=document.getElementById('CSVFileInput');
function handleFiles() {
var file = inputElement.files[0];
var fr = new FileReader();
fr.readAsText(file);
var outputString=fr.result;
var command = "dataString ='"+outputString+"'";
setTimeout(function(){
IPython.notebook.kernel.execute(command);}
,5000);
}
inputElement.addEventListener("change", handleFiles, false);
</script>
And in the next cell I test the output and get NameError because dataString is undefined, here is the next cell:
dataString
Also, I'm kind of new to javascript, so any and all advice is welcome, I only think this is the easy way. Pls? And, of course, thanks a lot!
FileReader methods are asynchronous so there is no data loaded at the time you try to set outputString to the result.
The proper way to handle this is to use the load event handler, so instead of using setTimeout(), which is unreliable for asynchronous handling, you can modify the code to the following:
function handleFiles() {
var file = this.files[0]; // "this" is the calling element
var fr = new FileReader();
fr.onload = function() {
var outputString = this.result; // here the data is ready. Now "this" = fr
var command = "dataString ='" + outputString + "'";
IPython.notebook.kernel.execute(command);
};
fr.readAsText(file); // invoked asynchronously
}
Also remove the inline JavaScript in the HTML:
<input type="file" id="CSVFileInput" onchange="handleFiles(this.files)" value="upload csv">
to
<input type="file" id="CSVFileInput" title="upload csv">
(value has no effect when input is type=file, use title instead). Then use the following code to take care of event handling (after the DOM has loaded):
document.getElementById("CSVFileInput").addEventListener("change", handleFiles);
function handleFiles() {
var file = this.files[0]; // "this" is the calling element
var fr = new FileReader();
fr.onload = function() {
var outputString = this.result; // here the data is ready. Now "this" = fr
var command = "dataString ='" + outputString + "'";
//IPython.notebook.kernel.execute(command);
console.log("Loaded file. Command:", command);
};
fr.readAsText(file); // invoked asynchronously
}
document.getElementById("CSVFileInput").addEventListener("change", handleFiles);
<input type="file" id="CSVFileInput" title="upload csv">
So, #K3N definitely gave me a valuable piece of the puzzle and lesson in async functions. However, the main issue was that python did not recognize the string inputs it was receiving from javascript, so I thought I'd share my journey with all. I ended up converting the string to a 2d javascript array, then I shift() the first row off for the column names, transpose the remaining rows, and clean out all the dumb stuff from english that stops it from working (quotes and apostrophes). Now I can pd.DataFrame(dict(zip(colNames,cols))) and run all the calculations I run on the same .tsv when I read it in from my filesystem. Here is the full script, basically the fixes #K3N showed me plus the pythonify(arr) function:
<input type="file" id="TSVFileInput" title="upload tsv">
function handleFiles() {
//read in file and instantiate filereader
let file = this.files[0];// "this" is the calling element
let fr = new FileReader();
fr.onload = function() {
//split on row delimeter (CRLF)
let outputBuffer = this.result.split("\r\n");
let command;
// split outputBuffer into 2d array
outputBuffer= outputBuffer.map(line => line.split("\t"));
//pop names row from output Buffer
let names=outputBuffer.shift();
//optimized transpose
outputBuffer=outputBuffer.reduce(
(temp,row) =>
row.map((element,i) =>
(temp[i] || []).concat(element))
,[] //initializes temp
);
//build python command
command="colNames ="+pythonify(names);
//send command to notebook kernel
IPython.notebook.kernel.execute(command);
//loop appends columns on python side
for(let i=0 ; i< outputBuffer.length ; i++){
command="cols.append("+pythonify(outputBuffer[i])+")";
IPython.notebook.kernel.execute(command); //send command to kernel
}
}; //end fr.onload()
fr.readAsText(file); // invoked asynchronously, triggers fr.onload
}//end handleFiles()
function pythonify (arr){
//turns javascript array into string representation of python list
let out= '[';
for(let i=0 ; i<arr.length ; i++){
var element=arr[i];
//format double and single quotes
element=element.replace(/\"/g,'\\"').replace(/'/g,"\\'");
//use python raw string
out+='r"'+element+'"';
if(i<arr.length-1){ //skip last comma
out+=',';
}
}
out+=']';
return out;
}// end pythonify(arr)
document.getElementById("CSVFileInput").addEventListener("change", handleFiles);

I want to load images withen the folder and want to get all images in base64 in my js for future use

i am facing the issue i always get the last image in my image array due to kind of Filereader library function onloadend.
how can i get base64 for all images in my folder.
<input id="file-input" multiple webkitdirectory type="file" />
var input = document.getElementById('file-input');
var file_names = "";
var entries_length = 0;
var entries_count = 0;
var image = new Array();
var obj = {};
var j = 0;
input.onchange = function(e) {
var files = e.target.files; // FileList
entries_length = files.length;
console.log(files);
for (var i = 0, f; f = files[i]; ++i){
console.log("i:"+i);
entries_count = entries_count + 1;
//console.debug(files[i].webkitRelativePath);
if(files[i].type=="image/jpeg")
{
var string = files[i].webkitRelativePath;
var name = string.split("/")[3]; //this is because my image in 3rd dir in the folder
var reader = new FileReader();
reader.onloadend = function() {
obj.name = string.split("/")[3];
obj.image = reader.result;
image[j] = obj;
j = j+1;
}
reader.readAsDataURL(files[i]);
}
}
console.log(image);
}
The issue is caused by the asynchronous loading of files. You iterate over the array and set the onloadend handler for the reader each time, then start loading by calling readAsDataURL.
One problem is that by the time your first image loads, it is possible the for loop has completed, and i is already at the last index of the array.
At this point, obtaining the path from files[i].webkitRelativePath will give you the last filename, and not the one you are expecting.
Check the example for readAsDataURL on MDN to see one possible solution - each load is performed in a separate function, which preserves its scope, along with file.name. Do not be put off by the construction they are using: [].forEach.call(files, readAndPreview). This is a way to map over the files, which are a FileList and not a regular array (so the list does not have a forEach method of its own).
So, it should be sufficient to wrap the loading logic in a function which takes the file object as a parameter:
var images = [];
function loadFile(f) {
var reader = new FileReader();
reader.onloadend = function () {
images.push({
name : f.name, // use whatever naming magic you prefer here
image : reader.result
});
};
reader.readAsDataURL(f);
}
for (var i=0; i<files.length; i++) {
loadFile(files[i]);
}
Each call of the function 'remembers' the file object it was called with, and prevents the filename from getting messed up. If you are interested, read up on closures.
This also has the nice effect of isolating your reader objects, because I have a sneaking suspicion that, although you create a new 'local' reader each iteration, javascript scoping rules are weird and readers could also be interfering with each other (what happens if one reader is loading, but in the same scope you create a new reader with the same variable name? Not sure).
Now, you do not know how long it would take for all images to be loaded, so if you want to take an action right after that, you would have to perform a check each time an onloadend gets called. This is the essence of asynchronous behavior.
As an aside, I should note that it is pointless to manually keep track of the last index of images, which is j. You should just use images.push({ name: "bla", image: "base64..." }). Keeping indices manually opens up possibilities for bugs.

Wait until all files are read asynchronously (FileReader) and then run code

I have a page where the user can select a folder to upload files. Before sending the files, I need to read them and check the data. My code is organized as follows:
$( '#folder-select' ).on('change', getValidFileList);
var fileList = [];
var getValidFileList = function(event) {
//Get the selected files
files = $( this ).get(0).files;
for(var i=0; i<files.length; i++) {
checkFile(files[i]);
}
//Do something with the final fileList
console.log(fileList);
};
var checkFile = function(file) {
var reader = new FileReader();
reader.onload = function (event) {
//Here I parse and check the data and if valid append it to fileList
};
reader.readAsArrayBuffer(file);
};
I would like to take the resulting fileList array to keep processing/displaying the uploaded files. I found that reader.onload() is called asynchronously, so the result of the console.log(fileList) after the for loop is an empty array (it is executed before the reader.onload() is fired). Is there any way to wait until all files are read and appended to fileList?
Just keep track of how many files has been processed compared to how many files has been given:
function getValidFileList(files, callback) {
var count = files.length; // total number of files
var fileList = []; // accepted files
//Get the selected files
for(var i = 0; i < count; i++) { // invoke readers
checkFile(files[i]);
}
function checkFile(file) {
var reader = new FileReader();
reader.onload = function(event) {
var arrayBuffer = this.result;
//Here I parse and check the data and if valid append it to fileList
fileList.push(arrayBuffer); // or the original `file` blob..
if (!--count) callback(fileList); // when done, invoke callback
};
reader.readAsArrayBuffer(file);
}
};
The --count will subtract one per reader onload hit. When =0 (or !count) it invokes the callback. Notice that the array order may not be the same as the one from files[n] it this should matter.
Then invoke it like this:
$( '#folder-select' ).on('change', function() {
getValidFileList(this.files, onDone)
});
function onDone(fileList) {
// continue from here
}

Load multiple JSON files in pure JavaScript

I am new to JavaScript. I have already understood how to create an object from a JSON-file with JSON.Parse() and now I need to load multiple local JSONs into an array. I've been googling my problem for a while, but everything that I found was related to single JSON files.
Is there any way to do this in pure JavaScript without any libraries like jQuery and etc.?
P.S.: There is no need to work with web-server or else, the code is running locally.
To do this, you need to first get the actual files. Then, you should parse them.
// we need a function to load files
// done is a "callback" function
// so you call it once you're finished and pass whatever you want
// in this case, we're passing the `responseText` of the XML request
var loadFile = function (filePath, done) {
var xhr = new XMLHTTPRequest();
xhr.onload = function () { return done(this.responseText) }
xhr.open("GET", filePath, true);
xhr.send();
}
// paths to all of your files
var myFiles = [ "file1", "file2", "file3" ];
// where you want to store the data
var jsonData = [];
// loop through each file
myFiles.forEach(function (file, i) {
// and call loadFile
// note how a function is passed as the second parameter
// that's the callback function
loadFile(file, function (responseText) {
// we set jsonData[i] to the parse data since the requests
// will not necessarily come in order
// so we can't use JSONdata.push(JSON.parse(responseText));
// if the order doesn't matter, you can use push
jsonData[i] = JSON.parse(responseText);
// or you could choose not to store it in an array.
// whatever you decide to do with it, it is available as
// responseText within this scope (unparsed!)
}
})
If you can't make an XML Request, you can also use a file reader object:
var loadLocalFile = function (filePath, done) {
var fr = new FileReader();
fr.onload = function () { return done(this.result); }
fr.readAsText(filePath);
}
You can do something like this:
var file1 = JSON.parse(file1);
var file2 = JSON.parse(file2);
var file3 = JSON.parse(file3);
var myFileArray = [file1, file2, file3];
// Do other stuff
// ....
// Add another file to the array
var file4 = JSON.parse(file4);
myFileArray.push(file4);
If you already have an array of un-parsed files you could do this:
var myFileArray = [];
for(var i=0; i<unparsedFileArray.length; i++){
myFileArray.push(JON.parse(unparsedFileArray[i]));
}

How to run code on last iteration of html5 read file method?

In this javascript/jquery code I attempt to read multiple files and store them in a dictionary.
function handleFileSelect(evt) {
var files = evt.target.files; // FileList object
var f, filename;
for (var i = 0; i<files.length; i++) {
f = files[i];
filename = escape(f.name);
if (filename.toLowerCase().endsWith(".csv")) {
var reader = new FileReader();
// Closure to capture the file information.
reader.onload = (function(e) {
var text = reader.result;
var arrays = $.csv.toArrays(text);
frequencies[filename] = arrays;
generateMenuFromData();
});
// Read in the image file as a data URL.
reader.readAsText(f);
}
}
}
I read only the .csv files. I want to run generateMenuFromData(); only on the last time the reader.onload function runs.
I can't find a good way to do this properly. Does anyone know how?
Thanks.
Increase a counter inside the event handler. If it is the same the length of the array, execute the function. A more structured approach would be to use promises, but in this simple case it would suffice:
function handleFileSelect(evt) {
var files = evt.target.files;
var f, filename, loaded = 0;
for (var i = 0; i<files.length; i++) {
f = files[i];
filename = escape(f.name);
if (filename.toLowerCase().endsWith(".csv")) {
var reader = new FileReader();
reader.onload = (function(filename, reader) {
return function(e) {
frequencies[filename] = $.csv.toArrays(reader.result);
loaded += 1; // increase counter
if (loaded === files.length) {
// execute function once all files are loaded
generateMenuFromData();
}
};
}(filename, reader)); // <-- new scope, "capture" variable values
reader.readAsText(f);
}
}
}
Now, your real problem might be that you are creating a closure inside the loop. That means when the load event handlers are called, filename and reader will refer to the values the variable had in the last iteration of the loop. All handlers share the same variables.
See also Javascript closure inside loops - simple practical example.

Categories

Resources