Search code examples
javascriptformsfilefilelist

Add Further Selection Of Files From FilePicker To Existing Array Created With DataTransfer - JavaScript


I have a file uploader that when an image is removed from the images previews, this image is removed from the FileList. This done by creating a new array and removing the target (deleted) element from the new array and then using dataTranser() to set this as the 'file' input element's FileList. All of the is works OK.

The Problem

The issue I have is if further files are added via the filepicker input element prior to submission (irrespective of if any have been deleted), even though the images previews are correct in terms of the number of and specific images shown, the files that are uploaded/submitted are always the files added with the most recent selection. If only one set of files are selected this of course is fine, but if additional images/files are added prior to submission this second set overrides the first set (despite the images still showing correctly in the image preview).

To see the problem if you select files to upload via the 'browse' button in the code example, and then click the 'browse' button a second time to add further images, you'll see what I mean.

The Question

How I get it so that when a second selection of files are added these are added to the existing array and don't override it?

Note 1: to keep the code simpler I've set it so the image is removed when the specific image preview is clicked, and not via a 'remove image' button.

Note 2: the backend code used to process the form is PHP, but I haven't included this because it isn't part of the problem.

Codepen: https://codepen.io/thechewy/pen/wvjOdEq

let attachFiles = document.getElementById("attach-files");
let previewWrapper = document.getElementById("show-selected-images");
let form = document.getElementById("upload-images-form");

attachFiles.addEventListener("change", (e) => {
  [...e.target.files].forEach(showFiles);
});

function showFiles(file) {
  let previewImage = new Image();

  previewImage.dataset.name = file.name;
  previewImage.classList.add("img");
  previewImage.src = URL.createObjectURL(file);

  previewWrapper.append(previewImage); // append preview image

  // -- remove the image preview visually and change FileList
  document.querySelectorAll(".img").forEach((i) => {
    i.addEventListener("click", (e) => {
      const transfer = new DataTransfer();
      const name = e.target.dataset.name;

      for (const file of attachFiles.files) {
        if (file.name !== name) {
          transfer.items.add(file);
        }
      }

      attachFiles.files = transfer.files;

      //remove image preview element when image is clicked
      e.target.remove();
    });
  });
}
form {
  padding: 1rem 2rem;
  width: 50%;
  border: 1px solid;
}

input,
button {
  display: block;
  margin: 2rem 0;
}

.img {
  width: 200px;
  height: 200px;
  object-fit: cover;
  margin: 0 1rem;
}

img:hover {
  cursor: pointer;
}
<form enctype="multipart/form-data" method="post" id="upload-images-form">
  <input id="attach-files" type="file" name="attach-files[]" multiple>
  <button name="submit" id="submit">SUBMIT</button>
  <div id="show-selected-images"></div>
</form>


Solution

  • This uses one DataTransfer object called submitData to add the files selected during the change event to attachFiles, and remove them on the click event to the <img> preview .

    Let me know if you want to do something different with the FileList on submit, like possibly POST it with fetch.

    let attachFiles = document.getElementById("attach-files");
    let previewWrapper = document.getElementById("show-selected-images");
    let form = document.getElementById("upload-images-form");
    let submitData = new DataTransfer();
    
    attachFiles.addEventListener("change", (e) => {
      const currentSubmitData = Array.from(submitData.files);
    
      previewWrapper.replaceChildren();
    
      [...e.target.files].forEach(file => {
        if (currentSubmitData.every(currFile => currFile.name !== file.name)) {
          submitData.items.add(file);
        }
      });
      [...submitData.files].forEach(showFiles);
      attachFiles.files = submitData.files;
    });
    
    function showFiles(file) {
      let previewImage = new Image();
    
      previewImage.dataset.name = file.name;
      previewImage.classList.add("img");
      previewImage.src = URL.createObjectURL(file);
    
      previewWrapper.append(previewImage); // append preview image
    
      // -- remove the image preview visually and change FileList
      document.querySelectorAll(".img").forEach((i) => {
        i.addEventListener("click", (e) => {
          const name = e.target.dataset.name;
    
          [...submitData.files].forEach((file, idx) => {
            if (file.name === name) {
              submitData.items.remove(idx);
            }
          });
          attachFiles.files = submitData.files;
    
          //remove image preview element when image is clicked
          e.target.remove();
        });
      });
    }
    form {
      padding: 1rem 2rem;
      width: 50%;
      border: 1px solid;
    }
    
    input,
    button {
      display: block;
      margin: 2rem 0;
    }
    
    .img {
      width: 200px;
      height: 200px;
      object-fit: cover;
      margin: 0 1rem;
    }
    
    img:hover {
      cursor: pointer;
    }
    <form enctype="multipart/form-data" method="post" id="upload-images-form">
      <input id="attach-files" type="file" name="attach-files[]" multiple>
      <button name="submit" id="submit">SUBMIT</button>
      <div id="show-selected-images"></div>
    </form>

    Here is an alternative approach that cleans up some of the other areas of the code. There are inline comments explaining the approach.

    Mostly it makes use of addEventListener on your dynamically created Image() to attach the click listener before appending to the DOM, instead of appending and then querying the DOM to attach the listener subsequently. Also uses currentTarget instead of target to ensure any bubbling phase of the event does not trigger the listener for an unexpected DOM node.

    let attachFiles = document.getElementById("attach-files");
    let previewWrapper = document.getElementById("show-selected-images");
    let form = document.getElementById("upload-images-form");
    let submitData = new DataTransfer();
    
    attachFiles.addEventListener("change", (e) => {
      const currentSubmitData = Array.from(submitData.files);
    
      // For each addded file, add it to submitData if not already present
      [...e.target.files].forEach(file => {
        if (currentSubmitData.every(currFile => currFile.name !== file.name)) {
          submitData.items.add(file);
        }
      });
    
      // Sync attachFiles FileList with submitData FileList
      attachFiles.files = submitData.files;
    
      // Clear the previewWrapper before generating new previews
      previewWrapper.replaceChildren();
      
      // Generate a preview <img> for each selected file
      [...submitData.files].forEach(showFiles);
    });
    
    function showFiles(file) {
      let previewImage = new Image();
    
      // Set relevant <img> attributes
      previewImage.dataset.name = file.name;
      previewImage.classList.add("img");
      previewImage.src = URL.createObjectURL(file);
      
      // Add click event listener to <img> preview
      previewImage.addEventListener('click', e => {
        const target = e.currentTarget;
        const name = target.dataset.name;
    
        // Remove the clicked file from the submitData
        [...submitData.files].forEach((file, idx) => {
          if (file.name === name) {
            submitData.items.remove(idx);
          }
        });
    
        // Reset the attachFiles FileList
        attachFiles.files = submitData.files;
    
        // Remove the <img> node from the DOM
        target.remove();
      });
     
      // Append <img> preview node to DOM
      previewWrapper.append(previewImage);
    }
    form {
      padding: 1rem 2rem;
      width: 50%;
      border: 1px solid;
    }
    
    input,
    button {
      display: block;
      margin: 2rem 0;
    }
    
    .img {
      width: 200px;
      height: 200px;
      object-fit: cover;
      margin: 0 1rem;
    }
    
    img:hover {
      cursor: pointer;
    }
    <form enctype="multipart/form-data" method="post" id="upload-images-form">
      <input id="attach-files" type="file" name="attach-files[]" multiple>
      <button name="submit" id="submit">SUBMIT</button>
      <div id="show-selected-images"></div>
    </form>