javascripthtmlimagepreloading

Retrieve preloaded image from cached array


I'm using multiple image preloading with JavaScript, and I'm assigning some style attributes to each of them, which is possible since these are HTML Images elements. I use the Promise technique, that I found here on StackOverflow:

function preloadImages(srcs) {
  function loadImage(imgToLoad) {
    return new Promise(function(resolve, reject) {
        var img = new Image();
        img.onload = function() {
            resolve(img);
        };
        img.onerror = img.onabort = function() {
            reject(imgToLoad.src);
        };
        img.className = 'testimg'; //Test to see if I can retrieve img
        img.src = imgToLoad.src;
        img.style.top = imgToLoad.top;
    });
}
var promises = [];
for (var i = 0; i < srcs.length; i++) {
    promises.push(loadImage(srcs[i]));
}
return Promise.all(promises);
}

An example of the value of "srcs":

srcs[0].top = '15%';
srcs[0].src = 'img/myImage1.jpg';
srcs[1].top = '24%';
srcs[1].src = 'img/myImage2.jpg';

The preloading part works perfectly, and I'm able to get all of the preloaded images at the end of the process, seeing that the style attributes have been applied, as following:

preloadImages(imgToLoad).then(function(imgs) {
    // all images are loaded now and in the array imgs
    console.log(imgs); // This works
    console.log(document.getElementsByClassName('testimg')); //This doesn't
}, function(errImg) {
    // at least one image failed to load
    console.log('error in loading images.');
});

However, later in my code, I would like to take some of the elements from the array of preloaded images, so I can append them without having to precise the style attributes.

By that, I mean something more than only .append('aSrcThatHasBeenPreloaded'); . I want to keep the class and style attributes I defined during the preload.

Is that even possible? And if so, how should I proceed?

I tried document.getElementsByClassName('myPreloadedClass') but it returns an empty result, which means the preloaded images are not part of the DOM (which makes perfect sense).

Thank you for your help.


Solution

  • You're going to have to make sure you can somehow retrieve the img elements you've preloaded. There are many ways to do this but three that come to mind straight away.

    1. Added loaded image in the DOM

    In your example you're trying to retrieve the images with a query selector. This fails because the img elements aren't added to the DOM. You could attach them to a DOM element that is hidden from the user.

    var 
      images = [
        {
          src: '//placehold.it/550x250?text=A', 
          top: 0, 
          class:'img-a' // unique class name to retrieve item using a query selector.
        }, 
        {
          src: '//placehold.it/550x250?text=B', 
          top: 0, 
          class:'img-b'
        }],
      imageCache = document.getElementById('image-cache');
      
    function preloadImages(srcs) {
      function loadImage(imgToLoad) {
        return new Promise(function(resolve, reject) {
            var img = new Image();
            img.onload = function() {
                // The image has been loaded, add it to the cache element.
                imageCache.appendChild(img);
                resolve(img);
            };
            img.onerror = img.onabort = function() {
                reject(imgToLoad.src);
            };
            img.className = 'testimg ' + imgToLoad.class; //Test to see if I can retrieve img
            img.src = imgToLoad.src;
            img.style.top = imgToLoad.top;
        });
      }
      
      var promises = [];
      for (var i = 0; i < srcs.length; i++) {
          promises.push(loadImage(srcs[i]));
      }
      return Promise.all(promises);
    }
      
    // Load all the images.
    preloadImages(images)
      .then(result => {
        // Add img B from the cache to the proof div.
        document.getElementById('proof').appendChild(imageCache.querySelector('.img-b'));
      });
    .hidden { 
      display: none;
    }
    <div id="image-cache" class="hidden"></div>
    <div id="proof"></div>

    2. Added loaded image to a document fragment

    If you don't want to add the img to the DOM you could add them to a document fragment. This way a visitor of the website won't be able to easily access them and you can still use a query selector to retrieve the image.

    var 
      images = [
        {
          src: '//placehold.it/550x250?text=A', 
          top: 0, 
          class:'img-a' // unique class name to retrieve item using a query selector.
        }, 
        {
          src: '//placehold.it/550x250?text=B', 
          top: 0, 
          class:'img-b'
        }],
      // Create a document fragment to keep the images in.
      imageCache = document.createDocumentFragment();;
      
    function preloadImages(srcs) {
      function loadImage(imgToLoad) {
        return new Promise(function(resolve, reject) {
            var img = new Image();
            img.onload = function() {
                // The image has been loaded, add it to the cache element.
                imageCache.appendChild(img);
                resolve(img);
            };
            img.onerror = img.onabort = function() {
                reject(imgToLoad.src);
            };
            img.className = 'testimg ' + imgToLoad.class; //Test to see if I can retrieve img
            img.src = imgToLoad.src;
            img.style.top = imgToLoad.top;
        });
      }
      
      var promises = [];
      for (var i = 0; i < srcs.length; i++) {
          promises.push(loadImage(srcs[i]));
      }
      return Promise.all(promises);
    }
      
    // Load all the images.
    preloadImages(images)
      .then(result => {
        // Add img B from the cache to the proof div.
        document.getElementById('proof').appendChild(imageCache.querySelector('.img-b')); 
        // And try to add img B from the cache to the proof2 div. The image will only be visible once in the site.
        document.getElementById('proof2').appendChild(imageCache.querySelector('.img-b'));
      });
    .hidden { 
      display: none;
    }
    <div id="proof"></div>
    <div id="proof2" style="margin-top:1em"></div>

    The downside

    This second solution is very similar to the first one. You can still use a query selector to access the images. However both solutions have the same downside, once you place an image from the cache anywhere else on the site you can no longer retrieve the image from the cache. This is because an element can be in the DOM only once and appending it to another element will remove it from its previous parent element.

    3. A reusable bit of code

    I would recommend creating either a closure or an ES6 class where you can put all the functionality for preloading the images and retrieving them.

    The closure from the snippet below returns two methods:

    1. preloadImages which takes an array of images to preload.
    2. getImage which takes a valid CSS selector which is used to retrieve the needed img element from the document fragment. Before returning the element it will clone it so you can add the same preloaded image multiple times to the DOM.

    Cloning an element will also clone its ID, something to be mindful of. I think it is not much of an issue here since the img is created in code so you have full control over it.

    I used a document fragment to store the preloaded images because I think it is a more elegant solution than adding the images to the DOM. It makes it a lot more difficult to get the img element without using the getImage method and thus prevents that the preloaded img element is somehow removed from the image cache.

    var
      images = [
        {
          src: '//placehold.it/550x250?text=A', 
          top: 0, 
          class:'img-a' // unique class name to retrieve item using a query selector.
        }, 
        {
          src: '//placehold.it/550x250?text=B', 
          top: 0, 
          class:'img-b'
        }];
            
    var imagesPreloader = (function() {
      var imagesCache = document.createDocumentFragment();
      
      function loadImage(imgToLoad) {
        return new Promise(function(resolve, reject) {
            var img = new Image();
            img.onload = function() {
              imagesCache.appendChild(img)
              resolve();
            };
            img.onerror = img.onabort = function() {
                reject(imgToLoad.src);
            };
            img.className = 'testimg ' + imgToLoad.class ; //Test to see if I can retrieve img
            img.src = imgToLoad.src;
            img.style.top = imgToLoad.top;
        });
      }
      
      function preloadImages(srcs) {
        var promises = [];
    
        for (var i = 0; i < srcs.length; i++) {
            promises.push(loadImage(srcs[i]));
        }
    
        return Promise.all(promises);  
      }
    
      function getImage(imageSelector) {
        const
          // Find the image in the fragment
          imgElement = imagesCache.querySelector(imageSelector);
          
        // When the selector didn't yield an image, return null.
        if (imgElement === null) {
          return null;
        }
        
        // Clone the image element and return it.
        const
          resultElement = imgElement.cloneNode();
        return resultElement;
      }
    
      return {
        getImage,
        preloadImages
      };
    })();
    
    
    imagesPreloader.preloadImages(images)
      .then(result => {
        console.log('Image with class "img-a": ', imagesPreloader.getImage('.img-a'));
        document.querySelector('#proof').appendChild(imagesPreloader.getImage('.img-b'));
        document.querySelector('#proof2').appendChild(imagesPreloader.getImage('.img-b'));
      });
    <div id="proof"></div>
    <div id="proof2" style="margin-top:1em"></div>