Multiple Images

Chapter 9 8 mins

Learning outcomes:

  1. Loading multiple images

Introduction

Uptil now we've covered a lot of key ideas and concepts revolving around lazy loading but in terms of a single image. All the examples and code snippets we saw in the previous chapter were curated for a single image. However nearly none real word example needs to lazy load just one image - the numbers are almosy always at least higher than one.

Likewise in this chapter we will see how to curate our algorithm to lazy load multiple images based on the concepts learnt for a single one.

So let's begin on with a journey of restructuring.

Making everything multiply

In this chapter we will consider three images to be lazily loaded.

And now it's time to put your thought into a test. For all the programmers out there do you think this loading multiple images would be any difficult? What tools will you need to build this algorithm? How will you need to restructure the previous JavaScript for a single image.

Listing out the tools we would first need a loop to iterate over all the images and as we do so carry out the respective offset calculation for each image and assign a scroll listener for it.

This can be done easily using a function closure. If you don't know what a closure is, consider reading this amazing explanation on the tough concept of JavaScript closures.

Since now we are in the hood of multiple images we rename our previous variables lazyCont and lazyImage to lazyConts and lazyImages respectively; to sound plural.

var lazyConts = document.getElementsByClassName("lazy");
var lazyImages = document.getElementsByClassName("lazy_img");
var len = lazyConts.length; // number of lazy images

for (var i = 0; i < len; i++) {
    (function(i) { // the closure
        // calculate offset for the image
        var offset = lazyConts[i].getBoundingClientRect().top + window.pageYOffset - window.innerHeight;

        // add a new scroll listener to listen for this offset
        window.addEventListener("scroll", function() {
            if (window.pageYOffset > offset) {
                lazyImages[i].src = lazyImages[i].dataset.src;
            }
        });
    })(i)
}
Don't get panicked by looking at the complexity of this code - it's exactly the same code that we constructed in the previous chapters with just slight changes to deal with multiple images.

Let's explain some parts of this code:

Wherever, previously, we had lazyCont or lazyImage written now we have lazyConts[i] and lazyImages[i] to select the ith index image.
Instead of the onscroll property here we've used addEventListener() to add a scroll event to window because this time we need to add another scroll listener with each subsequent image, not just one scroll listener.
The closure (from line 6 - 16), would serve to encapsulate the all our code for the algorithm dealing with multiple images. It remembers the variable i passed to it and thus can operate and resolve things using it easily.

Without this closure we would've been in a complete state of mess; it would've then been impossible to load multiple images!

Next we need to give a loading icon to every lazy image just like we gave one to a single image in the Loading Icons chapter and the class .unloaded to give it a fade-in transition when it loads into view, as we did in the Fade Effect chapter. The CSS remains the same as before.

for (var i = 0; i < len; i++) {
    (function(i) {
        // give a loading icon
        var loader = document.createElement("span");
        loader.innerHTML = '<div class="icon"><span></span><span></span><span></span></div>';
        loader.className = 'loader';
        lazyConts[i].appendChild(loader);

        // add class .unloaded for the fade effect to work
        lazyImages[i].classList.add("unloaded");

        var offset = lazyConts[i].getBoundingClientRect().top + window.pageYOffset - window.innerHeight;
        window.addEventListener("scroll", function() {
            if (window.pageYOffset > offset) {
                lazyImages[i].src = lazyImages[i].dataset.src;
            }
        });
    })(i)
}

Now onto the load and error events for our lazy images - same as before except for only changing to the new variables lazyImages and lazyConts.

for (var i = 0; i < len; i++) {
    (function(i) {
        /* code to add loading icon and class unloaded */

        function reloadImage() {
            loader.innerHTML = '<div class="icon"><span></span><span></span><span></span></div>';
            lazyImage.src = lazyImage.dataset.src;
        }

        lazyImages[i].onerror = function (e) {
            loader.innerHTML = '<div class="icon" onclick="reloadImage()"><i class="fas fa-redo"></i></div>'; // add reloading icon
        }

        lazyImages[i].onload = function () {
            loader.style.display = "none"; // remove loading icon
            lazyImages[i].classList.remove("unloaded"); // fade in image
            lazyImages[i].onerror = null; // remove error handler
        }

        /* code to calculate offset and assign scroll listener */
    })(i)
}

If a lazy image loads without any error this code works perfectly without any error. However if the image fails for some reason, this code throws an error in the console: "Undefined function reloadImage()". Why does this happen?

Recall from the previous chapter that when an image fails to load, the underlying onerror event is fired - and indeed it does fire in this algorithm as well. The problem is that the moment we click the reload button we call a function namely reloadImage() that is just not defined globally.

If you know about variable scoping; resolving variable values; event calls and current scope resolution then you will right away be able to pick up the reason to why is reloadImage() considered NOT to be defined.

Well even if you don't know these concepts well, here is the intuitive explanation.

Any variable declared inside a function is available ONLY inside that function. In our case since we have specified the click event listener for the .icon element inside the onclick attribute rather than the onclick property, the interpreter tries to find a global function namely reloadImage().

Since it cant find any function with the name reloadImage once we click the reload icon, it throws an error in the console.

This can be rectified in two ways:

  1. By declaring reloadImage() globally and altering it slightly to work from the global environment
  2. By shifting the click event of .icon from the onclick attribute to the onclick property

The latter is relatively quicker as compared to the former which requires more thinking on your side. Hence we will go with the latter approach. You should however try out the former as well, as it would be a good exercise for you to do.

for (var i = 0; i < len; i++) {
    (function(i) {
        /* code to add loading icon and class unloaded */

        function reloadImage() {
            loader.innerHTML = '<div class="icon"><span></span><span></span><span></span></div>';
            lazyImage.src = lazyImage.dataset.src;
        }
        
        var reloader;
        lazyImages[i].onerror = function() {
            loader.innerHTML = '<div class="icon reloader"><i class="fas fa-redo"></i></div>';
            reloader = loader.getElementsByClassName("reloader")[0];
            reloader.onclick = reloadImage;
        }

        /* code for the onload event */
        /* code to calculate offset and assign scroll listener */
    })(i)
}

First we give the reloading icon another class - .reloader - to be able to select it easily. The variable reloader serves to select this very element for a failed image which has fired the onerror event. After selecting the element, finally in line 14 we assign it a click event with the handler reloadImage.

Now we've gotten things to work smoothly and flawlessly!

The last thing left to do is to remove the scroll listener for each image once it loads into view.

Since we've used addEventListener() to add a scroll listener for each image, we'll need to use the removeEventListener() method to remove it.

The problem with the method is that it needs a function name to remove listening for a given event - we can't mention a function definition directly and get the handler removed!

So in solving this problem we create another variable lazyFx holding the function definition of our scroll handler and then accordingly use it in the addEventListener() and removeEventListener() methods to add and remove the handler respectively from the scroll event.

for (var i = 0; i < len; i++) {
    (function(i) {
        /* code to add loading icon and class unloaded */
        /* code for the reloadImage() function and onerror event */

        /* code for the onload event */

        var lazyFx = function() {
            if (window.pageYOffset > offset) {
                lazyImages[i].src = lazyImages[i].dataset.src;
                window.removeEventListener("scroll", lazyFx);
                lazyFx = null;
            }
        }
        window.addEventListener("scroll", lazyFx);
    })(i)
}

In line 12 we write lazyFx = null just optionally to empty the variable lazyFx when its corresponding image comes into view.

And with this we conclude our algorithm dealing with multiple images pretty nicely.

Multiple Images Lazy Loader

"I created Codeguage to save you from falling into the same learning conundrums that I fell into."

— Bilal Adnan, Founder of Codeguage