Lewis Anesa
Lewis Anesa

Reputation: 104

"this" keyword application on nested functions

I have this code :

<!DOCTYPE html>

<html>
    <head>
        <title>Drag-Drop tests</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <script>
            var body = document.body;
            
            var cursor = document.createElement("div");
            cursor.innerText = "Contenus des fichiers :\n";
            
            cursor.ondragover = e => e.preventDefault();
            cursor.ondrop = function(e) {
                e.preventDefault();
                
                if (e.dataTransfer.items) {
                    for (var i = 0; i < e.dataTransfer.items.length; i++) {
                        if (e.dataTransfer.items[i].kind === "file") {
                            var file = e.dataTransfer.items[i].getAsFile();
                            
                            file.cursor = document.createElement("p");
                            body.appendChild(file.cursor);
                            
                            file.cursor.innerText = file.name + " contient :";
                            
                            file.text().then(function(value) {
                                file.cursor.innerText += " " + value;
                            });
                        }
                    }
                }
                else {
                    for(var i = 0; i < e.dataTransfer.files.length; i++) {
                        var file = e.dataTransfer.files[i];
                            
                        file.cursor = document.createElement("p");
                        body.appendChild(file.cursor);
                        
                        file.cursor.innerText = file.name + " contient :";
                        
                        file.text().then(function(value) {
                            file.cursor.innerText += " " + value;
                        });
                    }
                }
            };
            
            body.appendChild(cursor);
        </script>
    </body>
</html>

As is, if I drop two files on the div element, I get this output :

Contenus des fichiers :

File1.txt contient :

File2.txt contient : Content of file 1 Content of file 2

In file.text().then function, "file" refers to the last file reference declared.

If I replace file.cursor.innerText += by this.cursor.innerText += I get this output :

Contenus des fichiers :
Content of file 1 Content of file 2

File1.txt contient :

File2.txt contient :

In file.text().then function, "this" refers to the first caller, that is, the div itself.

Is there any way to get this :

Contenus des fichiers :

File1.txt contient : Content of file 1

File2.txt contient : Content of file 2

Keeping anonymous nested functions as param for callers.

I know that then does not occurs at the time I define it's callback. I would like to know if I can attach some data to an object and retrieve it on a callback execution.

Thx by advance.

Upvotes: 0

Views: 76

Answers (4)

Lewis Anesa
Lewis Anesa

Reputation: 104

Okay,

Another answer pointed out that "var" scopes the function and not the block.

Replacing "var" by "let" anywhere makes the keyword "this" in file.text().then callback not point anything beacuse of the execution flow.

Anyway, "let" makes "file" of each for iteration independant.

Therefore I have this code working :

<!DOCTYPE html>

<html>
    <head>
        <title>Drag-Drop tests</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <script>
            let body = document.body;
            
            let cursor = document.createElement("div");
            cursor.innerText = "Contenus des fichiers :\n";
            
            cursor.ondragover = e => e.preventDefault();
            cursor.ondrop = e => {
                e.preventDefault();
                
                let fileLoad = file => {
                    file.cursor = document.createElement("p");
                    body.appendChild(file.cursor);
                    
                    file.cursor.innerText = file.name + " contient :";
                    file.text().then(value => file.cursor.innerText += " " + value);
                }
                
                if(e.dataTransfer.items)
                    for(let item of e.dataTransfer.items)
                        if(item.kind === "file") fileLoad(item.getAsFile());
                else
                    for(let file of e.dataTransfer.files)
                        fileLoad(file);
            };
            
            body.appendChild(cursor);
        </script>
    </body>
</html>

And at the end, I don't really care of code optimisation (for the amount of line code).

I will generate it with some C code.

Upvotes: 0

Thomas
Thomas

Reputation: 12637

In file.text().then function, "file" refers to the last file reference declared.

You come from a C background. In JS vars are function scoped and get hoisted. Better use the newer keywords let and const for block scoped variables.

What's the difference between using “let” and “var”?

In file.text().then function, "this" refers to the first caller, that is, the div itself.

this in JS is notorious; especially for people already having expectations on how it should behave. In JS this is context-sensitive and depends on how you call a function/method.

How does the “this” keyword work?

Sidenote: I see you wrote some code twice, take a look at Iterators and Generators. I use them to "normalize" whatever is available in dataTransfer into a sequence of Files.

var body = document.body;

var cursor = document.createElement("div");
cursor.innerText = "Contenus des fichiers :\n";

// https://mdn.io/Generator
function* getFiles(dataTransfer) {
  if (dataTransfer.items) {
    // https://mdn.io/for...of
    for (let item of dataTransfer.items) {
      if (item.kind === "file") {
        // https://mdn.io/yield
        yield item.getAsFile();
      }
    }
  } else {
    // https://mdn.io/yield*
    yield* dataTransfer.files;
  }
}

cursor.ondragover = e => e.preventDefault();
cursor.ondrop = function(e) {
  e.preventDefault();

  for (const file of getFiles(e.dataTransfer)) {
    const fileCursor = document.createElement("p");
    fileCursor.innerText = file.name + " contient :";

    body.appendChild(fileCursor);

    file.text().then(text => fileCursor.append(text));
  }
};

body.appendChild(cursor);

Edit:

I don't even seen any let, const, function*, yeld and yeld*.

In 2020/21 that's basically a fail on the side of the course. const and let have been introduced with ES2015 (Javascript version) and are implemented in basically every browser for years.

How would you name such a subtility that consists of adding an asterisk after "fonction" or "yeld" keyword?

The general topic is Iterators and Generators. I've added some specific URLs in the code snippet above to the different keywords.

Upvotes: 1

Darius
Darius

Reputation: 61

JSFiddle: https://jsfiddle.net/7q9Lphjf/1/

You are getting this result because += " " + value happens after the promise is executed. In other words, in your example code the following steps happen:

  1. Add "File 1 contient" to the div.
  2. Start reading file 1
  3. Add "File2 contient" to the div.
  4. Start reading file 2

[...] a few moments later

  1. file1 reading is complete, so the promise is resolved and the contents of file1 are appended to the div
  2. file2 reading is complete, so the promise is resolved and the contents of file2 are appended to the div

In this particular case it also has to do with the scope of the file variable which is changed in the for loop, so by the time promise1 is resolved, file already references the second paragraph, so file.cursor.innerText refers to the second paragraph's contents.

Instead of :

file.cursor = document.createElement("p");
body.appendChild(file.cursor);
file.cursor.innerText = file.name + " contient :";
                            
file.text().then(function(value) {
  file.cursor.innerText += " " + value;
});

Use:

file.text().then(function(value) {
  file.cursor = document.createElement("p");
  body.appendChild(file.cursor);
  file.cursor.innerText = file.name + " contient :";
  file.cursor.innerText += " " + value;
});

This solution will however not guarantee any particular order. Whenever the promise is resolved for a file (file reading is complete), only then the contents of that file be added to the div.

If you absolutely need a specific order in which the contents are appended, there are a number of possible solutions to also achieve that, for example promise chaining.

However I'm not sure if this is a requirement, so I'll not make this answer longer than it already is :)

Upvotes: 0

Jeremy Thille
Jeremy Thille

Reputation: 26360

.text() is asynchronous. All the .then() occur after the whole for loop has ended. Very classic asynchronous-operation-in-a-loop problem. You just need to await instead of using the (old school) .then() syntax.

cursor.ondrop = async function(e) {
     // ...
     for(...) {
         const value = await file.text();
         file.cursor.innerText += " " + value;
     }
}

Upvotes: 0

Related Questions