It's indeed impossible to add a local file to a file input that the user didn't request; however, there is a way to add Blob
or File
objects to or remove specific files from a file input.
To change the files in the file input, you have to replace the fileInput.files
with another FileList
object.
The problem is that FileList
objects are immutable and have no constructor exposed to JS.
The only way of creating custom FileList
s is by abusing DataTransfer
, an API designed for transferring files using drag-and-drop or through the clipboard.
It is supported in all mainstream browsers, but not in IE, and only since version 14.1 in Safari. You can check it here.
You can create a DataTransfer
object by invoking its constructor without arguments:
const dataTransfer = new DataTransfer()
The created DataTransfer
object has a files
property, which is, fortunately, a FileList
containing all files the DataTransfer
has.
But then how to add files to the DataTransfer
?
Each DataTransfer
has a DataTransferItemList
associated with it (accessible through its items
property) that has the methods for adding (and removing) items to (and from) the DataTransfer
.
To add a file, you have to call the add
method on it and pass a File
object.
Like so:
dataTransfer.items.add(file)
If you have data that is not in a File
object (e.g. a Blob
or ArrayBuffer
of bytes), you can use the File
constructor, passing the data, the filename, and optionally an object with type
and lastModified
properties to it:
const file = new File([blob], 'file.txt', {type: 'text/plain', lastModified: modificationDate})
So, to sum up, we have something like this:
new DataTransfer()
|
v
DataTransfer
| |
| +--[ .items ]---> DataTransferItemList
| |
+--[ .files ]------+ +---[ .add(file) ]---> DataTransferItem
| ^
v |
+--[ .files ] <-- FileList |
| +--- File <--- new File([data], filename, options)
| ^
| |
<input type="file"> Blob ----------+---------------------+
ArrayBuffer ---+
string --------+
I'm sure this is quite messy right now, but let's see some code!
If you wanted to create a file named hello.txt
containing Hello world!
and set the input to contain this file, here's how can you do it:
const fileInput = document.getElementById('fileInput')
const dataTransfer = new DataTransfer()
const file = new File(['Hello world!'], 'hello.txt', {type: 'text/plain'})
dataTransfer.items.add(file)
fileInput.files = dataTransfer.files
<p>Notice that the file input contains "hello.txt" now: </p>
<input type="file" id="fileInput" />
But instead of replacing the files, how can you edit the list of files already in the input?
- Create a
DataTransfer
- Add all existing files from the input except the ones you want to remove
- Add the files you want to add
- Replace the files in the input with the ones in the
DataTransfer
See also this answer of mine about that topic.
For doing this, I've created a pair of functions for getting and setting the file list from an array of File
s, so the transformations can be performed on arrays instead of doing complicated stuff with DataTransfer
s:
function getFiles(input){
const files = new Array(input.files.length)
for(let i = 0; i < input.files.length; i++)
files[i] = input.files[i]
return files
}
function setFiles(input, files){
const dataTransfer = new DataTransfer()
for(const file of files)
dataTransfer.items.add(file)
input.files = dataTransfer.files
}
You can use them like this:
function getFiles(input){
const files = new Array(input.files.length)
for(let i = 0; i < input.files.length; i++)
files[i] = input.files[i]
return files
}
function setFiles(input, files){
const dataTransfer = new DataTransfer()
for(const file of files)
dataTransfer.items.add(file)
input.files = dataTransfer.files
}
const fileInput = document.querySelector('#fileInput')
document.querySelector('#removeFirst').addEventListener('click', () => {
const files = getFiles(fileInput)
files.shift()
setFiles(fileInput, files)
})
document.querySelector('#removeLastModified').addEventListener('click', () => {
const files = getFiles(fileInput)
let latest = 0, latestIndex
for(let i = 0; i < files.length; i++)
if(files[i].lastModified > latest){
latest = files[i].lastModified
latestIndex = i
}
files.splice(latestIndex, 1)
setFiles(fileInput, files)
})
document.querySelector('#addFile').addEventListener('click', () => {
const files = getFiles(fileInput)
const newFiles = getFiles(document.querySelector('#addFileInput'))
files.push(...newFiles)
setFiles(fileInput, files)
})
document.querySelector('#addRandomHello').addEventListener('click', () => {
const files = getFiles(fileInput)
const newFile = new File(['Hello world!'], 'hello.txt', {type: 'text/plain'})
const index = Math.floor(Math.random() * (files.length + 1))
files.splice(index, 0, newFile)
setFiles(fileInput, files)
})
Hint: hover over the file input to see the list of all files in it <br>
<input type="file" id="fileInput" multiple ><br><br>
<button id="removeFirst">Remove first file</button><br>
<button id="removeLastModified">Remove latest modified file</button><br>
<button id="addFile">Append file from this input</button> <input type="file" id="addFileInput" /><br>
<button id="addRandomHello">Add a hello.txt file to a random place</button><br>