2

Goal: I would like for my images to load once they get close (happens to be 20% in my example) to the right or bottom of the viewport. I want the user to have the experience that the images "have always been there" as they scroll, i.e. as they're viewing the current image, the image to the right and the image below it are loading off-screen

I tried 2 approaches. I made comments on my approaches in the code and you can easily toggle each approach on and off. With one approach, the right margin behaves correctly, but the bottom margin doesn't. And with the other approach, the opposite happens: the right margin doesn't work correctly, but the bottom margin does.

JavaScript:

import React, {useEffect} from 'react';
import './test.css';

// dummy test data
const coffeeShops = [
    {
        title: 'Coffee Shop 1',
        photos: [
            'picture 1 of Coffee Shop 1',
            'picture 2 of Coffee Shop 1',
            'picture 3 of Coffee Shop 1',
            'picture 4 of Coffee Shop 1'
        ]
    },
    {
        title: 'Coffee Shop 2',
        photos: [
            'picture 1 of Coffee Shop 2',
            'picture 2 of Coffee Shop 2',
            'picture 3 of Coffee Shop 2',
            'picture 4 of Coffee Shop 2'
        ]
    },
    {
        title: 'Coffee Shop 3',
        photos: [
            'picture 1 of Coffee Shop 3',
            'picture 2 of Coffee Shop 3',
            'picture 3 of Coffee Shop 3',
            'picture 4 of Coffee Shop 3'
        ]
    },
    {
        title: 'Coffee Shop 4',
        photos: [
            'picture 1 of Coffee Shop 4',
            'picture 2 of Coffee Shop 4',
            'picture 3 of Coffee Shop 4',
            'picture 4 of Coffee Shop 4'
        ]
    }
]

export const Test = () => {
    useEffect(() => {
        const svgs = document.querySelectorAll('.mySvg');
        // I am setting the images to load once they come within 20% of the right or bottom of the viewport.
        const rootMargin = '0% 20% 20% 0%';

        // Approach 1:
        // bottom viewport works!
        // right viewport doesn't work: The images load right when they intersect with the right side of the viewport (i.e. 0% instead of the 20% that I set) :(
        createObserver(svgs, rootMargin, null);

        // Approach 2: (the exact opposite happens)
        // bottom viewport doesn't work: If you scroll down you can see that all the images already loaded when the page loaded :(
        // right viewport works!
        // const scrollContainers = document.querySelectorAll('.scroll-container');
        // for (const scrollContainer of scrollContainers) {
        //     createObserver(svgs, rootMargin, scrollContainer);
        // }
    }, [])

    return (
        <>
            <div style={{marginBottom: '50px'}}>Awesome Coffee Shops:</div>

            {coffeeShops.map(shop => {
                return (
                    <>
                        <div className='scroll-container'>
                            {shop.photos.map(photo => {
                                return (
                                    <svg width='4032' height='3024' className='mySvg'>
                                        <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
                                    </svg>

                                );
                            })}
                        </div>
                        <div>{shop.title}</div>
                    </>
                );
            })}
        </>
    );
};


export const createObserver = (svgs, rootMargin, root) => {
    const config = {
        root,
        rootMargin,
        threshold: 0
    };

    const lazyImageObserver = new IntersectionObserver(function (entries, observer) {
        entries.forEach(function (entry) {
            if (entry.isIntersecting) {
                const svg = entry.target;
                setTimeout(() => svg.childNodes[0].setAttribute('fill', 'purple'), 500); // the setTimeout simulates downloading an image
                observer.unobserve(svg);
            }
        });
    }, config);

    svgs.forEach(function (svg) {
        lazyImageObserver.observe(svg);
    });
};

css

.scroll-container {
    overflow: auto;
    white-space: nowrap;
}

svg {
  width: 90%;
  height: 90%;
}
Nick
  • 101
  • 9
  • I'm currently on my phone and can't test your code. Iirc possitive number in rootMargin will expand the check region and negative will shrink it. So I think you need to change it to `rootMargin = '0% -20% -20% 0%'`. – haolt Jun 04 '23 at 05:27
  • My goal is: For my images that are far offscreen to the right and to the bottom --> As the user scrolls, once those images come within 20% of the viewport I'd like for them to load. I want them to have the experience that the images have "always been loaded" as they scroll. A negative 20% would mean that the images would get 20% into the viewport before they begin loading. The user experience in this case would be that they'd see an empty white box on the screen. – Nick Jun 04 '23 at 19:40
  • Sorry, I misunderstood what you want to do. Does my answer below solve it? – haolt Jun 05 '23 at 01:07

1 Answers1

0

If each of your approaches only works in one side, use both. Use approach 1 to attach an observer for each container and approach 2 to load each image in its container.

function loadContainer(entries, observer) {
  entries.forEach(function(entry) {
    if (entry.isIntersecting) {
      const container = entry.target;
      const svgs = container.querySelectorAll('.mySvg');
      createObserver(svgs, rootMargin, container, loadImage);
      observer.unobserve(container);
      console.log(entry.rootBounds); //null in SO snippet but DOMRectReadOnly in a normal page???
    }
  });
}

function loadImage(entries, observer) {
  entries.forEach(function(entry) {
    if (entry.isIntersecting) {
      const svg = entry.target;
      setTimeout(() => svg.children[0].setAttribute('fill', 'purple'), 500); // the setTimeout simulates downloading an image
      observer.unobserve(svg);
    }
  });
}
const createObserver = (targets, rootMargin, root, callBack) => {
  const config = {
    root,
    rootMargin,
    threshold: 0
  };
  const observer = new IntersectionObserver(callBack, config);
  targets.forEach(function(target) {
    observer.observe(target);
  });
};
const scrollContainers = document.querySelectorAll('.scroll-container');
const rootMargin = '0% 20% 20% 0%';
createObserver(scrollContainers, rootMargin, null, loadContainer);
.scroll-container {
  overflow: auto;
  white-space: nowrap;
}
<div style="margin-bottom: 50px">Awesome Coffee Shops:
  <div class='scroll-container'>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
  </div>
  <div>Coffee Shop 1</div>
  <div class='scroll-container'>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
  </div>
  <div>Coffee Shop 2</div>
  <div class='scroll-container'>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
     <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
  </div>
  <div>Coffee Shop 3</div>
  <div class='scroll-container'>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
    <svg width='4032' height='3024' class='mySvg'>
      <rect x="0" y="0" width='4032' height="3024" stroke='black' fill='white' />
    </svg>
  </div>
  <div>Coffee Shop 4</div>
</div>
haolt
  • 353
  • 1
  • 9
  • just realized that the svgs in my question are pretty huge and hard to work with, so I edited the css portion of my question so that they are easier to work with/see on the screen As far as your answer, scrolling to the right works perfectly! But scrolling down does not. When you scroll down slowly in your snippet, you can see that the image below loads right at the intersection of the viewport (as if the bottom margin on root margin were in fact 0% instead of 20%). – Nick Jun 06 '23 at 02:39
  • Try running this in its own page, somehow running inside SO snippet iframe messes up viewport detection. This line: `console.log(entry.rootBounds);` returned null when run in the snippet but as an offline HTML file it returned a DOMRectReadOnly with correct size. – haolt Jun 06 '23 at 03:29
  • So it looks like it scrolls to the right and loads correctly (good), and scrolls down and loads correctly (good). But when I load the page, I see that all of the images have been shifted to the left. So I'm only seeing the right half of the first image. The left half is permanently offscreen. So you've solved my problem while at the same time introducing a new bug – Nick Jun 13 '23 at 01:33