IntersectionObserver sets isIntersecting property of all elements to true on initial load - javascript

Yesterday I ran this code and everything worked as intended. The observer loaded in the images when they were intersecting the viewport:
<template>
<div id="gallery" class="gallery">
<div class="gallery-card">
<img src="../../images/1.jpg">
<img src="../../images/ph.png" data-src="../../images/2.jpg">
<img src="../../images/ph.png" data-src="../../images/3.jpg">
<img src="../../images/ph.png" data-src="../../images/4.jpg">
<img src="../../images/ph.png" data-src="../../images/5.jpg">
<img src="../../images/ph.png" data-src="../../images/6.jpg">
</div>
</div>
</template>
<script setup>
import {onMounted} from "vue";
onMounted(() => {
let config = {
rootMargin: '0px 0px 50px 0px',
threshold: 0
};
const observer = new IntersectionObserver(function(entries, self) {
console.log(entries)
entries.forEach(entry => {
if(entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
self.unobserve(img);
}})
}, config);
const lazyImages = document.querySelectorAll('[data-src]');
lazyImages.forEach(img => {
console.log(img.src)
observer.observe(img);
});
})
</script>
But today the IntersectionObserver loads in all images at once during initial page load. I've tried to debug this with console.log() and the weird thing is that the correct img element is passed to the observer:
const lazyImages = document.querySelectorAll('[data-src]');
lazyImages.forEach(img => {
console.log(img.src)
observer.observe(img);
});
Output (x5, placeholder image):
http://localhost:3000/images/ph.png?3d03f427893c28791c9e0b8a347a277d
but the observer seems to receive an initial entries object with all isIntersecting properties set to true, which then loads in all images:
const observer = new IntersectionObserver(function (entries, self) {
console.log(entries)
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
self.unobserve(img);
}
})
}, config);
Output:
Any way I can prevent that from happening?

Maybe a really late response. But anyway I'll share some ideas that could be helpful.
rootMargin: '0px 0px 50px 0px',
threshold: 0
that would be "use the viewport as root, consider a 50px bottom margin below the viewport's bottom (see this image to understand rootMargin).
What can be happening is that all your images are in fact already visible on the initial load, so all images are being intersected.
I had a similar problem with a custom scrollspy implementation but with the !isIntersecting condition. Solved it using a boolean flag that starts on false in order to catch the initial load, and it's set to true when the scrolling begins.
In your case, I think changing the:
if (entry.isIntersecting) {
line to:
if (entry.isIntersecting && startedIntersecting) {
would solve the initial intersecting problem.
This piece of code captures the first scroll, sets the flag to true and stops capturing the scroll event, leaving all the rest to the IntersectionObserver.
For this I used RxJS's fromEvent like this:
startedIntersecting = false;
fromEvent(document, 'scroll').pipe(
debounceTime(300),
distinctUntilChanged(),
takeWhile(() => startedIntersecting !== true)
).subscribe($evt => {
console.log('startedIntersecting', $evt);
startedIntersecting = true;
});
Hope it enlightens things a bit.

Related

React useState() array mapping inside of component doesn't update if I don't scroll or click on browser

I've made this effect with React and Tailwind:
GIF with the effect
As you can see, so far, so good. However, this effect just works when:
I scroll on the browser
I click on the browser
I open the Chrome tools inspector
This is what happens when I do not do something of the above:
GIF with the effect but not working as expected
As you can see, that's not the effect I'm looking for.
So, apparently, there is a React render problem ocurring right here, and I'm stucked.
Inside, this works as follows:
const HomeHeader = () => {
// This are imported images
const IMAGES = [insta, twitter, netflix, mix];
// This is an array that maps inside the component and renders the images as JSX
const [images, setImages] = useState([]);
useEffect(() => {
let counter = 0;
// An interval that creates a new element in the array "images" every 50 miliseconds
const interval = setInterval(() => {
setImages(prevState => [
...prevState,
{
id: counter++,
duration: getRandom(5, 9) * 1000,
image: IMAGES[getRandom(0, IMAGES.length)],
height: getRandom(1, 6),
top: getRandom(0, window.innerHeight - window.innerHeight / 4),
right: getRandom(0, window.innerWidth)
}
]);
}, 50);
return () => clearInterval(interval);
}, [])
// Every time the state variable "images" is changed, add the cool animation to the last image and, when it ends, delete the image from the HTML
useEffect(() => {
if(images.length > 0) {
let image = images[images.length - 1];
let element = document.getElementById(image.id);
element.classList.add('opacity-0');
element.classList.add('scale-125');
setTimeout(() => {
element.parentNode.removeChild(element);
}, image.duration);
}
}, [images])
return (
<div id="header" className="relative h-full bg-gray-900 flex justify-center items-center px-16 overflow-hidden">
<h1 className="bg-black text-gray-200 font-bold text-5xl sm:text-8xl md:text-9xl text-center z-10"></h1>
{
images.map((element, index) => {
return <img key={index} id={element.id} src={element.image} style={{top: element.top, right: element.right}} className={"imageRRSS absolute filter blur-sm w-auto h-" + element.height + "/6 w-auto transform transition-transform-opacity duration-" + element.duration}/>
})
}
</div>
)
}
What it does is to add every 50 miliseconds a new object to the state array images that contains the properties of the image to add.
After each addition, as it is a state variable, React re-renders the mapping of the component inside of the return:
{
images.map((element, index) => {
return <img key={index} id={element.id} src={element.image} style={{top: element.top, right: element.right}} className={"imageRRSS absolute filter blur-sm w-auto h-" + element.height + "/6 w-auto transform transition-transform-opacity duration-" + element.duration}/>
})
}
And also it goes to the useEffect and adds the cool animation and, when the animations ends, it deletes the tag from the HTML:
useEffect(() => {
if(images.length > 0) {
let image = images[images.length - 1];
let element = document.getElementById(image.id);
element.classList.add('opacity-0');
element.classList.add('scale-125');
setTimeout(() => {
element.parentNode.removeChild(element);
}, image.duration);
}
}, [images])
However, the render just occurs when I interact with the browser.
This is how I understand what it's happening. However, something is clearly wrong as it's not working as expected.
Anybody knows what it's happening?
Thanks!
Ok, solved. The problem was with React applying tailwind effects instantly in the element. I just added the onLoad event to the component instead of ussing useEffect and it works perfectly.

resolve lazy-loading images with parcel-bundler by using vanilla javascript

It seems parcel does not recognizes the image when "src" path is replaced with "data-src" attribute!
Hope this helps!
This is my HTML code:
Lazy-loading HTML Markup
Javsascript with intersectionObserverAPI:
[Lazy-loading with intersectionObserverAPI][2]
When the image isIntersecting I'm removing the lazy-image class after the "load" event.
[CSS lazy-image class][3]
[2]: https://i.stack.imgur.com/pf1OO.pngenter code here
[3]: https://i.stack.imgur.com/nOoSZ.png
This is JS Code:
/* Lazy Loading Hidden Images of Signin and SignUp Forms */
const imgTarget = document.querySelector(".more__itf__img");
// console.log(imgTargetsArray)
const loadingImage = (entries, observer) => {
const [entry] = entries;
console.log(entry);
if (!entry.isIntersecting) return;
// Replace image "src" attribute with "data-src" attribute.
// Loading of images happens behind the scenes, and once its finished
// loading that image it will emit the "load" event.
entry.target.src = entry.target.dataset.src;
// Listening for "load" event.
entry.target.addEventListener("load", ()=>{
entry.target.classList.remove("lazy-image");
});
observer.unobserve(entry.target);
};
const imgObserver = new IntersectionObserver(loadingImage, {
root: null,
threshold: 0.15,
// loading lazy-images before reaching the threshold value.
rootMargin: "200px",
});
if(imgTarget){
imgObserver.observe(imgTarget);
}
HTML Markup:
<section class="more__itf aos_element">
<img
src="./images/blur_img.svg"
data-src="./images/more_itf_purple.svg"
alt="more__itf"
class="more__itf__img lazy-image"
/>
<h3 class="more__itf__text">more in the future...</h3>
</section>
CSS Style:
/* Lazy-image loading */
.lazy-image {
filter: blur(10px);
}

How to show content after page has been reloaded when I reveal sections?

Hi I'm trying to show content after the page has been scrolled and revealed.
This is the code, should I make a function to check if the page has been loaded already then take off the animation? Thanks.
const allSections = document.querySelectorAll(".section");
const revealSection = function (entries, observer) {
const [entry] = entries;
if (!entry.isIntersecting) return;
entry.target.classList.remove("section--hidden");
observer.unobserve(entry.target);
};
const sectionObserver = new IntersectionObserver(revealSection, {
root: null,
threshold: 0.15,
});
allSections.forEach(function (section) {
sectionObserver.observe(section);
section.classList.add("section--hidden");
});

Accessing slide elements of react-slick slideshow

I'm adding a slideshow using react-slick and implementing my own lazy-loading routine so that I can pre-load images a slide ahead (eventually 2 ahead). In order to do this, I need to access the DOM element containing the list of slides wrapped by Slick. Once I have that element, I should easily be able to use querySelector() to target a slide to load its images.
However, I'm having trouble using the slider ref I've created. It's returning a reference to the Slick object, which is not a DOM element, and using querySelector() on slider.current gives the error:
Uncaught TypeError: slider.current.querySelector is not a function
Any thoughts as to how to reference the Slick DOM element?
export default function Carousel({ children, ...slickProps }) {
const slider = useRef();
const slickDefaults = {
dots: true,
infinite: false,
slidesToScroll: 1,
slidesToShow: 1,
speed: 200,
arrows: true
};
const onNext = () => {
slider.current.slickNext();
};
const onPrev = () => {
slider.current.slickPrev();
};
return (
<Slider
{...slickDefaults}
{...slickProps}
ref={slider}
className="carousel"
beforeChange={(_currentSlide, nextSlide) => {
if (slider.current) {
// The error occurs here
const slideElement = slider.current.querySelector(`[data-index="${nextSlide}"]`);
if (slideElement) {
const images = slideElement.getElementsByTagName('IMG');
for (let i = 0; i < images.length; i++) {
const image = images[i];
const lazySrc = image.getAttribute('data-lazy');
if (lazySrc) {
image.setAttribute('src', lazySrc)
image.removeAttribute('data-lazy');
}
}
}
}
}}
prevArrow={
<CarouselButton
direction="left"
clickEvent={onPrev}
/>
}
nextArrow={
<CarouselButton
direction="right"
clickEvent={onNext}
/>
}
>
{children}
</Slider>
);
}
It turns out that I just had to dig a little deeper into the object pointed to by the ref. In most cases, ref.current will suffice, but here the slideshow DOM element can be found at: ref.current.innerSlider.list, which references the element div.slick-list.
The error can be resolved by replacing the line with:
const slideElement = slider.current.innerSlider.list.querySelector(`[data-index="${slideIndex}"]`);

Bugs when applying intersection observer in VueJs

I want to render my image only when the user has seen it or when the image element is visible within the user viewport. I already tried it, but the image is only rendered when I already passed all the image element, not one by one when i scroll.
I create a custom directive and place it on my parent div.
This is my intersection observer using Vue directive:
inserted: el => {
let arrayChildren = Array.from(el.children)
const config = {
root: null,
rootMargin: '0px',
threshold: 1.0,
}
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src
observer.unobserve(entry.target)
}
})
}, config)
function getAllImgQuery() {
let imageArray = []
arrayChildren.forEach(element => {
if (element.nodeName === 'IMG') {
imageArray.push(element)
}
})
return imageArray
}
let imageQuery = getAllImgQuery()
imageQuery.forEach(image => {
observer.observe(image)
})
},
And this is my Vue component:
<template>
<div id="image-container">
<div class="product">
<figure class="image-wrapper" v-lazyload>
<img :data-src="url" style="margin-top: 100px">
<img :data-src="url" style="margin-top: 100px">
<img :data-src="url" style="margin-top: 100px">
<img :data-src="url" style="margin-top: 100px">
<img :data-src="url" style="margin-top: 100px">
<img :data-src="url" style="margin-top: 100px">
</figure>
</div>
</div>
</template>
<script>
import lazyLoadDirective from "../directives/lazyLoadImage.js";
export default {
data() {
return {
url:
"https://s2.reutersmedia.net/resources/r/?m=02&d=20190823&t=2&i=1422065068&w=1200&r=LYNXNPEF7M1OI"
};
},
directives: {
lazyload: lazyLoadDirective
}
};
</script>
At the end, the six images loads at the same time only when I already seen it or intersect it (when I was at the bottom of the page). How can i load my image one by one only after i scroll pass it ?
I think your problem is related to the threshold you have set. I recommend reading: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
Treshold:
Either a single number or an array of numbers which indicate at what
percentage of the target's visibility the observer's callback should
be executed. If you only want to detect when visibility passes the 50%
mark, you can use a value of 0.5. If you want the callback to run
every time visibility passes another 25%, you would specify the array
[0, 0.25, 0.5, 0.75, 1]. The default is 0 (meaning as soon as even one
pixel is visible, the callback will be run). A value of 1.0 means that
the threshold isn't considered passed until every pixel is visible.

Categories

Resources