Bugs when applying intersection observer in VueJs - javascript

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.

Related

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

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.

How to use JS to get HTML img height and set it as a variable?

In the following snippet, I'm trying to get the height of any image nested inside a particular class. Once that image height is gathered, I need to store it in a variable to be used elsewhere.
The two issues I'm running into are:
The second image is coming back as 0px
Storing the dynamic image height result as a variable
let getImgHeight = (()=> {
let images = document.querySelectorAll('.wistia img');
images.forEach(image => console.log(`${image.clientHeight}px`));
})();
<div class="wistia">
<img src="https://media.moddb.com/images/members/5/4550/4549205/duck.jpg" alt="">
<img src="https://www.thedesignwork.com/wp-content/uploads/2011/10/Random-Pictures-of-Conceptual-and-Creative-Ideas-02.jpg" alt="">
</div>
You get zero because you check the image height before it was loaded. You can easily check it if you wrap your function in some long timeout.
The better approach is to wait for all the loading images (ignoring ones that are already loaded) and after that get their height.
Here is an example below:
let getImgHeight = (()=> {
let images = document.querySelectorAll('.wistia img');
Promise.all(Array.from(images)
.filter(img => !img.complete)
.map(img => new Promise(resolve => {
img.onload = img.onerror = resolve; })
)
).then(() => {
images.forEach(image => console.log(`${image.clientHeight}px`));
});
})();
<div class="wistia">
<img src="https://media.moddb.com/images/members/5/4550/4549205/duck.jpg" alt="">
<img src="https://www.thedesignwork.com/wp-content/uploads/2011/10/Random-Pictures-of-Conceptual-and-Creative-Ideas-02.jpg" alt="">
</div>
UPDATE 1
As long as you want
to set the height of the parent container
I'd suggest rewriting your code as follows:
const images = document.querySelectorAll('.wistia img');
for (const image of Array.from(images)) {
if (image.complete) {
image.parentElement.style.height = image.clientHeight + 'px';
} else {
image.onload = () => {
image.parentElement.style.height = image.clientHeight + 'px';
}
}
}
<div class="wistia">
<img src="https://media.moddb.com/images/members/5/4550/4549205/duck.jpg" alt="">
</div>
The issue is most likely that you're trying to get the height of an image that hasn't finished loading. The best way to deal with that is to use the load event listener. You can then store the height as a data-attribute which is one way of making it a dynamic variable.
document.addEventListener('DOMContentLoaded', () => {
let images = document.querySelectorAll('.wistia img');
images.forEach(image => {
image.addEventListener('load', e => {
e.target.setAttribute('data-height', `${image.clientHeight}px`)
console.log(`${e.target.clientHeight}px`);
if (document.querySelectorAll('.wistia img[data-height]').length === images.length) {
console.log("ALL IMAGES LOADED");
}
});
});
});
<div class="wistia">
<img src="https://media.moddb.com/images/members/5/4550/4549205/duck.jpg" alt="">
<img src="https://www.thedesignwork.com/wp-content/uploads/2011/10/Random-Pictures-of-Conceptual-and-Creative-Ideas-02.jpg" alt="">
</div>
I tried your code in JSFiddle and it worked as expected. Check the version of jQuery you're using.

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.

React Cycling Image Gallery

i want to make a component that displays a number of images, it will change a single image after an interval of time.
So far i have this code:
import React from 'react';
const Gallery = (props) => {
const cellStyle = {
background: "red",
padding: "0"
}
const ImageCell = (props) => {
return (
<div className="col-lg-4" style={cellStyle}>
<div style={{
height: "100%",
width: "100%",
padding: "0",
background: `url(${props.theImage})`,
backgroundSize: "cover"
}}
>
</div>
</div>
);
}
return(
<div className="row full-width">
{
props.images.map(function(image) {
let theKey = image.slice(14, -4);
return <ImageCell key={theKey} theImage={image} />
})
}
</div>
);
}
export default Gallery;
I want it to switch out a random image from the 6x6 grid and switch it with a random image from the image set
So i don't really know how i would implement this, any ideas will be welcome.
Your Gallery should hold the randomization logic and pass the images down to its children.
So, the images-to-display can be set in an array within Gallery's state. Implement the componentDidMount method and instantiate a timer there; when the timer fires, you can replace one of the images in the state's array with a randomly selected image from the total images array. Updating state will then re-render Gallery, thus re-rendering its ImageCell children.

React.js events need 2 clicks to execute

I am building the Game of Life by React.js and I get stuck in a uncomfortable situation:
Every event which I set as onClick={ event } needs 2 clicks to execute.
Let me describe more:
As you can see in my code below, I have 2 buttons (one button is to change the size of the board to 10 x 10, the other one is to change the speed of the interval).
Everything is fine, except that when I click on these two buttons, I need to double click to execute. On the first click, with React Developer Tool in Chrome, I can see that the states including width, height, speed are changed, but the state board still remains unchanged. Only after the second click, the board state is changed.
Anyone can explain why and show me how to fix? Thank you
Here is a part of my code
var GameBoard = React.createClass({
getInitialState: function() {
return {
width: 10,
height: 10,
board: [],
speed: 1000,
};
},
// clear the board to the initial state
clear: function(width, height) {
this.setState({
width: width,
height: height,
});
this.setSize();
clearInterval(this.game);
},
// set the size of the board
setSize: function() {
var board = [];
for (var i = 0; i < this.state.height; ++i) {
var line = [];
for (var j = 0; j < this.state.width; ++j)
line.push(0);
board.push(line);
}
this.setState({
board: board
});
},
// start the game
start: function() {
this.game = setInterval(this.gameOfLife, this.state.speed);
},
gameOfLife: function() { // game of life },
// change the speed of the game
changeSpeed: function(speed) {
this.setState({ speed: speed });
clearInterval(this.game);
this.start();
},
// change the size to 10 x 10
smallSize: function() {
this.clear(10, 10);
},
render: function() {
return (
<div className="game-board">
<h1>Conway's Game of Life</h1>
<h2>Generation: { this.state.generation }</h2>
<div className="control">
<button className="btn btn-default" onClick={ this.start }>Start</button>
</div>
<Environment board={ this.state.board } onChangeSquare = { this.onChangeSquare }/>
<div className="size">
<h2>Size</h2>
<button className="btn btn-default" onClick={ this.smallSize }>Small (10 x 10)</button>
</div>
<div className="speed">
<h2>Speed</h2>
<button className="btn btn-default" onClick={ this.changeSpeed.bind(this, 900) }>Slow</button>
</div>
</div>
)
}
});
The reason is that the state of a component does not change immediately.
In clear() method you set the width and height state. But internally when they react setSize() method they will not be updated immediately. They will be updated only when they reach the render method.
When you click the button the second time the states would have been updated properly. That is why it works in the second instance.
One solution please dont keep the width and height as state use it in props. Keep the 10 * 10 as a separete default prop and use it in the setSize method.

Categories

Resources