54

I have:

<input type="file" id="f" name="f" onchange="c()" multiple />

Every time the user selects a file(s), I build a list of all the selected files by pushing each element of f.files to an array:

var Files = [];
function c() {
  for (var i=0;i<this.files.length;i++) {
    Files.push(this.files[i]);
  };
}

At form submit, f.files contains only the item(s) from the last select action, so I will need to update f.files with the list of FileList items I have accumulated:

const upload=document.getElementById("f");
upload.files = files;

But the second line gives:

Uncaught TypeError: Failed to set the 'files' property on 'HTMLInputElement': The provided value is not of type 'FileList'.

It is not happy that I am assigning it an array. How can I construct a FileList object from the list of FileList elements I have earlier collected?

Side question: I thought Javascript uses dynamic types. Why is it complaining about the wrong type here?

Old Geezer
  • 14,854
  • 31
  • 111
  • 198
  • I can't find a reference, but my strong suspicion is that `File` objects must be part of an active `FileList` associated with an `` element or a drag-and-drop target. – Pointy Aug 29 '18 at 13:47
  • Here someone answered similar question like this: https://stackoverflow.com/questions/38449440/javascript-create-file-list-object-from-list-of-files – Rana Khurram Aug 29 '18 at 13:57

3 Answers3

92

You can't modify a Filelist, but you can create a new one using a DataTransfer object, and if you wish you can copy your data into it to create a duplicate with the specific change you want to make.

let list = new DataTransfer();
let file = new File(["content"], "filename.jpg");
list.items.add(file);

let myFileList = list.files;

You can then set it as the file attribute of the DOM node:

fileInput.files = myFileList;

If you wished, you could iterate over your old FileList, copying files to the new one.

superluminary
  • 47,086
  • 25
  • 151
  • 148
52

It's like you said

Failed to set the 'files' property on 'HTMLInputElement': The provided value is not of type 'FileList'.

you can only set the files with a FileList instance, unfortunately the FileList is not constructible or changeable, but there is a way to get one in a round about way

/** @params {File[]} files - Array of files to add to the FileList */
function fileListFrom (files) {
  const b = new ClipboardEvent("").clipboardData || new DataTransfer()
  for (const file of files) b.items.add(file)
  return b.files
}

const fileList = fileListFrom([
  new File(['content'], 'sample1.txt'),
  new File(['abc'], 'sample2.txt')
])

fileInput.onchange = console.log
fileInput.files = fileList

console.log(fileInput.files)
<input type="file" id="fileInput" multiple />

EDIT: Setting input.files might trigger a change event. (seems to have been fixed now) so you might want to toggle the change event listener on and off. (But might not have to anymore. Included a change listener to the demo)

Endless
  • 34,080
  • 13
  • 108
  • 131
  • 3
    As a side note, this does not work on iOS Safari. I've yet to solve. – Jarvis Jul 19 '19 at 23:10
  • 1
    This doesn't work on test, ex. node complain `ClipboardEvent` not defined. – windmaomao Apr 04 '21 at 14:07
  • @windmaomao then you are probably using a old browser that don't have the global ClipboardEvent what you want to do in this case is to first check if ClipboardEvent exist and if not use the DataTransfer instead. – Endless Apr 04 '21 at 18:00
  • According to https://developer.apple.com/forums/thread/133001, "user interaction is necessary for clipboard access". – Code4R7 Nov 01 '21 at 11:31
  • For me it worked using `fileInput.prop('files', new FileListItems(files))` instead of `fileInput.files = new FileListItems(files)` – Lipsyor Nov 15 '21 at 12:29
  • FileListItems is a method, not object. Right? – eastwater May 21 '22 at 09:25
  • 1
    FileListItems is a function that return a [FileList](https://developer.mozilla.org/en-US/docs/Web/API/FileList) class of Files – Endless May 21 '22 at 15:16
0

Unfortunately it's not possible to modify FileList, but you can create a new DataTransfer object, add one or multiple files to dataTransfer.items and assign it to input.files. Here's my code example with drag&drop functionality:

const imageUploadLabel = document.getElementById('image-upload-label')
const imageUploadInput = document.getElementById('image-upload-input')

['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
  imageUploadLabel.addEventListener(eventName, event => {
    event.preventDefault()
    event.stopPropagation()
  })
})

['dragenter', 'dragover'].forEach(eventName => {
  imageUploadLabel.addEventListener(eventName, () => {
    imageUploadLabel.classList.add('highlight')
  })
})

['dragleave', 'drop'].forEach(eventName => {
  imageUploadLabel.addEventListener(eventName, () => {
    imageUploadLabel.classList.remove('highlight')
  })
})

let dataTransfer = new DataTransfer()

imageUploadLabel.addEventListener('drop', event => {
  uploadFiles(event.dataTransfer.files)
})

imageUploadInput.addEventListener('change', () => {
  uploadFiles(imageUploadInput.files)
})

function uploadFiles(files) {
  for (let i = 0; i < files.length; i++) {
    dataTransfer.items.add(files[i])
  }

  imageUploadInput.files = dataTransfer.files
  console.log(imageUploadInput.files)
}