Madogiwa Blog

主に技術系の学習メモに使っていきます。

Swiperでpagenationの中に前/次のnavigationを入れたい場合の実装メモ📝

以下のような感じでSwiper.jsを使って、pagenationの中にnavigationを入れて前後のスライドに移動できるようにするようなデザインを作りたいときにデモやネット上にもあまり情報が無く、結構悩んだのですが一定出来たのでやり方をメモしておきます📝

f:id:madogiwa0124:20201219014604g:plain

CSS等を使ったもっといいやり方があるかもしれないです。

swiperjs.com

前提事項

以下の環境での実装で検証したものになります。

"vue": "^2.6.11",
"swiper": "^5.4.5",
"vue-awesome-swiper": "^4.1.1",

実装memo

以下のような実装方針で今回は実装してみました。

  • pagenationのnodeを取得
  • swiperオブジェクトを引数に前/後のnavigationのインスタンスを生成
  • pagenationの一番最初に前、最後に後のnavigationを挿入
  • navigationを押下時にpropsで渡したswiperオブジェクトに前/後のスライドに移動するメソッドを呼び出す。

以下がswiperをwrapしたコンポーネントです。 Swiperの生成と管理する要素の設置、そしてmoutedでpagenation内に前/後のボタンを配置しています。

paginationclickableがtrueにしたときにpagenationクリック時の挙動がおかしくなります。。。おそらく要素のindexを使っており、pagenation内に他の要素が入るとおかしくなってそう?

<template>
  <div class="carousel">
    <swiper ref="mySwiper" :options="swiperOptions">
      <swiper-slide>Slide 1</swiper-slide>
      <swiper-slide>Slide 2</swiper-slide>
      <swiper-slide>Slide 3</swiper-slide>
      <swiper-slide>Slide 4</swiper-slide>
      <swiper-slide>Slide 5</swiper-slide>
      <div slot="pagination" class="swiper-pagination" />
    </swiper>
  </div>
</template>
<script>
import Vue from "vue";
import { Swiper, SwiperSlide } from "vue-awesome-swiper";
import CarouselPagenationNav from "./CarouselPagenationNav.vue";
import "swiper/css/swiper.min.css";

export default {
  name: "Carousel",
  components: {
    Swiper,
    SwiperSlide,
  },
  data() {
    return {
      swiperOptions: {
        loop: true,
        centeredSlides: true,
        slidesPerView: 1.5,
        watchOverflow: true,
        spaceBetween: 16,
        pagination: {
          el: ".swiper-pagination", 
          clickable: false // clickableはfalseにしないとうまく動かない
        },
      },
    };
  },
  computed: {
    swiper() {
      return this.$refs.mySwiper.$swiper;
    },
  },
  mounted() {
    const pagenation = this.$el.querySelector("div.swiper-pagination");
    const ComponentClass = Vue.extend(CarouselPagenationNav);
    // NOTE: 動的コンポーネントで送出したemitを親コンポーネントで補足する方法がわからなかったので、
    // 仕方無しにpropsで引き回して子コンポーネントでswiperのslide操作を行っている。
    const prevBtn = new ComponentClass({ propsData: { swiper: this.swiper, type: "prev" } });
    const nextBtn = new ComponentClass({ propsData: { swiper: this.swiper, type: "next" } });
    prevBtn.$mount();
    nextBtn.$mount();
    // pagenationの最初に戻るのボタンを配置
    pagenation.insertBefore(prevBtn.$el, pagenation.children[0]);
    // pagenationの最後に次へのボタンを配置
    pagenation.appendChild(nextBtn.$el);
  },
  methods: {},
};
</script>
<style lang="scss">
.carousel {
  .swiper-container {
    color: white;

    .swiper-slide {
      background-color: burlywood;
      height: 250px;
    }
  }
}
</style>

以下が前後のボタンのコンポーネントです。 前か後かによってクリック時に実行する処理をslidePrev() or slideNext()で切り替えています。

あとついでにpagenationとnavigationは<swiper>...</swiper>の外に配置し、スライドの外に表示されるようにしています。

<template>
  <button class="carousel-pagenation-nav" @click="handleOnClick">
    {{ typeText }}
  </button>
</template>
<script>
export default {
  name: "CarouselPagenationNav",
  props: {
    swiper: {
      type: Object,
      required: true,
    },
    type: {
      type: String,
      required: true,
    },
  },
  data() {
    return {};
  },
  computed: {
    isPrev() {
      return this.type == "prev";
    },
    typeText() {
      return this.isPrev ? "<" : ">";
    },
  },
  methods: {
    handleOnClick() {
      // propsで受け取ったswiperに対して戻る/進むのメソッドを実行
      this.isPrev ? this.swiper.slidePrev() : this.swiper.slideNext();
    },
  },
};
</script>
<style lang="scss"></style>

2021/01/17 追記

以下のような感じにすると特に動的にボタンを追加したりしなくても良さそうだったので追記

f:id:madogiwa0124:20210117160638g:plain

諸々修正していますがポイントは、以下です。

  • navigationに使う要素のclass名はswiper-button-prevswiper-button-next以外の値にしてデフォルトのスタイルを当てないようにして、スタイルのカスタマイズができるように
  • pagenationとnavigationの要素はspanにして横並びで表示させられるように
<template>
  <div class="carousel">
    <swiper ref="mySwiper" :options="swiperOptions">
      <swiper-slide>Slide 1</swiper-slide>
      <swiper-slide>Slide 2</swiper-slide>
      <swiper-slide>Slide 3</swiper-slide>
      <swiper-slide>Slide 4</swiper-slide>
      <swiper-slide>Slide 5</swiper-slide>
    </swiper>
    <div class="swiper-controll">
      <!-- NOTE: swiper-button-prev/nextにすると -->
      <!-- swiperのデフォルトのnavigationのスタイルがあたってしまうので別のclass名を設定 -->
      <span class="button-prev">&lt;</span>
      <span slot="pagenation" class="swiper-pagination" />
      <span class="button-next">&gt;</span>
    </div>
  </div>
</template>
<script>
import { Swiper, SwiperSlide } from "vue-awesome-swiper";
import "swiper/css/swiper.min.css";

export default {
  name: "Carousel",
  components: {
    Swiper,
    SwiperSlide
  },
  data() {
    return {
      swiperOptions: {
        slidesPerView: 1.5,
        slidesPerGroup: 1,
        slidesPerColumn: 1,
        slidesPerColumnFill: "column",
        centeredSlides: true,
        loop: true,
        watchOverflow: true,
        spaceBetween: 16,
        breakpoints: {
          480: {
            slidesPerView: 3,
            slidesPerGroup: 3,
            slidesPerColumn: 2,
            slidesPerColumnFill: "row",
            loop: false,
            centeredSlides: false,
          },
        },
        navigation: {
          nextEl: ".button-next",
          prevEl: ".button-prev",
        },
        pagination: {
          el: ".swiper-pagination",
          clickable: true // clickable: trueにしても動く
        },
      },
    };
  },
  computed: {
    swiper() {
      return this.$refs.mySwiper.$swiper;
    },
  },
  methods: {},
};
</script>
<style lang="scss">
// scopedにしたいけど.swiper-pagination-bulletのstyleが当たらないのでscopedにしていない。
// https://github.com/surmon-china/vue-awesome-swiper/issues/22
.carousel {
  .swiper-container {
    color: white;
    width: 100%;

    .swiper-slide {
      background-color: burlywood;
      height: 250px;
    }
  }

  .swiper-controll {
    margin-top: 5px;
    text-align: center;
    vertical-align: middle;

    .button-next,
    .button-prev {
      display: inline-block;
      border-radius: 50%;
      color: #ffffff;
      background-color: #007aff;
      height: 24px;
      width: 24px;
      padding: 0;

      @media screen and (max-width: 480px) {
        display: none;
      }
    }

    .swiper-pagination {
      position: static;
      display: inline;

      .swiper-pagination-bullet {
        margin: 0 2px 0 2px;
      }
    }
  }
}
</style>

参考資料

qiita.com

swiperjs.com