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>
CodePudding user response:
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>