1

I just want to disable certain buttons and show a loading spinner until images are loaded. This is a similar question- How to detect when an image has finished rendering in the browser (i.e. painted)? I am new to javascript and am confused why there is not a simple way to determine this! Something similar to this-

document.addEventListener('readystatechange', event => {
   if (event.target.readyState === 'complete') {
    alert('complete');
  }
});

Unfortunately after much searching it seems I will need to use a callback or a promise. I figured it would be a good time to learn about promises/async/await. I am having difficulty attempting to rewrite my functions using promises.

$('#container').on('click', '#imgSearch', function (e) {
$(this).html(`
  <i class="fa fa-spinner fa-spin"></i>Loading`);
//various if statements here to check radio buttons
showImgs(search, sort, phpController, limit);
}



    function showImgs(search, sort, phpController, limit) {
            imgArray = [];
            var a1 = $.post(phpController, {search: search, sort: sort, limit: limit}, {dataType: 'json'});
            $.when(a1).done(function (response) {
                if (response['error']) {
                    alert(response.error.img);
                } else {
                    var imgs = response;
                    if (Array.isArray(imgs)) {
                        for (let i = 0; i < imgs.length; i++) {
//I use setTimeout here to render one image per second
                            setTimeout(function () {
                                offset++;
                                saveImages(imgs[i]);
//calling alertFinished on last img is my temporary solution to removing spinner after render
                                if (i === imgs.length - 1) {
                                        alertFinished();
                                }
                            }, 1000 * i);
                        }
                    } else {
                        saveImages(imgs);
                    }
                }
            });
        }
        ;

I use the saveImages function to empty and push to another array as well as other purposes not shown-

function saveImages(img) {
    imgArray = [];
    imgArray.push(img);
    displayImages(imgArray);
}
;

displayImages renders the image while adding classes etc.-

function displayImages(imgs) {
    var counter = 0;
    imgs.forEach(img => {
        var html = '';
        html += `<div class="" id='${img.id}'><img src=${img.image}></div>`;
        $(html).hide().appendTo(imgSection).fadeIn(1000);
    });
}
;

AlertFinished removes the loading spinner from search button.

function alertFinished() {
            $('#imgSearch').text('Search');
}

Will someone please explain how I can use promises/async/await in this context? It's unfortunate there isn't a way to use an event listener similar to readystatechange because the alternative is to refactor every function that renders new elements in the DOM in order to disable buttons, show spinner etc. Showing a spinner/loading msg is such a widespread feature I am surprised I am having difficulty implementing it appropriately.

jmoney
  • 55
  • 1
  • 8
  • Why do you need to use `alert()`? Create you own modal that doesn't block the whole page and you will not need any hack. – Kaiido Dec 30 '19 at 06:31
  • @Kaiido not sure what you mean. Alert is only used for testing purposes to determine if rendering is complete then remove spinner. The function alertFinished should probably be renamed. – jmoney Dec 30 '19 at 06:38
  • `alert` is a very special method in a browser. It will completely stop the execution of js, and in some browsers of the whole event loop, including rendering. So when you call `alert` nothing will happen anymore, but you should actually never use it, not even for debugging. Instead prefer the [web console](https://developer.mozilla.org/en-US/docs/Tools/Web_Console). With all this said, I feel you are trying to solve a non issue. – Kaiido Dec 30 '19 at 06:43
  • @Kaiido please explain how I am trying to solve a non issue. The issue is how to best block user input while page renders elements. Alert has nothing to do with my problem. I use it to determine if the function runs at the appropriate time or not. I only used an alert in the example when asking why there is not a similar event to readystatechange. My code does not use an alert. By saying it's a non-issue are you saying my code is best practice? – jmoney Dec 30 '19 at 07:31

1 Answers1

0

So first off, as you can see from the question you linked to (and comments therein), there isn't a great way to tell when an image is actually painted. So I'll focus on a way you can call some function after images are loaded.

For starters, if you have a bunch of images in your page right as it first loads, and you just want to wait for them to load before doing something, you could try the super simple route which would be the window load event.

window.addEventListener('load', (event) => {
  // do stuff
});

But I get the impression you have a situation where you're adding images dynamically and want to do something after they're loaded, so that won't work. Still, you're overcomplicating things, I think. A quick look at your function shows you're calling saveImages and displayImages inside a loop even though they appear to be things you want to do only once after you're done adding images.

Assuming that at some point in your whole process you find yourself with a bunch of images that have been added to your DOM, at least some of which are still in the middle of loading, what you need to do is check for the last image to be loaded and then remove your loading spinner afterwards.

The trick here is figuring out which image is last to load. It won't necessarily be the last one you added because images added earlier on could be larger.

One approach you can use to skip the whole promise/async confusion all together would be to use a recursive function that every so often checks whether all images are loaded and if not waits a bit and then checks again. If you make the wait before the function calls itself again relatively short, this will likely work just fine for you.

Something along these lines:

const registeredImages = [];

// add your images    
for (let i = 0, l = someImages.length; i < l; i += 1) {
  doSomething(); // add someImages[i] to DOM where you want it
  someImages[i].setAttribute('data-id', `image-${i}`); // or whatever way to keep track of it
  someImages[i].addEventListener('load', register);
}
// remove spinner once they're loaded
tryToRemoveSpinner();

function register(event) {
  images.push(event.target.getAttribute('data-id');
}

function tryToRemoveSpinner {
  if (registeredImages.length === someImages.length) {
    removeSpinner(); // or whatever
  } else {
    setTimeout(() => { tryToRemoveSpinner() }, 100);
  }
}

An enhancement you could add here would be to put some kind of counter on tryToRemoveSpinner so if some image fails to load for whatever reason it eventually just bails out or runs removeSpinner() anyway or whatever you want to do in the event of an error, just in case.


Related reading: How to create a JavaScript callback for knowing when an image is loaded

cjl750
  • 4,380
  • 3
  • 15
  • 27