I have a form that submits image filenames and other information via PHP to a MySQL database which all works OK on the backend. On the frontend I'm using the JavaScript fetch()
API to ensure when certain functionality is undertaken there is no hard refresh of the page (specifically if an image is deleted prior to form submission).
The JavaScript below prevents the page doing a hard refresh when an image component is deleted using the .remove-image
button, and it also then removes the HTML of the image component as desired. There is however a side-effect to this code when the main form submission happens:
Problem
When the main form submission happens via the .upload-details
button this too is being affected by the fetch()
code. I only want the fetch code to fire when the delete button is clicked not when the main submission button is clicked. I thought I could achieve this by wrapping the if (fetchForms) {}
code in a click event listener on the .remove-image
button, but this doesn't work either (I've included this code below). It still seems to fire the e.submitter.closest('.upload-details-component').remove()
line of code?
Note: This problem only happens after the .remove-image
button has been clicked. If the .remove-image
button is never clicked the main .upload-details
button works as it should.
The error message I get is 'TypeError: Cannot read properties of null (reading 'remove') at app.js:234:77' which relates to this line of code e.submitter.closest('.upload-details-component').remove()
My Question
How do I get it so the fetch()
code only works when the delete .remove-image
button is clicked and not when the main .upload-details
button is clicked, under all circumstances?
When I do a hard page refresh after the main .upload-details-component
button has been pressed, although it had thrown an error in the console, the form submission has taken place. So I guess I need a way of turning the fetch() off when this other button is clicked?
JavaScript
let fetchForms = document.querySelectorAll('.fetch-form-js'),
removeImageButton = document.querySelectorAll('.remove-image')
// URL details
var myURL = new URL(window.location.href),
pagePath = myURL.pathname
removeImageButton.forEach((button) => {
button.addEventListener('click', () => {
if (fetchForms) {
fetchForms.forEach((item) => {
item.addEventListener('submit', function (e) {
e.preventDefault();
const formData = new FormData(this);
if (e.submitter) {
formData.set(e.submitter.name, e.submitter.value);
}
fetch(pagePath, {
method: "post",
body: formData
})
.then(function(response){
// only remove component if the 'remove-image' button is clicked
e.submitter.closest('.upload-details-component').remove()
return response.text();
// }).then(function(data){
// console.log(data);
}).catch(function (error){
console.error(error);
})
})
});
} // end of if (forms)
}) // 'remove' button clickEvent
}) // removeImageButton forEach
HTML
Note: The image component is outputted with a while
loop because there will almost always be more than one of them. I've only shown one component below for simplicity.
<form method="post" enctype="multipart/form-data">
<!-- IMAGE DETAILS COMPONENT. THESE COMPONENTS ARE OUTPUTTED WITH A WHILE LOOP -->
<div >
<div >
<label for="image-title">Image Title</label>
<input id="title-title>" type="text" name="image-title[]">
</div>
<div >
<label for="tags">Comma Separated Image Tags</label>
<textarea id="tags" type="text" name="image-tags[]"></textarea>
</div>
<div >
<!-- DELETE BUTTON -->
<button name="upload-details-delete" value="12" >DELETE</button>
<input type="hidden" name="image-id[]" value="12">
</div>
</div>
<!-- IMAGE DETAILS COMPONENT - END -->
<div >
<button id="upload-submit" type="submit" name="upload-submit">COMPLETE UPLOADS</button>
</div>
</form>
CodePudding user response:
Here's what I understand your high-level goals to be:
- When any
.remove-image
button is clicked, handle it by cancelling the default browser behavior (i.e. post page redirect), and instead do afetch
call to your server endpoint, and, if successful, remove the parts of the form corresponding to the image that was deleted. - Keep the
upload-submit
behavior as the default (i.e. post page redirect)
Let me first walk through how your code is currently working and why you're seeing the current behavior:
When the page loads, you're registering a "click" handler on all
.remove-image
buttons on the page:let removeImageButton = document.querySelectorAll(".remove-image"); removeImageButton.forEach((button) => { button.addEventListener("click", () => { // ... }) })
Whenever any
.remove-image
button is clicked, it will register a formsubmit
handler on all.fetch-form-js
forms that were found when the page loaded. This handler will execute when any<button type="submit">
element in the form is clicked (including theupload-submit
button). The handler cancels the default browser redirect behavior, manually executes afetch
request, and then tries toremove
the.upload-details-component
element that is the closest parent of whatever button was pressed to submit the form:// When the page loads.. let fetchForms = document.querySelectorAll(".fetch-form-js") // After any .remove-button is clicked... fetchForms.forEach((item) => { // ...register this handler on all .fetch-form-js forms. item.addEventListener("submit", function (e) { // When a .fetch-form-js is submitted (including by pressing and upload-submit button... // ...prevent browser redirect... e.preventDefault() // ... // ...execute a fetch request... fetch(/* ... */) // ... and, if successful, hide the .upload-details-component element that is // the closest parent of whatever button was clicked to submit the form. .then( function(response) { e.submitter.closest(".upload-details-component").remove() // ... }) // ... }) })
The main problem here is that the item.addEventListener("submit")
handler will execute on all submit buttons, including the upload-submit
button (which you want to keep with the default behavior). That's exactly what you don't want.
A secondary problem is that when it executes because an upload-submit
button is clicked, e.submitter.closest(".upload-details-component")
will be null
(because there is no .upload-details-component
that is a parent of upload-submit
)
Here's a way to refactor this to avoid both problems:
const fetchForms = document.querySelectorAll(".fetch-form-js");
// Simplify things by only registering "submit" events.
fetchForms.forEach((item) => {
item.addEventListener("submit", function (e) {
if (
e.submitter &&
// This will be "true" if the "remove-image" button is clicked to submit the form,
// and false if the ".upload-submit" button is clicked to submit the form.
// This way, you'll keep browser default behavior for the ".upload-submit" button 100% of the time.
e.submitter.classList.contains("remove-image")
) {
e.preventDefault();
const formData = new FormData(this);
formData.set(e.submitter.name, e.submitter.value);
fetch(pagePath, {
method: "post",
body: formData
})
.then(function (response) {
e.submitter.closest(".upload-details-component").remove();
return response.text();
})
.catch(function (error) {
console.error(error);
});
}
});
});
See this codesandbox for a working example and a side-by-side comparison with your original code.