How to change a file input's FileList programmatically?

16,397

Solution 1

For security reasons, browsers prevent javascript from changing the files which will be uploaded: only the user can select files via the user interface. This is to prevent an evil script to upload /etc/passwd, for example, without the user knowing.

The one exception is that calling "reset" on the form will clear the file or filelist, but you can never add to programmatically.

Solution 2

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.

After 10 years of waiting, the solution finally arrived.


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 FileLists 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?

  1. Create a DataTransfer
  2. Add all existing files from the input except the ones you want to remove
  3. Add the files you want to add
  4. 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 Files, so the transformations can be performed on arrays instead of doing complicated stuff with DataTransfers:

function getFiles(input){
  const files = new Array(input.files.length)
  for(let i = 0; i < input.files.length; i++)
    files[i] = input.files.item(i)
  return files
}

function setFiles(input, files){
  const dataTransfer = new DataTransfer()
  for(const file of files)
    dataTransfer.items.add(file)
  fileInput.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.item(i)
  return files
}

function setFiles(input, files){
  const dataTransfer = new DataTransfer()
  for(const file of files)
    dataTransfer.items.add(file)
  fileInput.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>
Share:
16,397

Related videos on Youtube

dave
Author by

dave

Updated on January 26, 2022

Comments

  • dave
    dave about 2 years

    I have this input of type "file", and I want to change its files list. Example:

    <input type = "file" id = "fileinput" />
    <script type = "text/javascript">
      document.getElementById("fileinput").files = [10];
    </script>
    

    The problem is that the fileinput element's files list is not set. How do I do it?

  • dave
    dave about 13 years
    So how would i add more files to the file input's filelist when clicking it multiple times?
  • Tomáš Zato
    Tomáš Zato over 9 years
    @odrm you can't add local real files to file list, but there's nothing wrong with adding File or Blob. I could imagine an userscript that would do that and be helpful.
  • Andreas Tasoulas
    Andreas Tasoulas about 7 years
    @Tomáš Zato I don't think you could, because how the browser would know the origin of the file, if it is a programmatically created or stolen from the user?
  • Tomáš Zato
    Tomáš Zato about 7 years
    @atas you cannot steal blob from the user programmatically unless they submit it already.
  • Andreas Tasoulas
    Andreas Tasoulas about 7 years
    @TomášZato that was what I meant. You cannot add an arbitrary file you create yourself programatically (or retrieved from a server), which the user had not submitted.
  • Tomáš Zato
    Tomáš Zato about 7 years
    @atas And what does that have to do with security? You could always override form's onsubmit event and create the request using AJAX.
  • Manstie
    Manstie over 2 years
    Take note of the bowser support on the DataTransfer constructor if you choose to use this answer.
  • FZs
    FZs over 2 years
    @Manstie Well, yes, it's not supported in IE, but support seems fair otherwise. IE support is often dropped in present-day projects. Anyway, I'll mention that.
  • Manstie
    Manstie over 2 years
    My main concern would be Safari >=14.1, eliminating support for old Macintosh systems where they can not upgrade.
  • Manstie
    Manstie over 2 years
    None of this is a criticism to your answer, just warning people if they go to use it.
  • FZs
    FZs over 2 years
    I got that, I'm just trying to add everything to my answer so that people don't have to read the comments to get this info.

Related