We’re using the new elm/file
package at work, and as we are completing our first project that uses it, I noticed that file selection fails in some situations (Elm fails to receive the file selected by the OS). Searching for this led me to this Discourse thread: [elm/file] Selecting files using File.Select.file(s) "sometimes" fails and this GitHub issue.
I am reposting my reply to the GitHub issue here in order to gather more feedback from more people.
I just did some testing based off of what @evancz suggested in the Discourse topic, to quote him:
Can someone test this with just JS code to see:
- If the bug reproduces with the exact same JS and no Elm code.
- If so, does adding it to the DOM and then removing it in the event handler make it more reliable?
The existing version of the Elm.Kernel.File
function is:
function _File_uploadOne(mimes)
{
return __Scheduler_binding(function(callback)
{
var node = document.createElementNS('http://www.w3.org/1999/xhtml', 'input');
node.setAttribute('type', 'file');
node.setAttribute('accept', A2(__String_join, ',', mimes));
node.addEventListener('change', function(event)
{
callback(__Scheduler_succeed(event.target.files[0]));
});
node.dispatchEvent(new MouseEvent('click'));
});
}
The issue with this code is that it completely fails on Safari on certain versions of iOS, and seems to be unreliable on Android (as reported in the original Discourse thread).
However, I made a modification to make the function append the node to the DOM and then remove it:
function _File_uploadOne(mimes)
{
return _Scheduler_binding(function(callback)
{
var node = document.createElement('input');
node.setAttribute('type', 'file');
node.setAttribute('accept', A2(elm$core$String$join, ',', mimes));
node.addEventListener('change', function(event)
{
// node is removed after change event fires
document.body.removeChild(node);
callback(_Scheduler_succeed(event.target.files[0]));
});
// node is appended before click event is dispatched
document.body.appendChild(node);
node.dispatchEvent(new MouseEvent('click'));
});
}
This change fixes the issue on Safari on iOS 10 (fixing both ‘Take Photo’ and ‘Photo Library’). This also seems to work 100% reliably on Chrome on Android (which failed sporadically before), and continues to work on Chrome/Firefox on Linux without any issue.
It turns out that Browser.Elm.Kernel.Debugger
uses the exact same technique for its own upload function.
One of the concerns that Evan mentions is modifying the DOM may cause issues with the virtual-dom:
I do not really like (2) because I do not want to mess with the DOM and potentially disrupt virtual-dom. The click should be synchronous, so it may work alright. Not super confident in that approach without testing it out a bunch.
One alternative might be to append the temporary element to the outside of the <body>
by using document.documentElement.appendChild()
instead of document.body.appendChild()
as in my example above. This places the <input>
element outside of the <body>
and right before the closing </html>
tag at the end of the document, if this helps mitigate the risk of interfering with the virtual-dom.
A cost of this approach is that if the user cancels file selection, the <input>
node will stay orphaned at the end of the browser. There probably isn’t a reliable way to mitigate this issue given that It is quite difficult to reliably detect if someone has clicked the Cancel button across browsers, but this seems like a small price to pay in exchange for file selection working reliably on all platforms.
Happy to hear any thoughts or other experiences anyone has had with this.