1

Context

  1. In the simple form shown a file input element allows for multiple file uploads.
  2. An image preview is generated for each file.
  3. When an image is clicked on, this preview image is deleted (for simplicity I haven't included a delete button, it is deleted when you click the image).

On a side note the image files are submitted via PHP in the backend and the backend code all works as expected.

When a number of files are attached via the files input element it creates an array which you can see via the console.log([...chooseFiles.files]); line of code, which shows details of the files attached.

Problem

Can someone explain how when an image is clicked (and thus removed/ deleted visually) how you also remove that specific image from the chooseFiles.files array?

At the moment, because the image is only visually removed, the related image file is still submitted with the form.

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

let chooseFiles = document.getElementById("choose-files");
let previewWrapper = document.getElementById("preview-wrapper");

chooseFiles.addEventListener("change", (e) => {
  [...chooseFiles.files].forEach(showFiles);
  console.log([...chooseFiles.files]);
});

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

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

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

  // -- remove the image preview visually
  document.querySelectorAll(".img").forEach((i) => {
    i.addEventListener("click", (e) => {
      e.target.remove();
    });
  });
}
form {
  padding: 2rem;
  background: red;
  width: 50%;
}

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

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

img:hover {
  cursor: pointer;
}
<form enctype="multipart/form-data" method="post">
  <input id="choose-files" type="file" name="choose-files[]" multiple>
  <button name="submit" id="submit">SUBMIT</button>
  <div id="preview-wrapper"></div>
</form>
Christian
  • 4,902
  • 4
  • 24
  • 42
pjk_ok
  • 618
  • 7
  • 35
  • 90
  • 1
    For one thing, posting directly to the db from the front seems like a bad idea, you gonna expose the connection string and password in the front end bundle, I even wonder if this is possible, sql connection is not through http. I also don’t quite understand the need for adding submitter to the mix, as far as I know submitter will refer to a button that was pressed (on submit button), why would you need an info like this ? – Luke Celitan Oct 08 '22 at 12:37
  • @LukeCelitan The data is being transferred to the database with PHP in the backend. Although this is mentioned in the linked question, it wasn't intially mentioned in this question which I have now added so there aren't any security issues on that front. – pjk_ok Oct 08 '22 at 15:45
  • @pjk_ok I agree with the first comment too. I had to read the question more than twice to understand it. **just remove database part in your question altogether.** DB has nothing to do with it. it's just a form submission with files. – sungryeol Oct 09 '22 at 03:40
  • This question makes no sense. – morganney Oct 09 '22 at 13:48
  • @sungryeol when I did that initially I immediately had an answer (subsequently deleted), and a comment (still there) saying I should never submit directly from the front end to a database for security reasons. Hence why I added that database reference in. But yes it's just a form submission with files but I can't get it to work and it's becoming hugely frustrating., – pjk_ok Oct 09 '22 at 14:58
  • @morganney - i've edited the question right down to the core issue and removed the excess code, which hopefully should make it easier to digest. – pjk_ok Oct 10 '22 at 01:18
  • @pjk_ok Ok, I've added an approach you can take to only send the valid files to the server. – morganney Oct 10 '22 at 02:05

1 Answers1

3

You can compare what is in the DOM in terms of the image previews and what is in the stored FileList from the input. Then only submit what is actually still in the DOM as a valid preview.

There is no way to remove an individual item from a FileList as you can see more of an explanation here.

  • Add a data-name attribute to the image preview.
  • Write a submit event handler for the form preventing the default behavior.
  • Read the data-name from all the .img elements in the submit listener and compare them to the FileList while keeping only the ones with an associated data-name.

let chooseFiles = document.getElementById("choose-files");
let previewWrapper = document.getElementById("preview-wrapper");
let form = document.getElementById('form');

form.addEventListener('submit', (evt) => {
  evt.preventDefault()

  const toSend = []
  const imgs = [...document.querySelectorAll('.img')].map(img => img.dataset.name);

  [...chooseFiles.files].forEach(file => {
    if (imgs.includes(file.name)) {
      toSend.push(file)
    }
  })

  console.log('sending', toSend);
})

chooseFiles.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
  document.querySelectorAll(".img").forEach((i) => {
    i.addEventListener("click", (e) => {
      e.target.remove();
    });
  });
}
form {
  padding: 2rem;
  background: red;
  width: 50%;
}

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

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

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

Alternatively, you can use a DataTransfer object to reset the FileList from the input each time a preview image is removed. This does a bit more work for each click to remove, but has the benefit of keeping the FileList in sync with the list of previews. It also uses the FormData in the submit handler so you can add other fields dynamically and post the data with fetch.

let chooseFiles = document.getElementById("choose-files");
let previewWrapper = document.getElementById("preview-wrapper");
let form = document.getElementById('form');

form.addEventListener('submit', (e) => {
  const fd = new FormData();
 
  e.preventDefault();

  for (const file of chooseFiles.files) {
    fd.append('choose-files[]', file, file.name)
  }
  
  // You can POST this to the server with fetch like
  // fetch(url, { method: 'POST', body: fd })
  console.log('submit', Array.from(fd.values()));
});
chooseFiles.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
  document.querySelectorAll(".img").forEach((i) => {
    i.addEventListener("click", (e) => {
      const transfer = new DataTransfer();
      const name = e.target.dataset.name;
 
      for (const file of chooseFiles.files) {
        if (file.name !== name) {
          transfer.items.add(file);
        }
      }

      chooseFiles.files = transfer.files;
      e.target.remove();
    });
  });
}
form {
  padding: 2rem;
  background: red;
  width: 50%;
}

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

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

img:hover {
  cursor: pointer;
}
<form enctype="multipart/form-data" method="post" id="form">
  <input id="choose-files" type="file" name="choose-files[]" multiple>
  <button name="submit" id="submit">SUBMIT</button>
  <div id="preview-wrapper"></div>
</form>
morganney
  • 6,566
  • 1
  • 24
  • 35
  • Thanks for taking the time to answer. i'm travelling at the moment so will go through in more detail later. I think the dataTransfer is going to be useful because the uploader is going to have drag and drop and a styled 'select files' button that uses this too. I took all of that out to reduce the code overload in the original question – pjk_ok Oct 10 '22 at 03:32
  • @pjk_ok if this solution works for you can you accept the answer, or provide a reason why it does not work? – morganney Oct 14 '22 at 16:46
  • Like in the codesandbox this prints a message to the console but doesn't actually submit the images, which I think I'm going to need to do with `FormData()`, but which means I'm pretty much where I was with the original question before editing it all down. Although a very much appreciated and useful answer I'm now just facing a different issue. – pjk_ok Oct 15 '22 at 00:44
  • @pjk_ok I've updated the last example by simply removing the `submit` handler for the form, so you can submit it natively using the second approach if you like. I think you just are not understanding what I've shown, or you're not asking the question you need answered, oh well. – morganney Oct 15 '22 at 00:55
  • can you revert the changes. I do need to use dataTransfer and FormData, I just need to wrap my head around a lot of new stuff. When the bounty expires I'll mark it as correct and if I get stuck with fetch()/formData I will post a separate questoin. In fairness you did answer what I asked. – pjk_ok Oct 15 '22 at 00:58
  • @pjk_ok it depends on what content type your backend expects from the frontend. You keep mentioning `FormData` which implies a content type of `multipart/form-data`, which is the same thing written in the HTML `
    `'s `enctype` attribute, so either approach sends the same content type.
    – morganney Oct 15 '22 at 01:00
  • If you want or need to use `FormData` because you want to add other fields dynamically, you can use the `submit` handler to add the files from the `FileList` using the approach from the example on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData/append#examples) – morganney Oct 15 '22 at 01:04
  • I need to use `FormData()` because I need to add a progress loading bar which I believe needs `JS fetch()` (or equivalent) to work. If I'm using `fetch()` then from what I understand I'll need to use `FormData()` to process the upload? – pjk_ok Oct 15 '22 at 01:05
  • @pjk_ok I've updated the second example again but this time to use `FormData` in the submit handler. – morganney Oct 15 '22 at 01:12
  • I'm marking this as correct, but I need to wrap my head around a lot of stuff and when I no doubt ask a second questoin on here with a future Issue I can see coming up I'll drop a link here if it gets to the 'bounty' stage. I do appreciate your help. – pjk_ok Oct 15 '22 at 01:25
  • @pjk_ok thanks, ping me if you do need some help with a second question. – morganney Oct 15 '22 at 01:45