Bootstrap Slides lagging while using multi item ca

2020-03-12 03:11发布

问题:

My issue is I am using Angular 6, Bootstrap v4 and jQuery for the multi-item carousel. If I have data like [1,2,3,4,5,6] there on the carousel it has to show [1,2,3]. After pressing the next it has to show [4,5,6].

Up to this part I achieved, I am facing issues. Only the first active card is moving and there is a lagging also and the previous and next button are not visible. Please check once Stackblitz file you will understand the issue

Here is my work in Stackblitz

https://stackblitz.com/edit/angular-jvr6dh

HTML code

<div class="container">
  <div id="dataInfo">
      <h2>Information</h2>
      <div id="myCarousel" class="carousel slide" data-ride="carousel">
          <div class="carousel-inner row w-100 mx-auto">
            <div class="carousel-item col-md-4 active">
              <div class="card">
                <img class="card-img-top img-fluid" src="https://images.freeimages.com/images/large-previews/85a/daisy-s-1375598.jpg"  width="100" height="100" alt="Card image cap">
                <div class="card-body">
                  <h4 class="card-title">Card 1</h4>
                  <p class="card-text">This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
                  <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p>
                </div>
              </div>
            </div>
            <div class="carousel-item col-md-4">
              <div class="card">

                <img class="card-img-top img-fluid" src="https://images.freeimages.com/images/large-previews/85a/daisy-s-1375598.jpg"  width="100" height="100" alt="Card image cap"><div class="card-body">
                  <h4 class="card-title">Card 2</h4>
                  <p class="card-text">This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
                  <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p>
                </div>
              </div>
            </div>
            <div class="carousel-item col-md-4">
              <div class="card">

                <img class="card-img-top img-fluid" src="https://images.freeimages.com/images/large-previews/85a/daisy-s-1375598.jpg"  width="100" height="100" alt="Card image cap"><div class="card-body">
                  <h4 class="card-title">Card 3</h4>
                  <p class="card-text">This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
                  <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p>
                </div>
              </div>
            </div>
            <div class="carousel-item col-md-4">
              <div class="card">

                <img class="card-img-top img-fluid" src="https://images.freeimages.com/images/large-previews/85a/daisy-s-1375598.jpg"  width="100" height="100" alt="Card image cap"><div class="card-body">
                  <h4 class="card-title">Card 4</h4>
                  <p class="card-text">This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
                  <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p>
                </div>
              </div>
            </div>
            <div class="carousel-item col-md-4">
              <div class="card">

                <img class="card-img-top img-fluid" src="https://images.freeimages.com/images/large-previews/85a/daisy-s-1375598.jpg"  width="100" height="100" alt="Card image cap"> <div class="card-body">
                  <h4 class="card-title">Card 5</h4>
                  <p class="card-text">This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
                  <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p>
                </div>
              </div>
            </div>
            <div class="carousel-item col-md-4">
              <div class="card">

                <img class="card-img-top img-fluid" src="https://images.freeimages.com/images/large-previews/85a/daisy-s-1375598.jpg"  width="100" height="100" alt="Card image cap"><div class="card-body">
                  <h4 class="card-title">Card 6</h4>
                  <p class="card-text">This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
                  <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p>
                </div>
              </div>
            </div>
            <div class="carousel-item col-md-4">
              <div class="card">

                <img class="card-img-top img-fluid" src="https://images.freeimages.com/images/large-previews/85a/daisy-s-1375598.jpg"  width="100" height="100" alt="Card image cap"><div class="card-body">
                  <h4 class="card-title">Card 7</h4>
                  <p class="card-text">This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
                  <p class="card-text"><small class="text-muted">Last updated 3 mins ago</small></p>
                </div>
              </div>
            </div>
          </div>
          <a class="carousel-control-prev" href="#myCarousel" role="button" data-slide="prev">
            <span class="carousel-control-prev-icon" aria-hidden="true"></span>
            <span class="sr-only">Previous</span>
          </a>
          <a class="carousel-control-next" href="#myCarousel" role="button" data-slide="next">
            <span class="carousel-control-next-icon" aria-hidden="true"></span>
            <span class="sr-only">Next</span>
          </a>
        </div>

  </div>


</div>

. ts code:

$("#myCarousel").on("slide.bs.carousel", function(e) {
      var $e = $(e.relatedTarget);
      var idx = $e.index();
      var itemsPerSlide = 3;
      var totalItems = $(".carousel-item").length;

      if (idx >= totalItems - (itemsPerSlide - 1)) {
        var it = itemsPerSlide - (totalItems - idx);
        for (var i = 0; i < it; i++) {
          // append slides to end
          if (e.direction == "left") {
            $(".carousel-item")
              .eq(i)
              .appendTo(".carousel-inner");
          } else {
            $(".carousel-item")
              .eq(0)
              .appendTo($(this).find(".carousel-inner"));
          }
        }
      }
    });
  }

.scss code

  .carousel-inner .active,
.carousel-inner .active + .carousel-item,
.carousel-inner .active + .carousel-item + .carousel-item {
  display: block;
}

.carousel-inner .carousel-item.active:not(.carousel-item-right):not(.carousel-item-left),
.carousel-inner .carousel-item.active:not(.carousel-item-right):not(.carousel-item-left) + .carousel-item,
.carousel-inner .carousel-item.active:not(.carousel-item-right):not(.carousel-item-left) + .carousel-item + .carousel-item {
  transition: none;
}

.carousel-inner .carousel-item-next,
.carousel-inner .carousel-item-prev {
  position: relative;
  transform: translate3d(0, 0, 0);
}

.carousel-inner .active.carousel-item + .carousel-item + .carousel-item + .carousel-item {
  position: absolute;
  top: 0;
  right: -33.3333%;
  z-index: -1;
  display: block;
  visibility: visible;
}

/* left or forward direction */
.active.carousel-item-left + .carousel-item-next.carousel-item-left,
.carousel-item-next.carousel-item-left + .carousel-item,
.carousel-item-next.carousel-item-left + .carousel-item + .carousel-item,
.carousel-item-next.carousel-item-left + .carousel-item + .carousel-item + .carousel-item {
  position: relative;
  transform: translate3d(-100%, 0, 0);
  visibility: visible;
}

/* farthest right hidden item must be abso position for animations */
.carousel-inner .carousel-item-prev.carousel-item-right {
  position: absolute;
  top: 0;
  left: 0;
  z-index: -1;
  display: block;
  visibility: visible;
}

/* right or prev direction */
.active.carousel-item-right + .carousel-item-prev.carousel-item-right,
.carousel-item-prev.carousel-item-right + .carousel-item,
.carousel-item-prev.carousel-item-right + .carousel-item + .carousel-item,
.carousel-item-prev.carousel-item-right + .carousel-item + .carousel-item + .carousel-item {
  position: relative;
  transform: translate3d(100%, 0, 0);
  visibility: visible;
  display: block;
  visibility: visible;
}

回答1:

Based on your example code, it looks like simplifying the Bootstrap code itself to use the standard carousel is the best approach. The link below is a fork of the example you provided with the following changes:

https://stackblitz.com/edit/angular-yaevix

  • Removed all custom DOM manipulation on prev/next/etc. from your javascript so it's using the Bootstrap carousel component to do everything
  • Removed the CSS that was trying to adjust the animation and placement of the cards
  • Added quick and dirty placement of next/prev icons (you can style this as you see fit)

You have each card set as a carousel-item but in your description you want to paginate 3 at a time. The correct approach here is to have one carousel-item for every three cards. See example below

Example of one carousel item with multiple cards:

<div id="myCarousel" class="carousel slide" data-ride="carousel">
    <div class="carousel-inner">
        <div class="carousel-item active">
            <div class="row">
                <div class="col-md-4">
                    <div class="card">Card 1</div>
                </div>
                <div class="col-md-4">
                    <div class="card">Card 2</div>
                </div>
                <div class="col-md-4">
                    <div class="card">Card 3</div>
                </div>
            </div>
        </div>
        <div class="carousel-item active">
            <div class="row">
                <div class="col-md-4">
                    <div class="card">Card 4</div>
                </div>
                <div class="col-md-4">
                    <div class="card">Card 5</div>
                </div>
                <div class="col-md-4">
                    <div class="card">Card 6</div>
                </div>
            </div>
        </div>
        <div class="carousel-item">
            <div class="row">
                <div class="col-md-4">
                    <div class="card">Card 7</div>
                </div>
            </div>
        </div>
    </div>
</div>


回答2:

You may find it easier to write your own carousel behaviour, and remove any reliance on jquery. You can still use Bootstrap to style.

We can also use Angular animations to retain the animation behaviour. (Please note that this is the first time I've used Angular animations, so there may be a neater way of doing this)

First step is to create a directive, which handles the next / prev behaviour:

  @Input() animationDuration = 500;

  private slideLeft: AnimationFactory;
  private slideRight: AnimationFactory;
  private slideInLeft: AnimationFactory;
  private slideInRight: AnimationFactory;

  constructor(private el: ElementRef, private _builder: AnimationBuilder) {
  }

  ngOnInit() {
    this.slideLeft = this._builder.build([
      style({ transform: 'translateX(0)' }),
      animate(this.animationDuration, style({ transform: 'translateX(-100%)' })),
      style({ transform: 'translateX(0)' }),
    ]);

    this.slideRight = this._builder.build([
      style({ transform: 'translateX(0)' }),
      animate(this.animationDuration, style({ transform: 'translateX(100%)' })),
      style({ transform: 'translateX(0)' }),
    ]);

    this.slideInLeft = this._builder.build([
      style({ transform: 'translateX(100%)', right: 0 }),
      animate(this.animationDuration, style({ transform: 'translateX(0)' })),
      style({ right: 'initial' })
    ]);

    this.slideInRight = this._builder.build([
      style({ transform: 'translateX(-100%)', left: 0 }),
      animate(this.animationDuration, style({ transform: 'translateX(0)' })),
      style({ left: 'initial' })
    ]);
  }

  next(steps) {
      let active = this.el.nativeElement.querySelectorAll('.carousel-item.active');
      let inactive = this.el.nativeElement.querySelector('.carousel-item:not(.active)');
      // Start the animation
      this.animateAll(active, this.slideLeft);
      // Start the slide in animation for the next element
      this.preMoveElement(inactive);
      this.slideInLeft.create(inactive).play();

      setTimeout(() => {
        // Move the last element to start and make it active.
        active = this.el.nativeElement.querySelectorAll('.carousel-item.active');
        inactive = this.el.nativeElement.querySelector('.carousel-item:not(.active)');
        active[0].classList.remove('active');
        this.el.nativeElement.insertBefore(active[0], null);
        inactive.classList.add('active');
        if (steps && steps - 1 > 0) {
          this.next(steps - 1);
        }
      }, this.animationDuration);
   // }
  }

  prev(steps) {
    const active = this.el.nativeElement.querySelectorAll('.carousel-item.active');
    const children = this.el.nativeElement.children;
    const lastChild = children[children.length - 1];
    // Start the animation
    this.animateAll(active, this.slideRight);
    // Start the slide in animation for the next element
    this.preMoveElement(lastChild);
    this.slideInRight.create(lastChild).play();


    setTimeout(() => {
      // Remove the active class
      const lastActive = active[active.length - 1];
      lastActive.classList.remove('active');
      // Move the last element to the start, and make it active
      this.el.nativeElement.insertBefore(lastChild, children[0]);
      lastChild.classList.add('active');
      if (steps && steps - 1 > 0) {
        this.prev(steps - 1);
      }
    }, this.animationDuration);
  }

  private animateAll(elements: any[], animation: AnimationFactory) {
    elements.forEach(element => {
      animation.create(element).play();
    });
  }

  private preMoveElement(element) {
    element.style.display = 'block';
    element.style.position = 'absolute';
    setTimeout(() => {
      element.style = null;
    }, this.animationDuration);
  }

You can then access the prev and next functions from the HTML:

Change the carousel-inner element, like so:

<div class="carousel-inner row w-100 mx-auto" #cara="Carousel">

We can then use the cara variable to access the public functions:

  <a class="carousel-control-prev" role="button" (click)="cara.prev(3)">
    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
    <span class="sr-only">Previous</span>
  </a>
  <a class="carousel-control-next" role="button" (click)="cara.next(3)">
    <span class="carousel-control-next-icon" aria-hidden="true"></span>
    <span class="sr-only">Next</span>
  </a>

Finally, mark 3 of the carousel-items as .active, so that 3 are displayed at the same time.

Here is a StackBlitz demo



回答3:

It's a long time of this question, but I can not resist to make another approach.

Disclaimer I stole 4 ideas of the great Netanel Basal and his amazing carousel

Disclamer 2 it's a long, long answer, so to animate to read, here is we are going to get

A carousel is only an html

<div class="wrap">
    <div #carousel></div>
</div>

where the div #carousel move to left/rigth with one animation.

The first idea from Nenatel I borrow is the animation. It's only has two functions: (I use severals variables to parametice the functions)

  @Input() timing = '500ms ease-in';
  slideWidth;    //width of each slide
  numberSlides;  //number of slides
  offset;        //a variable to move the origen of movimient

  private transitionCarousel(time: any, slide: number) {
    const offset = this.offset - this.slideWidth * this.numberSlides;
    const myAnimation: AnimationFactory = this.buildAnimation(offset, time);
    this.player = myAnimation.create(this.carousel.nativeElement);
    this.player.play();
  }

  private buildAnimation(offset, time: any) {
    return this.builder.build([
      animate(time == null ? this.timing : 0, style({ transform: `translateX(${offset}px)` }))
    ]);
  }

So, we call to

   transitionCarousel(null,4)  //to select the slide 4.

Well, I´m going to suppose I have 6 different images and we want show 4 at time. There are a two critical positions. Bellow our carousel in different states

 a)                            [0  1  2  3 ] 4  5  6
 b)                       0  1 [2  3  4  5 ] 6   
 c)                            [*  *  *  0 ]1  2  3  4  5  6
 d)           0  1  2  3  4  5 [6  *  *  * ]

So the case a) is when slide=0, the case b) is when slide=2 The case c) is when our slide 0 is in the 4th position and the case d) when slide=6

Clearly, the states c) and d) create us a problem because the * show empty espaces (and we don't want whites spaces). So, Why not repeat at first of our images the images 4, 5 and 6 and at the end the images 0 1 and 3, So our states c) and d) becomes like

 c)                            [4' 5' 6' 0 ] 1  2  3  4  5  6  0' 1' 2'
 d)  4' 5' 6' 0  1  2  3  4  5 [6  0' 1' 2']  

Well we need take account that the case c) -when slide=-3, it's identical than slide=4

 c)        4' 5' 6' 0  1  2  3 [4  5  6  0']1' 2'

So, if after a movement fall in a slide less than 0, we move instantly to the left

The other case to take account is d) when we click to the right button. In that case we need make two movements

 d)  4' 5' 6' 0  1  2  3  4  5 [6  0' 1' 2']  //slide=6
                           --click left button event--
                           //we move to the slide=-1 before transition
                          4' 5'[6' 0  1  2 ] 3  4  5  6  0' 1' 2' 
                          //and move to slide=3 in transition 
              4' 5' 6' 0  1  2 [3  4  5  6 ] 0' 1' 2' 

So, our function transitionCarousel becomes like

  private transitionCarousel(time: any, slide: number) {
    if (slide >= this.slides.length) {
        this.transitionCarousel(0, this.currentSlide - this.slidesQuantity)
        slide -= this.slidesQuantity;
    }

    const offset = this.offset - this.slideWidth * slide;
    const myAnimation: AnimationFactory = this.buildAnimation(offset, time);
    this.player = myAnimation.create(this.carousel.nativeElement);
    if (time != 0) {
      if (slide < 0) {
        this.player.onDone(() => {
          this.transitionCarousel(0, this.currentSlide)
        })
      }
      else
        this.currentSlide = slide;
    } this.player.play();
  }

Well, we need some mechanics to duplicate slides. We are going to do this using a structural directive (2nd stole to Netanel Basal)

@Directive({
  selector: '[carouselItem]'
})
export class CarouselDirective implements OnInit {

  constructor(
    public templateRef: TemplateRef<any>,
    public viewContainer: ViewContainerRef) { }

  ngOnInit()
  {
    this.viewContainer.createEmbeddedView(this.templateRef)
  }
}

See that templateRef and viewContainer it's declared as public, this allow us copy the template of each element

<div *carouselItem>Hello world</div>

inside another one.

Our carousel becomes like

<app-carousel #carousel>
  <ng-container *ngFor=" let item of items">
      <img *carouselItem [src]="item.img" >
  </ng-container>
</app-carousel>

where, e.g.

  items = [{ img: 'https://picsum.photos/200/200?random=1' },
  { img: 'https://picsum.photos/200/200?random=2' },
  { img: 'https://picsum.photos/200/200?random=3' },
  { img: 'https://picsum.photos/200/200?random=4' },
  { img: 'https://picsum.photos/200/200?random=5' },
  { img: 'https://picsum.photos/200/200?random=6' },
  { img: 'https://picsum.photos/200/200?random=7' },
  { img: 'https://picsum.photos/200/200?random=8' }
  ];

I want my carousel scalable, so I need know how many slides can be holder inside the carousel. Of course we can enclose the carousel in a div

<div style="width:600px">
    <app-carousel #carousel>
       ....
    </app-carousel>
</div>

And send the number of slides to our carousel, but it's better our carousel make the work for us. So we need know the width of the slides. Here the second last stole to Netanel. The idea of Netanel is to have an .html like

<div class="carousel-model">
    <ng-container [ngTemplateOutlet]="slides && slides.first?slides.first.templateRef:null">
    </ng-container>
</div>

if we make a directive that selected a class like

@Directive({
  selector: '.carousel-model'
})
export class CarouselSlideElement {
}

And defined a ViewChild as

  @ViewChild(CarouselSlideElement, { static: false, read: ElementRef }) slideElement: ElementRef

in ngAfterViewInit we can ask about

   this.slideElement.nativeElement.getBoundingClientRect()

to get the dimensions

Well, I put in a "timer" because if I'm loading images I can forget about the width of this image.

  ngAfterViewInit() {
    timer(0, 200).pipe(takeWhile(() => !this.slideWidth || !this.slides || !this.slides.first)).subscribe(() => {
      const square = this.slideElement.nativeElement.getBoundingClientRect();
      this.slideWidth = square.width;
      if (this.slideWidth && this.slides && this.slides.first)
        this.resizeCarousel()
    })
  }

One steps more. The part more complex is duplicate the slides. Remember that slides.first is our first slide and slides.last our last slide. I repaint all in the function resizeCarousel

  private resizeCarousel() {
    if (this.carousel) {
      let totalWidth = this.carousel.nativeElement.getBoundingClientRect().width;
      this.increment = Math.floor(totalWidth / this.slideWidth);

      let count = (this.increment * this.slideWidth) != totalWidth ? 1 : 0;
      this.offset = (totalWidth - 3 * (this.increment) * this.slideWidth) / 2 - this.slideWidth * count;
      console.log(totalWidth,count)
      this.slides.first.viewContainer.clear()
      this.slides.last.viewContainer.clear()

      this.slides.last.viewContainer.createEmbeddedView(this.slides.last.templateRef);
      let added = 0;
      this.slides.forEach((x, index) => {
        if (index && index >= (this.slides.length - this.increment - count)) {
          this.slides.first.viewContainer.createEmbeddedView(x.templateRef)
          added++
        }
        if (index < this.increment + count) {
          this.slides.last.viewContainer.createEmbeddedView(x.templateRef)
          added++
        }
      })
      this.slides.first.viewContainer.createEmbeddedView(this.slides.first.templateRef)
      this.currentSlide = 0;
      this.transitionCarousel(0, this.currentSlide);
    }
  }

I add a image to each side if dont' fit exactly the images

well, the last "borrow idea" that I stole to NetBasal, a hotListener to resize window

  @HostListener('window:resize', ['$event'])
  onResize(event) {
    if (this.slideWidth && this.slides && this.slides.first)
      this.resizeCarousel();
  }

And that's all folk, three functions to next, prev and set and "listo el pollo"

  prev() {
    this.transitionCarousel(null, this.currentSlide + this.increment);
  }

  next() {
    this.transitionCarousel(null, this.currentSlide - this.increment);
  }

  setSlide(slide: number) {
    slide = slide;
    this.transitionCarousel(null, slide);
  }

Update a better aproach to make the animation is used

private transitionCarousel(time: any, slide: number) {

const myAnimation: AnimationFactory = this.buildAnimation(time,slide);
this.player = myAnimation.create(this.carousel.nativeElement);
this.currentSlide = (slide >= this.slides.length) ? slide - this.slides.length :
                    (slide < 0) ? this.currentSlide = slide + this.slides.length :
                     slide
this.player.play();

}

private buildAnimation(time: any,slide:number ) { const animation:number=(slide >= this.slides.length)?1:(slide < 0)?2:0;

const offsetInitial = (slide >= this.slides.length)?
        this.offset - this.slideWidth * (this.currentSlide - this.slides.length):
        0;
let offsetFinal = (slide < 0)?
        this.offset - this.slideWidth * (slide + this.slides.length):
        0;

const offset = (slide >= this.slides.length)?
         this.offset - this.slideWidth * (slide-this.slides.length):
         this.offset - this.slideWidth * slide;

return animation==1 ? this.builder.build([
  style({ transform: `translateX(${offsetInitial}px)` }),
  animate(time == null ? this.timing : 0, style({ transform: `translateX(${offset}px)` }))
]) : animation==2 ? this.builder.build(sequence([
  animate(time == null ? this.timing : 0, style({ transform: `translateX(${offset}px)` })),
  style({ transform: `translateX(${offsetFinal}px` })]))
    : this.builder.build([
      animate(time == null ? this.timing : 0, style({ transform: `translateX(${offset}px)` }))
    ]);

}



回答4:

here is simple pure angular directive way implementation for multi item carousel

here you can configure how many items on a slide

i hope you can do a single item carousel too with this

https://github.com/amalroshan/angular-multiitem-carousel