Accessing slide elements of react-slick slideshow - javascript

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}"]`);

Related

SplideJs Issues with video and slider carousel

I am using Splidejs for a slider carousel and a video carousel. I am having a issue where if I mount the video carousel first, the slider carousel does not render, and vise versa. Iv made sure to use different css selectors and they render correctly if they are the first mounted carousel so that does not seem to be the issue. Any guidance would be appreciated.
I receive this error for the second carousel.
Uncaught Error: [splide] null is invalid.
Current Code
const splideVideo = new Splide( '#splide2', {
heightRatio: 0.5625,
cover : true,
video : {
loop: true,
},
} );
splideVideo.mount( { Video } );
const splideSlider = new Splide( '#slider1' );
splideSlider.mount();
Was able to come to a solution.
//Splide Hero Slider
const HeroSplideSlider = document.querySelector(".hero-splide-slider");
HeroSplideSlider && new Splide('.hero-splide-slider').mount();
//Splide Hero Splide
const splideVideo = document.querySelector(".splide-video");
const splideConfig = {
heightRatio: 0.5625,
cover : true,
video : {
loop: true,
},
}
splideVideo && new Splide('.splide-video', splideConfig).mount( { Video } );

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.

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.

How to change background-image with React when a page is loading?

How to change the background of the page when it loads without having to click the button?
import img1 from '../images/img1.jpg';
import img2 from '../images/img2.jpg';
import img3 from '../images/img3.jpg';
const [background, setBackground] = useState(img1);
const list = [img1, img2, img3];
const css = {
backgroundImage: `url(${background})`,
};
return (
<div style={css}>
<button onLoad={() => setBackground(list)}>Change background</button>
</div>
);
but setBackground(list)} does not read the array and the onLoad event does not work when the page is reloaded.
thanks for any help!
As mentioned above the solution requires localStorage, useEffect, useRef. All logic is inside useEffect, because when the component is mounting all the magic happens here. working example
Initialize an absolute number with useRef
Trying to get value from localStorage, if is empty then we set
default value. And the same place, we checking the array index.
Second time avoid the check and we increace gotted value andabsolute
number.
Then sending result to state and localStorage
App.js
import { useState, useEffect, useRef } from "react";
export default function App() {
const img1 = "https://dummyimage.com/400x200/a648a6/e3e4e8.png";
const img2 = "https://dummyimage.com/400x200/4e49a6/e3e4e8.png";
const img3 = "https://dummyimage.com/400x200/077d32/e3e4e8.png";
let [lsNum, setLsNum] = useState(0);
// Absolute number '1'
let count = useRef(1);
const list = [img1, img2, img3];
useEffect(() => {
// Get value from localStorage, transform to number
const lS = Number(localStorage.getItem("image"));
// Check! If localStorage have number 2 / more
// Set number 0
if (lS >= 2) {
localStorage.setItem("image", 0);
return;
}
// Get absolute number and increase with number from localStorage
count.current = count.current + lS;
// Set result to state
setLsNum(count.current);
// Set the resulting number to localStorage
localStorage.setItem("image", count.current);
}, []);
const css = {
height: "200px",
width: "400px",
display: "block",
backgroundImage: `url(${list[lsNum]})`, // index change
backgroundPosition: "center",
backgroundSize: "cover",
border: "1px solid red"
};
return (
<div className="App">
<div style={css}></div>
</div>
);
}

Canvas in react not rerendering every props update

I have been trying to implement a scroll video parallax effect inspired by Apple's landing page
in React.js. I have gotten pretty close following these references:
https://codepen.io/ollieRogers/pen/lfeLc/
http://www.petecorey.com/blog/2019/08/19/animating-a-canvas-with-react-hooks/
However when scrolling, the frame only updates when I stop scrolling. My desired result for the canvas is to render in a fluid like fashion while scrolling. In other words the frame should be updating every time my props.scrollPos updates.
All that's important to know about the parent is that it's passing a scroll position using scrollTop. This component is suppose to be reusable and work relative to its parent which is why I chose to not keep a scroll state internally.
You can view a working example of my code here on CodeSandbox
ParallaxVideo.js
...
// request animation frame
// when scrollpos updates the current frame needs to change and cause rerender
// frame = (scrollPos - offset) / playbackConst
const ParallaxVideo = props => {
...
useEffect(() => {
let requestId;
const context = canvasRef.current.getContext("2d")
const offset = heightRef.current.offsetTop
videoRef.current.currentTime = (props.scrollPos - offset) / playbackConst
console.log(props.scrollPos)
const render = () => {
context.drawImage(videoRef.current, 0, 0)
requestId = requestAnimationFrame(render)
}
render()
return () => cancelAnimationFrame(requestId)
})
return (
<div
style={{height: scrollHeight}}
ref={heightRef}
>
<VideoWrap>
<Video
muted
preload={preload}
autoPlay={autoPlay}
loop={loop}
ref={videoRef}
fitToScreen={fitToScreen}
onLoadedMetadata={updateHeight}
playsInline
>
{props.children}
</Video>
<Canvas ref={canvasRef}/>
</VideoWrap>
</div>
)
}
for sure will not change the useEffect should have a inputs set to listen for changes,
useEffect(() => {
let requestId;
const context = canvasRef.current.getContext("2d")
const offset = heightRef.current.offsetTop
videoRef.current.currentTime = (props.scrollPos - offset) / playbackConst
console.log(props.scrollPos)
const render = () => {
context.drawImage(videoRef.current, 0, 0)
requestId = requestAnimationFrame(render)
}
render()
return () => cancelAnimationFrame(requestId)
},[props.scrollPos])

Categories

Resources