简体   繁体   中英

Detect The Last Instance Of A ForEach() Loop Inside A Promise — JavaScript

I have some image files that are previewed as part of a file uploader. A well as validations happening in the backend (PHP) I am setting up frontend validations too.

When the images are attached to the uploader's file <input> element, a preview thumbnail is generated using URL.createObjectURL()

These previews are looped over and a decode() method inside the loop detects if they fail certain criteria. If they fail then an error message is outputted / attached to the specific image file (preview thumbnail) that fails.

If this error message is detected, a generic error message appears at the top of the page telling the user to check the errors shown on the image previews thumbnails.

The Issue

The generic error message at the top of the page is outputted, but because this all happens inside a loop, if 5 files are attached, and only one fails, the generic error message flashes because it is being detected on each instance of the loop.

The Question

How do I detect the last instance of a loop and only output the generic error message when the last loop instance happens?

Note: I have included a simplified example of the code below, but would be happy to show the full code (but I am thinking this might be overkill in relation to the issue).

submitData = new DataTransfer();

// initial 'change' event listener on the files input element which is stored in a variable 'attachFiles'
attachFiles.addEventListener("change", (e) => {

    // stuff here handles the data transfer method and detects any change in the number of files attached etc

    // then run the 'showFiles function on each file attached
    [...submitData.files].forEach(showFiles);

});

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

    // Set relevant <img> attributes
    previewImage.className = "upload-preview-image";
    previewImage.src = URL.createObjectURL(file);

    // get the original width and height values of the thumbnail using the decode() method
    previewImage.decode().then((response) => {

        let w = previewImage.naturalWidth;
        let h = previewImage.naturalHeight;

        // error message that is appended to each image when it fails
        let resError = `<p class="upload-preview-error">Image must be bigger than 2MP</p>`

        if(w * h < 2000000) {
            // append the above errorMessage to the specific image preview in question
            previewImage.insertAdjacentHTML('beforebegin', resError);
        }

        // store HTML class from the above template string in a variable
        let imgError = document.querySelectorAll('.upload-preview-error');

        // set the variable that changes to true when an error message is detected
        let imgErrorMessage = false;

        /* SOMEHOW RUN THIS NEXT CODE ON THE LAST INSTANCE OF THE LOOP, 
        SO THAT THE MAIN ERROR MESSAGE AT THE TOP OF THE PAGE IS ONLY OUTPUTTED ONCE 
        */

        if(imgError.length) {
            if (imgErrorMessage === false) {

                // Append this generic message into a <div> with the class '.js-upload-error'
                document.querySelector('.js-upload-error').innerHTML = `<p class="warning">YOU HAVE ERRORS. CHECK YOUR IMAGES BELOW</p>`

                imgErrorMessage = true;

            }
        }
    }).catch((encodingError) => {
        // Do something with the error.
    });

} // end of showfiles(file)

You look to have a dummy error handling block here:

}).catch((encodingError) => {
    // Do something with the error.
});

When there are problems, consider throwing an error inside showFiles and then handling it in the caller by using Promise.allSettled to wait for all calls to settle (to either fulfill or reject).

From a UI perspective, you almost certainly also want to actually do something to tell the user when there's some other error - in other words, drop the empty // Do something with the error. block entirely and let the caller deal with it. (Or, if you actually do do something with the error in that block, you can re-throw it so that the caller's .allSettled can see that there was a problem.)

function showFiles(file) {
    const previewImage = new Image();
    previewImage.className = "upload-preview-image";
    previewImage.src = URL.createObjectURL(file);
    return previewImage.decode().then((response) => {
        const w = previewImage.naturalWidth;
        const h = previewImage.naturalHeight;
        const resError = `<p class="upload-preview-error">Image must be bigger than 2MP</p>`
        if (w * h < 2000000) {
            previewImage.insertAdjacentHTML('beforebegin', resError);
            throw new Error('Image too small');
        }
    });
}

and replace

[...submitData.files].forEach(showFiles);

with

Promise.allSettled([...submitData.files].map(showFiles))
    .then((results) => {
        if (results.some(result => result.status === 'rejected')) {
            document.querySelector('.js-upload-error').innerHTML = `<p class="warning">YOU HAVE ERRORS. CHECK YOUR IMAGES BELOW</p>`;
            // you can put more error handling here
            // or you can put it in the .catch block if you re-throw
        }
    });

If there's an error other than the image being too small, you might add such an error message in the .catch , such as:

return previewImage.decode().then((response) => {
    // ...
})
    .catch((error) => {
        if (error.message !== 'Image too small') {
            previewImage.insertAdjacentHTML('beforebegin', '<p class="upload-preview-error">Decoding error</p>');
        }
        throw error;
    });

or use the .then(success, fail) syntax.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM