Programmatically generated/activated file input doesn't always fire `input` event

Viewed 552

I have a button on my web application, which has the following code in the click event handler:

const fileInputEl = document.createElement('input');
fileInputEl.type = 'file';
fileInputEl.accept = 'image/*';

fileInputEl.addEventListener('input', (e) => {
  if (!e.target.files.length) {
    return;
  }

  // Handle files here...
});  

fileInputEl.dispatchEvent(new MouseEvent('click'));

Sometimes (about 1 out of 8), after selecting the file, the input event doesn't fire after choosing a file. I'm guessing this is a browser bug around the lifecycle of the element.

Any way around this short of appending the element to the page and removing it later? What's the proper way to handle this in modern browsers these days?

I'm testing with Google Chrome on Windows.

JSFiddle: http://jsfiddle.net/pja1d5om/2/

3 Answers

Citate from your question: Sometimes (about 1 out of 8), after selecting the file, the input event doesn't fire after choosing a file.

I can confirm this behavior with input and with change events using Opera (ver. 55.0.2994.61, newest version at this time) which uses Google Chrome browser engine "Blink". It happens about 1 out of 25.

Solution

This happens because sometimes your input element object was deleted after file dialog closing because it is not in using anymore. And when it happens you have not the target which could receive input or change event.

To solve this just add your input element somewhere to the DOM after creating as hidden object like follows:

fileInputEl.style.display = 'none';
document.body.appendChild(fileInputEl);

And then when the event was fired you can delete it like follows:

document.body.removeChild(fileInputEl);

Full example

function selectFile()
{
    var fileInputEl = document.createElement('input');
    fileInputEl.type = 'file';
    fileInputEl.accept = 'image/*';
    //on this way you can see how many files you select (is for test only):
    fileInputEl.multiple = 'multiple';

    fileInputEl.style.display = 'none';
    document.body.appendChild(fileInputEl);

    fileInputEl.addEventListener('input', function(e)
    {
        // Handle files here...
        console.log('You have selected ' + fileInputEl.files.length + ' file(s).');
        document.body.removeChild(fileInputEl);
    });  

    try
    {
        fileInputEl.dispatchEvent(new MouseEvent('click'));
    }
    catch(e)
    {
        console.log('Mouse Event error:\n' + e.message);
        // TODO:
        //Creating and firing synthetic events in IE/MS Edge:
        //https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/dn905219(v=vs.85)
    }
}
<input type="button" onclick="selectFile()" value="Select file">

Citate from your bounty description: Bounty will be awarded to someone who ... show an appropriate workaround.

My old suggested workaround (now irrelevant)

We can use setInterval function to check if input value was changed. We save intervalID in our new fileInputEl as property. Because we always create a new file input element then its value is always empty on start (on each button click). And if this value was changed we can detect it when we compare it with empty string. And when it happens then we pass our fileInputEl to fileInputChanged() function and clear/stop our interval function.

function selectFile()
{
    var fileInputEl = document.createElement('input');
    fileInputEl.type = 'file';
    fileInputEl.accept = 'image/*';
    //on this way you can see how many files you select (is for test only):
    fileInputEl.multiple = 'multiple';

    fileInputEl.intervalID = setInterval(function()
    {
        // because we always create a new file input element then
        // its value is always empty, but if not then it was changed:
        if(fileInputEl.value != '')
            fileInputChanged(fileInputEl);
    }, 100);

    try
    {
        fileInputEl.dispatchEvent(new MouseEvent('click'));
    }
    catch(e)
    {
        console.log('Mouse Event error:\n' + e.message);
        // TODO:
        //Creating and firing synthetic events in IE/MS Edge:
        //https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/dn905219(v=vs.85)
    }
}

function fileInputChanged(obj)
{
    // Handle files here...
    console.log('You have selected ' + obj.files.length + ' file(s).');
    clearInterval(obj.intervalID);
}
<input type="button" onclick="selectFile()" value="Select file">

It seems this is a browser bug/fluke and likely has something to do with garbage collection. I can get around it by adding the file input to the document:

fileInputEl.style.display = 'none';
document.querySelector('body').appendChild(fileInputEl);

When done, it can be cleaned up with:

fileInputEl.remove();

That is a very interesting bug you got there, I have not been able to reproduce it.

Is there a reason why you are approaching a file input the way you are doing? Is it because you want to try and style it?

I read this article, and I applied what it was doing. I found that this worked fine. What this article tries to do is to have a label connected to the input with a for attribute on the label tag. Then in CSS and JavaScript, the file input tag is hidden and the label acts as the "button" in essence.

For example...

Note I did make some changes to the code but all credit to Osvaldas Valutis who is the writer of the article mentioned above at CoDrops.

var inputs = document.querySelectorAll('.inputfile');

inputs.forEach(input => {

  var label = input.nextElementSibling,
    labelVal = label.innerHTML;

  input.addEventListener('change', function(e) {

    var fileName = '';

    if (this.files && this.files.length > 1)
      fileName = (this.getAttribute('data-multiple-caption') || '').replace('{count}', this.files.length);
    else
      fileName = e.target.value.split('\\').pop();

    if (fileName)
      label.querySelector('span').innerHTML = fileName;
    else
      label.innerHTML = labelVal;

  });

});
* {
  font-family: sans-serif;
  font-weight: 300;
}

.inputfile {
  display: none;
}

.inputfile+label {
  font-size: 1.25em;
  font-weight: 700;
  color: white;
  background-color: darkred;
  display: inline-block;
  padding: 10px;
  border-radius: 10px;
  border: 1px darkred solid;
  cursor: pointer;
}

.inputfile+label:hover {
  background-color: darkred;
}
<input type="file" name="file" id="file" class="inputfile" data-multiple-caption="{count} files selected" multiple />
<label for="file">Choose a file <span></span></label>

Now I know that this may not be exactly what you are looking for, but this could be an alternative solution you could try.

Related