angulartypescriptcarouselangular-animations

Why is my animation state is being shared across component instances?


Whenever you interact with one call of the carousel (i.e. clicking next or prev) the animation plays for both instances of the component. What is wrong here?

carousel.component.ts

@Component({
 selector: 'app-carousel',
 standalone: true,
  templateUrl: './carousel.component.html',
  imports:[ CommonModule ],
  styleUrls: ['./carousel.component.scss'],
  animations: [ CarouselAnimation ]
})
export class CarouselComponent {
  @Input() items: string[] = [];
  @Input() itemsPerSlide: number = 0;
  @Input() carouselName:string = "";
  currentIndex: number = 0;
  hasNextItems: boolean = true;

  get groupedItems(): string[][] {
    const result: string[][] = [];
    for (let i = 0; i < this.items.length; i += this.itemsPerSlide) {
      result.push(this.items.slice(i, i + this.itemsPerSlide));
    }
    return result;
  }

  next() {
    const nextIndex = (this.currentIndex + 1) % this.groupedItems.length;
    this.hasNextItems = this.groupedItems[nextIndex].length > 0;
    if (this.hasNextItems) {
      this.currentIndex = nextIndex;
    }
  }

  prev() {
    const prevIndex = (this.currentIndex - 1 + this.groupedItems.length) % this.groupedItems.length;
    const prevItemsPerSlide = this.groupedItems[prevIndex].length;

    if (prevItemsPerSlide > 0) {
      this.hasNextItems = true;
      this.currentIndex = (this.currentIndex - 1 + this.groupedItems.length) % this.groupedItems.length;
    }
  }
}

carousel.component.html

<div id="DynamicCarousel" class="carousel slide carousel-fade" data-ride="carousel">
  <div class="carousel-inner">
    <div *ngFor="let itemGroup of groupedItems; let i = index" [class.active]="i === currentIndex" class="carousel-item"  [@fadeInOut]>
      <div class="d-flex justify-content-around">
        <div *ngFor="let item of itemGroup" class="carousel-inner-item">
          <img class="d-block w-100" [src]="item" alt="Slide {{i + 1}}">
        </div>
      </div>
    </div>
  </div>
  <a class="carousel-prev" href="#DynamicCarousel" role="button" data-slide="prev" (click)="prev()">
    <div class="icon-container">
      <i class="fa fa-solid fa-arrow-left"></i>
    </div>
  </a>
  <a class="carousel-next" href="#DynamicCarousel" role="button" data-slide="next" (click)="next()">
    <div class="icon-container">
      <i class="fa fa-solid fa-arrow-right"></i>
    </div>
  </a>
</div>

animations.ts


export const CarouselAnimation = [
  trigger('fadeInOut', [
    transition(':enter', [
      style({ opacity: 0 }),
      animate('300ms ease-in', style({ opacity: 1 }))
    ]),
    transition(':leave', [
      animate('300ms ease-out', style({ opacity: 0 }))
    ])
  ])
]

Usage

<h2>Carousel</h2>
<app-carousel [items]="carouselItems" [itemsPerSlide]="3" [carouselName]="'Carousel'"></app-carousel>
<h2>Carousel2</h2>
<app-carousel [items]="carouselItems2" [itemsPerSlide]="3" [carouselName]="'Carousel2'"></app-carousel>

carousel item arrays:

  carouselItems: string[] = [
    'https://via.placeholder.com/800x400',
    'https://via.placeholder.com/800x400',
    'https://via.placeholder.com/800x400',
    'https://via.placeholder.com/800x400',
  ];
  carouselItems2: string[] = [
    'https://via.placeholder.com/800x400',
    'https://via.placeholder.com/800x400',
    'https://via.placeholder.com/800x400',
    'https://via.placeholder.com/800x400',
    'https://via.placeholder.com/800x400',
    'https://via.placeholder.com/800x400',
    'https://via.placeholder.com/800x400',
    'https://via.placeholder.com/800x400',
  ];

Solution

  • you have problems with change detection understanding.

    On any interaction with your components there is new groupedItems created which causes elements to be recreated and thus re:enter the dom = play animation.

    To eliminate the bug you can add ChangeDetectionStrategy.OnPush so change detection won't trigger in the carousel that was not touched

    @Component({
      ...
      changeDetection: ChangeDetectionStrategy.OnPush,
    })