How to change a file input's FileList programmatically?
Asked Answered
S

3

23

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?

Salvucci answered 12/4, 2011 at 8:50 Comment(6)
You can't. It's read only for obvious security reasons.Pegg
I see, so then I can't allow it to upload multiple files with multiple clicks then?Salvucci
@Salvucci HTML5 allow multiple selection of files but you still can't decide what will be those files.Pegg
I see. So there is no way via javascript or scripting that I can alter the state of file input?Salvucci
@Salvucci you can alter anything but the selected file(s) of the control..Pegg
So could I alter fileinput.filelist?Salvucci
T
19

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.

Tartan answered 12/4, 2011 at 9:2 Comment(6)
So how would i add more files to the file input's filelist when clicking it multiple times?Salvucci
@Tartan 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.Sciential
@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?Venice
@atas you cannot steal blob from the user programmatically unless they submit it already.Sciential
@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.Venice
@atas And what does that have to do with security? You could always override form's onsubmit event and create the request using AJAX.Sciential
S
57

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 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[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>
Soluk answered 29/6, 2021 at 16:0 Comment(10)
Take note of the bowser support on the DataTransfer constructor if you choose to use this answer.Causality
@Causality 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.Soluk
My main concern would be Safari >=14.1, eliminating support for old Macintosh systems where they can not upgrade.Causality
None of this is a criticism to your answer, just warning people if they go to use it.Causality
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.Soluk
That's an awesome workaround!Hotbed
As of 2023 it still works, this should be noted as the right answer ! Also today with lot of people using typescript, your "getFiles()" could be replaced using destructuring : [...input.files]Sumptuary
@Sumptuary I posted this as a late answer and the OP isn't active anymore, that's why it's not accepted. Regarding [...input.files], that would also work in JS, but it requires FileList to be iterable, which isn't specified by the HTML standard, so I chose to avoid it (although Chromium and FF both implement FileList.prototype[@@iterator]).Soluk
The actual conversion should be Array.from(input.files) which handles almost all edge cases which choke generators and is supported everywhere.Caw
@BillerBuilder I changed it. I originally implemented it using the item() function because the WHATWG spec didn't specify that FileList has to support indexing (even though major browsers did implement it), and I decided to err on the safe side. Even now isn't it super clear, although now the spec includes a note saying that the item() syntax is being phased out.Soluk
T
19

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.

Tartan answered 12/4, 2011 at 9:2 Comment(6)
So how would i add more files to the file input's filelist when clicking it multiple times?Salvucci
@Tartan 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.Sciential
@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?Venice
@atas you cannot steal blob from the user programmatically unless they submit it already.Sciential
@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.Venice
@atas And what does that have to do with security? You could always override form's onsubmit event and create the request using AJAX.Sciential
R
-2

What you want is using the multiple attribute on the input element. That way, in newer browsers user will be able to select multiple files to upload.

Radie answered 12/4, 2011 at 9:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.