25

I can't manage to get both the result of the filereader and some parameters in a onload function. This is my code:

HTML of control:

<input type="file" id="files_input" multiple/>

Javascript function:

function openFiles(evt){
    var files = evt.target.files;
    for (var i = 0; i < files.length; i++) {
      var file=files[i];
      reader = new FileReader();
      reader.onload = function(){
          var data = $.csv.toArrays(this.result,{separator:'\t'});
      };
      reader.readAsText(file);
    }
  }

Add event:

 files_input.addEventListener("change", openFiles, false);

I use the filereader.result, in the onload function. If I use a parameter, like file, for this function, I can't not access to the result anymore. For example I'd like to use file.name in the onload function. How to resolve this issue ?

Alex
  • 11,115
  • 12
  • 51
  • 64
PatriceG
  • 3,851
  • 5
  • 28
  • 43
  • does it work with one file? Try creating different readers, add "var reader = new FileReader();" – Jon Ander Dec 12 '14 at 08:03
  • Use a simple rule: Declare all your local variables at the beginning of the closure. Don't declare them inside `for`, `in`, and other similar statements, because these statements do not create closures and consequently the declared variables are not local to the statement. This will help you determine when you need to create additional closures. – hon2a Dec 18 '14 at 10:51

6 Answers6

61

Try wrapping your onload function in another function. Here the closure gives you access to each file being processed in turn via the variable f:

function openFiles(evt){
    var files = evt.target.files;

    for (var i = 0, len = files.length; i < len; i++) {
        var file = files[i];

        var reader = new FileReader();

        reader.onload = (function(f) {
            return function(e) {
                // Here you can use `e.target.result` or `this.result`
                // and `f.name`.
            };
        })(file);

        reader.readAsText(file);
    }
}

For a discussion of why a closure is required here see these related questions:

Community
  • 1
  • 1
Chris
  • 44,602
  • 16
  • 137
  • 156
  • 3
    If browser has `FileReader`, I think it supports forEach and bind :) I think better use bind for adding file to onload callback – Alex Dec 12 '14 at 09:15
  • @Pinal that is a good suggestion. I won't update my answer to include these, since I wanted to keep my answer as close to the code supplied by the OP and only change what is necessary (introduce the closure), but it is good to be aware of these features. – Chris Dec 12 '14 at 09:18
  • In a similar vain, in production code I'd be tempted to move the FileReader code into it's own named function called from the loop. Closures would behave as expected and it helps break down the code. That said the example as given is more appropriate for demonstrating the technical solution. – Hargo Dec 18 '14 at 22:11
  • 2
    if i could i would upvote this answer 1000 times. Thank you ! – Kris Nov 05 '15 at 11:05
  • Thanks mate! Helped me a lot! –  May 30 '17 at 07:33
  • I seem to find that f is not accessible from within the return function – Steve May 15 '18 at 10:56
  • Thank you so much, still the best solution in 2019. – Alexandre Dec 31 '19 at 08:55
7

You should use closure at 'onload' handler. Example: http://jsfiddle.net/2bjt7Lon/

reader.onload = (function (file) { // here we save variable 'file' in closure
     return function (e) { // return handler function for 'onload' event
         var data = this.result; // do some thing with data
     }
})(file);
sergolius
  • 428
  • 3
  • 8
5

Use

var that = this;

to access external variables in the function scope.

function(){
    that.externalVariable //now accessible using that.___
}

My scenario - Using Angular 9. I struggled with this for a long time, I just couldn't seem to get it to work. I found the following to be a really elegant solution to access external variables inside a function() block.

public _importRawData : any[];

importFile(file){
    var reader = new FileReader();
    reader.readAsArrayBuffer(file);

    var data;
    var that = this;  //the important bit

    reader.onloadend = await function(){
        //read data

        that._importRawData = data;  //external variables are now available in the function
    }

One of the important parts in the above code is the var keyword, which scopes variables outside the function block. However, when I accessed the value of data after the function block, it was still undefined as the function executed after the other code. I tried async and await, but could not get it to work. And I could not access data outside of this function.

The saving grace was the var that = this line. Using that allows external variables to be accessed inside the function. So I could set that variable inside the function scope and not worry about when the code gets executed. As soon as it has been read, it is available.

For the original question the code would be:

function openFiles(evt){
    var files = evt.target.files;
    for (var i = 0; i < files.length; i++) {
        var file=files[i];
      
        var that = this; //the magic happens
      
        reader = new FileReader();
        reader.onload = function(){
            var data = $.csv.toArrays(this.result,{separator:'\t'});
          
            that.file.name //or whatever you want to access.
        };
        reader.readAsText(file);
    }
}
fcdt
  • 2,371
  • 5
  • 14
  • 26
3

Event handling is asynchronous and thus they pick up the latest value of all the enclosed local variables(i.e. closure). To bind a particular local variable to the event, you need to follow the code suggested by users above or you can look at this working example:-

http://jsfiddle.net/sahilbatla/hjk3u2ee/

function openFiles(evt){
  var files = evt.target.files;
  for (var i = 0; i < files.length; i++) {
    var file=files[i];
    reader = new FileReader();
    reader.onload = (function(file){
      return function() {
        console.log(file)
      }
    })(file);
    reader.readAsText(file);
  }
}

#Using jQuery document ready
$(function() {
  files_input.addEventListener("change", openFiles, false);
});
sahilbathla
  • 519
  • 3
  • 10
  • This isn't quite right. You should assign a function to `reader.onload`. The IIFE you are invoking doesn't return anything, so no code will run once the reader has fired its load event. – Chris Dec 18 '14 at 21:51
  • I don't clearly understand why returning a function is important, when the piece of code will run only on the onload event and has file binded to it – sahilbathla Dec 19 '14 at 04:35
  • Currently, your function `(function(file){console.log(file)})(file);` is run once for each iteration of your `for` loop. It doesn't return anything, so `reader.onload = undefined` for each interation of the loop. Therefore, while your `console.log` does print the correct file name, nothing will run when `reader` has fired it's `load` event. – Chris Dec 19 '14 at 08:17
  • Thanks for highlighting that, its my bad, I should fix this – sahilbathla Dec 19 '14 at 11:56
3

For Typescript;

for (var i = 0; i < files.length; i++) {
  var file = files[i];

  var reader = new FileReader();

  reader.onload = ((file: any) => {
    return (e: Event) => {
      //use "e" or "file"
    }
  })(file);

  reader.readAsText(file);
}
Muhammet Can TONBUL
  • 3,217
  • 3
  • 25
  • 37
-1

As the variable file is within the scope, you may use the file variable without passing it to function.

function openFiles(evt){
    var files = evt.target.files;
    for (var i = 0; i < files.length; i++) {
          var file=files[i];    
          reader = new FileReader();
          reader.onload = function(){
              alert(file.name);
              alert(this.result);
          };
      reader.readAsText(file);
    }
  }

files_input.addEventListener("change", openFiles, false);
<input type="file" id="files_input" multiple/>
Banu
  • 9
  • Thanks yeah got it :) But this seems to be working when I tested by displaying each file name and content. Run Code Snippet above. I guess I am missing something will see. – Banu Dec 12 '14 at 10:06
  • The above code works in this case because the onload event occurs as part of the iteration (reader.readAsText). Whereas in http://stackoverflow.com/questions/1451009 onclick event occurs after the iteration is completed – Banu Dec 12 '14 at 10:19
  • Whilst this may appear to work, when you run the above snippet and select multiple files the last file in the list is displayed multiple times. So this does not work as expected. – Chris Dec 12 '14 at 14:01