📚

Vue3でライブラリを使わずにカルーセルビューを実装する

2020/10/25に公開

はじめまして、初投稿のかずうぉんばっとです。

今回はVue3でライブラリを使わずにカルーセルビューを実装する方法をご紹介します。

カルーセルを作りたいなと思いライブラリを漁ったのですが、typescript対応してない、大きいUIコンポーネントライブラリ入れるのもな...とあまりいいライブラリが見当たらなかったので、自分で実装することにしました。
出来たのが以下のようなシンプルなものです。

早速ですが、以下が全体のソースです。(環境はVue3 Typescript環境です。)
gistはこちら

carousel.vue
<template>
  <div class="container">
    <!-- ページをクリッピングするコンテナ -->
    <div class="clipping-container">
      <!-- ページ全体、このleftをtransitionでスライドさせてページを動かす -->
      <div class="pages" :style="{ left: currentLeft }">
        <div class="page">1</div>
        <div class="page">2</div>
        <div class="page">3</div>
      </div>
    </div>
    <!-- ページを表現するドット -->
    <div class="dots">
      <!-- 現在のページはdot-currentクラスが当たる -->
      <span
        v-for="index in totalPage"
        :class="{ dot: true, 'dot-current': isCurrentPage(index) }"
        :key="index"
      >
      </span>
    </div>
    <div>
      <button @click="backwardPage"></button>
      <button @click="forwardPage"></button>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name: "HelloWorld",
  data() {
    return {
      currentPage: 1, // 現在のページ
      totalPage: 3, // ページの全数
      pageWidth: 200, // 1ページの幅
    };
  },
  methods: {
    // ページを1つ進める
    forwardPage() {
      // 最後のページの場合return
      if (this.currentPage === this.totalPage) {
        return;
      }
      this.currentPage += 1;
    },

    // ページを1つ戻す
    backwardPage() {
      // 最初のページの場合return
      if (this.currentPage === 1) {
        return;
      }
      this.currentPage -= 1;
    },

    // 現在のページをpositionに変換
    pageToPosition(): number {
      return -this.pageWidth * (this.currentPage - 1);
    },

    isCurrentPage(page: number) {
      return this.currentPage === page;
    },
  },
  computed: {
    // 現在のpositionからleftに変換
    currentLeft(): string {
      return String(this.pageToPosition()) + "px";
    },
  },
});
</script>

<style scoped>
.clipping-container {
  clip-path: inset(0);
  position: relative;
  height: 200px;
  width: 200px;
  display: inline-block;
}
.pages {
  display: flex;
  /* ここにtransitionをつけることでpagesのleftを変更した場合にアニメーションさせることができる */
  transition: left 0.5s ease;
  position: absolute;
  left: 0;
}
.page {
  width: 200px;
  height: 200px;
  background-color: aqua;
  border: 1px solid white;
  box-sizing: border-box;
}
.dots {
  width: 30px;
  display: flex;
  justify-content: space-between;
  margin: auto;
  margin-bottom: 16px;
}
.dot {
  height: 8px;
  width: 8px;
  background-color: #bbb;
  border-radius: 50%;
  display: inline-block;
}
.dot-current {
  background-color: skyblue;
}
</style>

ある程度コメントにも書いてあるんですが、これで終わりだと芸がないのでどういう仕組みで動いているのか簡単にご説明したいと思います。

コンポーネントが保持するデータ

コンポーネントは以下のデータを保持します。
後々このデータを利用するのでご確認ください。

data() {
  return {
    currentPage: 1, // 現在のページ
    totalPage: 3,   // ページの全数
    pageWidth: 200  // 1ページの幅
  }
}

コンテンツのページ移動

今回のメインとなるページ移動について、以下のように各ページのコンテンツはpagesクラス内に並べます。
ここでpagesのstyle leftを動かせるようにcurrentLeftというcomputedにbindしておきます。

<!-- ページ全体、このleftをtransitionでスライドさせてページを動かす -->
<div class="pages" :style="{left: currentLeft}">
  <div class="page">
    1
  </div>
  <div class="page">
    2
  </div>
  <div class="page">
    3
  </div>
</div>

pagesのcssではpositonをabsoluteにしておき、さらにtransitionをつけることで、leftの値が変化したときにアニメーションするようになります。

.pages {
  display: flex;
  /* ここにtransitionをつけることでpagesのleftを変更した場合にアニメーションさせることができる */
  transition: left 0.5s ease;
  position: absolute;
  left: 0;
}

これで準備完了です。
実際に→ボタンをクリックすると

  1. ページがインクリメントされます。
forwardPage() {
  ...
  this.currentPage += 1
},
  1. currentLeftの値はcurrentPageの値に依存したcomputedプロパティなので、値が変化します。
methods: {
  // ページを1つ進める
  forwardPage() {
    ...
    this.currentPage += 1
  },
  ...
  // 現在のページをpositionに変換
  pageToPosition(): number {
    return - this.pageWidth * (this.currentPage - 1)
  },
},
computed: {
  // 現在のpositionからleftに変換
  currentLeft(): string {
    return String(this.pageToPosition()) + 'px'
  }
}
  1. currentLeftの変化によって、pagesクラスのleftポジションの値が変化し、transitionでアニメーションしながら左方向に移動します。
<div class="pages" :style="{left: currentLeft}">
</div>

といった流れでページの移動を行うことができます。

ページのクリッピング

この状況だと画面に全てのページが表示されてしまっているので、現在のページだけ表示するようにクリッピングマスクします。
そのためにまずpagesの親要素にclipping-containerクラスのdomを作り、

<!-- ページをクリッピングするコンテナ -->
<div class="clipping-container">
  <div class="pages" :style="{left: currentLeft}">
    <!-- ... -->
  </div>
</div>

これにclip-pathを使って、ページのサイズでクリッピングします。

.clipping-container {
  clip-path: inset(0);
  position: relative;
  height: 200px;
  width: 200px;
  display: inline-block;
}

これだけで簡単にマスクが出来てしまいます。

ドットの移動

最後にページの○ ● ○の部分です。
描画はtotalPageプロパティを使ってv-forで描画します。

<div class="dots">
  <!-- 現在のページはdot-currentクラスが当たる -->
  <span 
  v-for="index in totalPage" 
  :class="{ dot: true, 'dot-current': isCurrentPage(index)}" 
  :key="index">
  </span>
</div>

さらにこのとき、以下のように書くことでdot-currentクラスが、現在のページのみに当たるようにします。

:class="{ dot: true, 'dot-current': isCurrentPage(index)}"
isCurrentPage(page: number) {
  return this.currentPage === page
}

これで現在ページのみにdot-currentクラスが当たるようになります。

まとめ

いかがでしたでしょうか?

今回のように自分で実装することで

  • ライブラリの依存を減らせる
  • 内部的な仕組みが完全に把握できているので、バグの修正が容易。カスタマイズ性も高い。
  • 実装する上で勉強になることが多い(今回css-clipは初めて知りました。)

などメリットも多いので、用法用量を守りつつ、こういった形で自分で何か実装してみることをオススメします🙂

Discussion