Vue-awesome-swiper: [Bug] Click event not propagated after a full loop

Created on 22 Dec 2017  ·  11Comments  ·  Source: surmon-china/vue-awesome-swiper

Vue.js version and component version:

Vuejs: 2.5.2
Component: V4.0.7 (for the repro) and ^3.0.6 (for my project)

Reproduction Link

https://jsfiddle.net/Lvev4md5/

Steps to reproduce

Do a a list of slide with the infinite loop mode one. When doing the whole loop, the click event is not propagated. In the repro link:

  • Click on the button and watch the console
  • The message: "clicked" shows up
  • Click and the right arrow and click the button
  • The message: "clicked" shows up
  • Click and the right arrow and click the button
  • The message doesn't appear anymore. because we are back to the slide one.

The bug also appears when rotating from the right to left.

What is Expected?

Message should be displayed in any case, even after a full loop.

What is actually happening?

Click event not propagated

Most helpful comment

This is how I ended up handling the issue, however still a bit hacky :)

<template>
    <div id="slider">
      <swiper
        :options="swiperOption"
        ref="swiper"
        @click.native="sliderClicked"
      >
        <swiper-slide
          v-for="(item, index) in items"
          :key="`${index}`"
        >
            Slide {{index}}
        </swiper-slide>
        <div class="swiper-pagination" slot="pagination"></div>
        <div class="swiper-button-prev" slot="button-prev"></div>
        <div class="swiper-button-next" slot="button-next"></div>
      </swiper>
    </div>
</template>

<script>
export default {
  data() {
    return {
      swiperOption: {
        effect: 'slide',
        loop: true,
        autoplay: {
          delay: 5000
        },
        pagination: {
          el: '.swiper-pagination',
          clickable: true
        },
        navigation: {
          nextEl: '.swiper-button-next',
          prevEl: '.swiper-button-prev',
        }
      }
    }
  },
  computed: {
    swiper() {
      return this.$refs.swiper.swiper
    }
  },
  methods: {
    sliderClicked: function (event) {
      if(
        !event.target.classList.contains('swiper-pagination-bullet') &&
        !event.target.classList.contains('swiper-button-next') &&
        !event.target.classList.contains('swiper-button-prev')
      ) {
        let item = this.items[this.swiper.realIndex];
        console.log('clicked item', item);
        //this.$router.push();
      }
    }
  }
}
</script>

All 11 comments

Sorry I don't understand chinese. The solution is not to use loop ?

Because, if the loop option is enabled, the swiper will copy two dom nodes at the beginning and end, but the event will not be copied, so any event bound is invalid. In addition, the loop option will cause a lot of unpredictable problems, which is the swiper It is recommended to avoid the use; In addition, the final solution should be:

 <swiper :options="swiperOption" @click.native="slideClicked(swiper.activeIndex.slide.link)">
      <swiper-slide v-for="(slide, index) in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]">
        Slide {{ index }}
      </swiper-slide>
</swiper>

<swiper :options="swiperOption" @click.native="slideClicked(swiper.activeIndex)">
<swiper-slide v-for="(slide, index) in data">
{{ data.img}}
</swiper-slide>
</swiper>
在配置中添加
loop : true,//循环
loopAdditionalSlides : this.data.length,//动态编辑数据的长度
在methods方法中
slideClicked(index){
console.log(index)
let dataIndex = index%this.data.length
console.log(dataIndex)
console.log(this.data[dataIndex])
this.$router.push(this.data[dataIndex].url)
},

It seems like a hacky way of getting it to work. And it introduces other issues as the "slideClicked" function will be triggered for any item clicked within the swiper.

An example is if you use navigation: { nextEl, prevEl }.

I'm aware that this can be prevented by stopping event propagation, but that would require extra tweaking of the plugin.

loop functionality is duplicating only DOM nodes, meaning that your duplicated slides for the loop don't have their JS code appended as well, only the rendered result.

I spent 2 days trying to figure this out, so by using the original Swiper's methods and events, I came to a semi-infinite-loop feature:

add the following to your swiper's config

loop: true,
loopedSlides: 1,
on: {
   slideChange: function () {
      let lastVisibleItem = this.realIndex + this.params.slidesPerView
      let slidesLength = this.slides.length - 2
      let lastVisibleIndex = this.realIndex + this.params.slidesPerView
      // if swiper reaches the end of displayed items, goToNext redirects swiper to the start
      if (lastVisibleItem > slidesLength) {
         this.slideTo(1)
      }
      // if swiper wants to go before the first item, then forward swiper to the last item
      if (lastVisibleIndex >= this.slides.length) {
         this.slideTo((slidesLength - this.params.slidesPerView) + 1)
      }
   }
}

This is how I ended up handling the issue, however still a bit hacky :)

<template>
    <div id="slider">
      <swiper
        :options="swiperOption"
        ref="swiper"
        @click.native="sliderClicked"
      >
        <swiper-slide
          v-for="(item, index) in items"
          :key="`${index}`"
        >
            Slide {{index}}
        </swiper-slide>
        <div class="swiper-pagination" slot="pagination"></div>
        <div class="swiper-button-prev" slot="button-prev"></div>
        <div class="swiper-button-next" slot="button-next"></div>
      </swiper>
    </div>
</template>

<script>
export default {
  data() {
    return {
      swiperOption: {
        effect: 'slide',
        loop: true,
        autoplay: {
          delay: 5000
        },
        pagination: {
          el: '.swiper-pagination',
          clickable: true
        },
        navigation: {
          nextEl: '.swiper-button-next',
          prevEl: '.swiper-button-prev',
        }
      }
    }
  },
  computed: {
    swiper() {
      return this.$refs.swiper.swiper
    }
  },
  methods: {
    sliderClicked: function (event) {
      if(
        !event.target.classList.contains('swiper-pagination-bullet') &&
        !event.target.classList.contains('swiper-button-next') &&
        !event.target.classList.contains('swiper-button-prev')
      ) {
        let item = this.items[this.swiper.realIndex];
        console.log('clicked item', item);
        //this.$router.push();
      }
    }
  }
}
</script>

This is how I ended up handling the issue, however still a bit hacky :)

<template>
  <div id="slider">
      <swiper
        :options="swiperOption"
        ref="swiper"
        @click.native="sliderClicked"
      >
        <swiper-slide
          v-for="(item, index) in items"
          :key="`${index}`"
        >
          Slide {{index}}
        </swiper-slide>
        <div class="swiper-pagination" slot="pagination"></div>
        <div class="swiper-button-prev" slot="button-prev"></div>
        <div class="swiper-button-next" slot="button-next"></div>
      </swiper>
    </div>
</template>

<script>
export default {
  data() {
    return {
      swiperOption: {
        effect: 'slide',
        loop: true,
        autoplay: {
          delay: 5000
        },
        pagination: {
          el: '.swiper-pagination',
          clickable: true
        },
        navigation: {
          nextEl: '.swiper-button-next',
          prevEl: '.swiper-button-prev',
        }
      }
    }
  },
  computed: {
    swiper() {
      return this.$refs.swiper.swiper
    }
  },
  methods: {
    sliderClicked: function (event) {
      if(
        !event.target.classList.contains('swiper-pagination-bullet') &&
        !event.target.classList.contains('swiper-button-next') &&
        !event.target.classList.contains('swiper-button-prev')
      ) {
        let item = this.items[this.swiper.realIndex];
        console.log('clicked item', item);
        //this.$router.push();
      }
    }
  }
}
</script>

This is how I ended up handling the issue, however still a bit hacky :)

<template>
  <div id="slider">
      <swiper
        :options="swiperOption"
        ref="swiper"
        @click.native="sliderClicked"
      >
        <swiper-slide
          v-for="(item, index) in items"
          :key="`${index}`"
        >
          Slide {{index}}
        </swiper-slide>
        <div class="swiper-pagination" slot="pagination"></div>
        <div class="swiper-button-prev" slot="button-prev"></div>
        <div class="swiper-button-next" slot="button-next"></div>
      </swiper>
    </div>
</template>

<script>
export default {
  data() {
    return {
      swiperOption: {
        effect: 'slide',
        loop: true,
        autoplay: {
          delay: 5000
        },
        pagination: {
          el: '.swiper-pagination',
          clickable: true
        },
        navigation: {
          nextEl: '.swiper-button-next',
          prevEl: '.swiper-button-prev',
        }
      }
    }
  },
  computed: {
    swiper() {
      return this.$refs.swiper.swiper
    }
  },
  methods: {
    sliderClicked: function (event) {
      if(
        !event.target.classList.contains('swiper-pagination-bullet') &&
        !event.target.classList.contains('swiper-button-next') &&
        !event.target.classList.contains('swiper-button-prev')
      ) {
        let item = this.items[this.swiper.realIndex];
        console.log('clicked item', item);
        //this.$router.push();
      }
    }
  }
}
</script>

you are so so so so so so so Smart !!! thank you, bsthomsen!

If anyone is reading this and it's not too late, I'd strongly suggest you don't use this plugin. It's got so many bugs and issues that it will cause you way more problems than just building your own solution.

Having said that, if you are trying to solve this solution, I think I have a better way, kind of taking the idea of jQuery's .on() method.

<template>
    <div id="slider">
        <swiper
            ref="swiper"
            v-swiper:swiper="swiperOption"
            @click.native="sliderClicked"
        >
            <swiper-slide
                v-for="(item, index) in items"
                :key="`${index}`"
            >
                <span class="go-home">
                    Go back to start of slideshow
                </span>

                <span
                    class="some-thing"
                    :data-index="index"
                >
                    Slide {{ index }}
                </span>
            </swiper-slide>
        </swiper>
    </div>
</template>

<script>
import { delegate } from "~/utils/tools.js"

export default {
    data() {
        return {
            swiperOption: {
                effect: "slide",
                loop: true,
                autoplay: {
                    delay: 5000
                }
            }
        }
    },
    mounted() {
        // Below we monitor for clicks on name because Awesome Swiper sucks!
        // Have to store this so we can access it in click handlers
        const $this = this

        // Go back to start
        delegate(this.$el, "click", ".go-home", function(event) {
            $this.goToSlide(0)
        })

        // Scroll down link
        delegate(this.$el, "click", ".some-thing", function(event) {
            console.log("This was clicked on:", this)
            console.log("Slide index:", parseInt(this.dataset.index))
        })
    },
    methods: {
        goToSlide(index) {
            this.swiper.slideToLoop(index)
        }
    }
}
</script>

And then in my ~/utils/tools.js file I have this function, which I took from here.

export function delegate(el, evt, sel, handler) {
    // Shim Matches
    if (!Element.prototype.matches) {
        Element.prototype.matches =
            Element.prototype.matchesSelector ||
            Element.prototype.webkitMatchesSelector ||
            Element.prototype.mozMatchesSelector ||
            Element.prototype.msMatchesSelector ||
            Element.prototype.oMatchesSelector ||
            function(s) {
                var matches = (
                        this.document || this.ownerDocument
                    ).querySelectorAll(s),
                    i = matches.length
                while (--i >= 0 && matches.item(i) !== this) {}
                return i > -1
            }
    }

    // Loop through everything of type event until it matches the selector
    el.addEventListener(evt, function(event) {
        var t = event.target
        while (t && t !== this) {
            if (t.matches(sel)) {
                handler.call(t, event)
            }
            t = t.parentNode
        }
    })
}

I used a similar but less verbose method of resolving the problem.

 <swiper
            ref="swiper"
            v-swiper:swiper="swiperOption"
            @click="sliderClicked"
        >

and

 <swiper-slide
                v-for="(item, index) in items"
                :key="`${index}`" v-bind-data-index="index"
            >

and

methods: {
    sliderClicked(e) {
        /* You may need to change e.target to something more specific, or bind the data attribute at the various layers that the click can occur at */
        let index = e.target.getAttribute('data-index');
        /* Now that you have the index / object that was clicked, handle the click here instead of at the slide level */
    }

@drewbaker vue-awesome-swiper is just a thin wrapper over swiper.js library which actually has design issue described here, so you should give your "sucks" credits to them. Btw, if you have better alternatives for swiper/slider lib that could cover so many use cases as swiper.js - I would gladly look into it as I guess thousands of other people who starred this and original library

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wuxiaobin1995 picture wuxiaobin1995  ·  4Comments

kooshkestani picture kooshkestani  ·  5Comments

bourhaouta picture bourhaouta  ·  3Comments

arpagrawal picture arpagrawal  ·  6Comments

tbcinteractive picture tbcinteractive  ·  6Comments