maxmarinich / react-alice-carousel

React responsive component for building content galleries, content rotators and any React carousels
MIT License
833 stars 91 forks source link

Change carousel items without breaking transitions #171

Closed pinktonio closed 3 years ago

pinktonio commented 3 years ago

I'm using two carousels which both share the same activeIndex through a state hook. One carousel acts as the thumbnail preview for the other by showing several slides at once with the responsive prop. Everything works well. However I would like to give the activeIndex item on the thumbnail carousel a different styling so users see it's the active slide. The items for both carousels are set using a state hook as that was the only way I could get both carousels and my custom prev/next buttons to work correctly and keep the carousel transitions on activeIndex change. Because the items are in a state I can not change the styling when the activeIndex changes without resetting the state. As mentioned that breaks the carousel transitions. Is there a way to change the items in a carousel, like changing a style, without breaking the transitions?

maxmarinich commented 3 years ago

Hi, @pinktonio! Please, provide a code sample.

pinktonio commented 3 years ago

Hi @maxmarinich, thanks for the quick reply. Here is the sample:

import React, { useState, useEffect } from 'react';
import AliceCarousel from 'react-alice-carousel';
import styles from './product.module.scss';
import Img from 'gatsby-image';

const Carousel = ({ product }) => {
  const [activeIndex, setActiveIndex] = useState(0);

  const [thumbnails, setThumbnails] = useState(
    product.images.map((image, index) => (
      <div className={styles.buttonContainer} key={`thumbnail-${index}`}>
        <button onClick={() => slideTo(index)}>
          <div
            className={
              index === activeIndex
                ? `${styles.overlay} ${styles.active}`
                : styles.overlay
            }
          />
          <Img
            className={styles.thumbnail}
            fluid={image.localFile.childImageSharp.fluid}
          />
        </button>
      </div>
    ))
  );

  const [images] = useState(
    product.images.map((image, index) => (
      <Img
        className={styles.image}
        fluid={image.localFile.childImageSharp.fluid}
      />
    ))
  );

  const [responsive] = useState({
    0: { items: 6 },
  });

  // Add class "active" to overlay in active thumbnail. Breaks slide transition in carousel for some indexes
  useEffect(() => {
    setThumbnails(
      product.images.map((image, index) => (
        <div className={styles.buttonContainer} key={`thumbnail-${index}`}>
          <button onClick={() => slideTo(index)}>
            <div
              className={
                activeIndex === index
                  ? `${styles.overlay} ${styles.active}`
                  : styles.overlay
              }
            />
            <Img
              className={styles.thumbnail}
              fluid={image.localFile.childImageSharp.fluid}
            />
          </button>
        </div>
      ))
    );
  }, [activeIndex]);

  const slideTo = index => setActiveIndex(index);
  const slideNext = () => {
    if (activeIndex < product.images.length - 1) {
      setActiveIndex(activeIndex + 1);
    } else {
      setActiveIndex(0);
    }
  };
  const slidePrev = () => {
    if (activeIndex !== 0) {
      setActiveIndex(activeIndex - 1);
    } else {
      setActiveIndex(product.images.length - 1);
    }
  };

  return (
    <div className={styles.sliderContainer}>
      <div className={styles.activeImageCarouselContainer}>
        <div className={styles.activeImageCarousel}>
          <AliceCarousel
            items={images}
            autoHeight={true}
            activeIndex={activeIndex}
            infinite={true}
            disableDotsControls={true}
            disableButtonsControls={true}
            onSlideChanged={e => setActiveIndex(e.slide)}
          />
        </div>
        <button className={styles.prev} onClick={slidePrev}>
          Prev
        </button>
        <button className={styles.next} onClick={slideNext}>
          Next
        </button>
      </div>
      <div className={styles.thumbnailsCarousel}>
        <AliceCarousel
          items={thumbnails}
          activeIndex={activeIndex}
          autoWidth={true}
          disableDotsControls={true}
          disableButtonsControls={true}
          responsive={responsive}
          infinite={false}
        />
      </div>
    </div>
  );
};

export default Carousel;
kylesierens commented 3 years ago

I think this is the similar error to what I was trying to explain in my mention as well.

maxmarinich commented 3 years ago

@pinktonio, please update to v2.4. Use .alice-carousel__stage-item.__target classname to highlight an item. Needed live example is here.

import React, { useState } from 'react';
import AliceCarousel from 'react-alice-carousel';
import 'react-alice-carousel/lib/alice-carousel.css';

const items = [
    <div className="item" data-value="1">1</div>,
    <div className="item" data-value="2">2</div>,
    <div className="item" data-value="3">3</div>,
    <div className="item" data-value="4">4</div>,
    <div className="item" data-value="5">5</div>,
];

const thumbItems = (items, [setThumbIndex, setThumbAnimation]) => {
    return items.map((item, i) => (
        <div className="thumb" onClick={() => (setThumbIndex(i), setThumbAnimation(true))}>
            {item}
        </div>
    ));
};

const Carousel = () => {
    const [mainIndex, setMainIndex] = useState(0);
    const [mainAnimation, setMainAnimation] = useState(false);
    const [thumbIndex, setThumbIndex] = useState(0);
    const [thumbAnimation, setThumbAnimation] = useState(false);
    const [thumbs] = useState(thumbItems(items, [setThumbIndex, setThumbAnimation]));

    const slideNext = () => {
        if (!thumbAnimation && thumbIndex < thumbs.length - 1) {
            setThumbAnimation(true);
            setThumbIndex(thumbIndex + 1);
        }
    };

    const slidePrev = () => {
        if (!thumbAnimation && thumbIndex > 0) {
            setThumbAnimation(true);
            setThumbIndex(thumbIndex - 1);
        }
    };

    const syncMainBeforeChange = (e) => {
        setMainAnimation(true);
        if (e.type === 'action') {
            setThumbAnimation(true);
        }
    };

    const syncMainAfterChange = (e) => {
        setMainAnimation(false);

        if (e.type === 'action') {
            setThumbIndex(e.item);
            setThumbAnimation(false);
        } else {
            setMainIndex(thumbIndex);
        }
    };

    const syncThumbs = (e) => {
        setThumbIndex(e.item);
        setThumbAnimation(false);

        if (!mainAnimation) {
            setMainIndex(e.item);
        }
    };

    return [
       <AliceCarousel
            activeIndex={mainIndex}
            animationType="fadeout"
            animationDuration={800}
            disableDotsControls
            disableButtonsControls
            infinite
            items={items}
            mouseTracking={!thumbAnimation}
            onSlideChange={syncMainBeforeChange}
            onSlideChanged={syncMainAfterChange}
            touchTracking={!thumbAnimation}
       />,
       <div className="thumbs">
           <AliceCarousel
                activeIndex={thumbIndex}
                autoWidth
                disableDotsControls
                disableButtonsControls
                items={thumbs}
                mouseTracking={false}
                onSlideChanged={syncThumbs}
                touchTracking={!mainAnimation}
           />
           <div className="btn-prev" onClick={slidePrev}>&lang;</div>
           <div className="btn-next" onClick={slideNext}>&rang;</div>
       </div>
    ]
};
pinktonio commented 3 years ago

Thanks for your efforts @maxmarinich! The styling of the active thumbnail works great now. I also had to wrap items in a useMemo to get the animations on the main carousel to work. Unfortunately the main carousel's animation now occurs after the thumbnail carousel's animation and not simultaneously. Is there a way to fix that?