javascriptjquery

How do I find clientWidth and clientHeight of an image whose element was created?


I have an HTML file field for selecting multiple images for upload. I want to get the clientWidth and clientHeight of the first image. I used jQuery to create image element of the images. I appended the image element to the div where the images are supposed to appear. I tried to find the clientWidth and clientHeight of the image but both the clientWidth and clientHeight of the image were 0. On the other hand, naturalWidth and naturalHeight were correctly determined, How can I get the correct clientWidth and clientHeight of the image given that its element was created through jQuery?

$('#image-upload').change(function(event){
  
    const files = this.files;
    
    if (!files.length) return;
    
    let imageElement = '';
    
    $.each(files, function (index, file){
        const reader = new FileReader();
        
        reader.onload = function (event){
            
            let imgSrc = event.target.result;
            
            imageElement = $('<img>')
            .attr('src', imgSrc)
            .addClass('image')
            .css({width: '100%', 'max-width': '100%', display: 'block'});
            
            
            $('#div-container').append(imageElement);
            
            
            if(index === 0){
                
                $('.image').on('load', function () {
                    
                    let image = document.getElementsByClassName('image')[0];
                    
                    let naturalWidth = image.naturalWidth; //got correct width
                    let naturalHeight = image.naturalHeight; // got correct height
                    let renderedWidth = image.clientWidth; //gives 0
                    let renderedHeight = image.clientHeight; //gives 0
                });
            }
        };

        reader.readAsDataURL(file);
    });
    
});

Solution

  • There is an obvious logical issue in your code: you do not have a good understanding of the concept of asynchrony. In your example, there are two asynchronous operations that are nested within each other:

    $.each(files, function (index, file) { // sync
      const reader = new FileReader();
            
      reader.onload = function (event) { // async
        let imgSrc = event.target.result;
        
        imageElement = $('<img>')
          .attr('src', imgSrc)
          .addClass('image')
          .css({width: '100%', 'max-width': '100%', display: 'block'});
                
        $('#div-container').append(imageElement);
          if(index === 0){
            $('.image').on('load', function () { // async
              let image = document.getElementsByClassName('image')[0];
              let naturalWidth = image.naturalWidth; //got correct width
              let naturalHeight = image.naturalHeight; // got correct height
              let renderedWidth = image.clientWidth; //gives 0
              let renderedHeight = image.clientHeight; //gives 0
            });
          }
        };
    
        reader.readAsDataURL(file);
      }
    );
    

    Let me describe to you the general process of how the error occurs:

    1. Select multiple images.

    2. Iterate over the list of image files; this is synchronous.

    3. Read the contents of each image file; this is asynchronous.

    4. This is the first issue: due to asynchrony, it is possible that the second image in the list triggers the onload event first and is added to the DOM as the first image element.

    5. Then comes the second piece of erroneous code: you retrieve all the image elements that already exist in the DOM and add load event listeners to them.

      $('.image').on('load', function () {
        //...
      }
      

      You cannot guarantee that there is only the image element corresponding to the first image in the list in the DOM at this point. If other images finish loading first and trigger the load event while the first image is still loading, you will get incorrect clientWidth and clientHeight values.

    To solve these problems, you can collect the contents of all the images and explicitly execute the corresponding code only after the first image element has finished loading:

    $("#image-upload").change(function (event) {
      const files = this.files;
    
      if (!files.length) return;
    
      const imageElementsHTML = [];
    
      $.each(files, function (index, file) {
        const reader = new FileReader();
    
        reader.onload = function (event) {
          let imgSrc = event.target.result;
    
          imageElementsHTML[index] = $("<img>")
            .attr("src", imgSrc)
            .addClass("image")
            .css({ width: "100%", "max-width": "100%", display: "block" });
    
          if (imageElementsHTML.filter(Boolean).length === files.length) {
            console.log(imageElementsHTML);
    
            $("#div-container").append(imageElementsHTML);
    
            $("#div-container .image:first").on("load", (event) =>
              console.log(getImageDimensions(event.target))
            );
          }
        };
    
        reader.readAsDataURL(file);
      });
    
      function getImageDimensions(image) {
        let naturalWidth = image.naturalWidth;
        let naturalHeight = image.naturalHeight;
        let renderedWidth = image.clientWidth;
        let renderedHeight = image.clientHeight;
    
        return { naturalWidth, naturalHeight, renderedWidth, renderedHeight };
      }
    });