Home > database >  Use JavaScript Fetch and FormData On One Specific Button In A Form
Use JavaScript Fetch and FormData On One Specific Button In A Form

Time:04-22

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:

  1. When any .remove-image button is clicked, handle it by cancelling the default browser behavior (i.e. post page redirect), and instead do a fetch call to your server endpoint, and, if successful, remove the parts of the form corresponding to the image that was deleted.
  2. 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:

  1. 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", () => {
         // ...
       })
    })
    
  2. Whenever any .remove-image button is clicked, it will register a form submit 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 the upload-submit button). The handler cancels the default browser redirect behavior, manually executes a fetch request, and then tries to remove 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.

  • Related