Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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/

like image 712
Brad Avatar asked Aug 30 '18 18:08

Brad


2 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://learn.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://learn.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">
like image 155
Bharata Avatar answered Oct 23 '22 06:10

Bharata


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();
like image 1
Brad Avatar answered Oct 23 '22 05:10

Brad