eureka

Nuxt(Vue)で連続したアコーディオン(スライドトグル)の開閉をする

4

意外とよく使うんだよね

明けましておめでとうございます。今年初の記念すべき内容は、NuxtやVueでの連続したアコーディオンの実装です笑

アコーディオンだけではなく、複数の開閉メニューがある場合とかにも使えます。

今回はこんな感じのを作ります。

完成時のイメージ

環境

nuxt 2.14.0

テンプレートを作成する

まずはテンプレート作成からです。

<template>
  <div class="accordion">
    <ul>
      <li v-for="item in getContents" :key="item.id">
        <button
          type="button"
          class="accordion__toggle"
        >
          <span v-text="item.name" />
        </button>
        <p v-text="item.content" />
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  computed: {
    getContents() {
      const data = [
        {
          id: 1,
          name: 'アコーディオン1',
          content: 'コンテンツコンテンツコンテンツ',
        },
        {
          id: 2,
          name: 'アコーディオン2',
          content: 'コンテンツコンテンツコンテンツ',
        },
        {
          id: 3,
          name: 'アコーディオン3',
          content: 'コンテンツコンテンツコンテンツ',
        },
      ]
      return data
    },
  },
}
</script>

<style lang="scss" scoped>
.accordion {
  background-color: #fafafa;
  color: #444;
  width: 100%;
  position: fixed;
  top: 67px;
  left: 0;
  overflow: scroll;
  height: 100vh;
  padding: 36px 16px;
}

.accordion__toggle {
  font-size: 22px;
  font-weight: bold;
  line-height: 2.818;
  text-align: left;
  display: block;
  width: 100%;
  border-bottom: 1px solid #ebebeb;
  position: relative;

  &::before,
  &::after {
    content: '';
    display: inline-block;
    width: 18px;
    height: 3px;
    background-color: #444;
    position: absolute;
    top: 50%;
    right: 22px;
  }

  &::before {
    transform: translate(0, -50%);
  }

  &::after {
    transition: all 0.3s ease-in-out;
    transform: translate(0, -50%) rotate(90deg);
  }
}
</style>

アコーディオンの実装

フラグを作る

まずは開閉用のフラグを作成します。
今回のアコーディオンは配列の中身をv-forで展開しているのですが、この配列のindexを使用して開閉フラグを操作します。

そのためまずは配列から作成します。

<script>
export default {
  data() {
    return {
      isOpen: [],
    }
  },
}
</script>

続いて、開閉するアコーディオンの数だけ作成したisOpenの配列にfalseフラグを挿入します。

  created() {
    this.isOpen = Array(this.getContents.length).fill(false)
  },

これでisOpenにアコーディオンの数だけフラグが格納されました。

フラグをテンプレートで使用する

先程作成したテンプレートに作成したフラグを適用させていきます。

<li v-for="(item, index) in getContents" :key="item.id">
  <button
    type="button"
    class="accordion__toggle"
    :class="{ 'is-active': isOpen[index] }"
  >
    <span v-text="item.name" />
  </button>
  <p v-show="isOpen[index]" v-text="item.content" />
</li>

v-forの第二引数にindexを追加して、表示と非表示を切り替えたいコンテンツにv-showを追加しました。

また、buttonタグの+アイコンも、開閉中は−のアイコンに変えたいのでクラスを付与しました。
これに合わせてスタイルも追加しましょう。

.accordion__toggle {
  // 略

  &.is-active {
    &::after {
      transform: translate(0, -50%) rotate(0deg);
    }
  }
}

フラグの切り替え

開閉に対応するフラグをtrueに切り替えていきます。
関数を作成します。

  methods: {
    handleToggle(index) {
      this.isOpen.splice(index, 1, !this.isOpen[index])
    },
  },

index番号を引数にとり、spliceメソッドを使用して対応する配列番号の値を反転させます。
つまりisOpenのindex番目がfalseだったらtrueに、trueだったらfalseに、という内容になります。

buttonタグでclick時に関数を呼び出すように追記します。

  <button
    type="button"
    class="accordion__toggle"
    :class="{ 'is-active': isOpen[index] }"
    @click="handleToggle(index)"
  >
    <span v-text="item.name" />
  </button>

これで開閉できるようになりました。

完成時の開閉の様子

開閉にアニメーションをつける

ついでにせっかくなのでtransitionを使用して開閉アニメーションも追加します。

  <transition
    name="topSlide"
    @before-enter="beforeEnter"
    @enter="enter"
    @before-leave="beforeLeave"
    @leave="leave"
  >
    <p v-show="isOpen[index]" v-text="item.content" class="topSlide" />
  </transition>

開閉するpタグにもtopSlideというクラス名を追加しました。

スタイルも追加します。

.topSlide {
  transition: height 0.3s ease-in-out;
  overflow: hidden;
}

.topSlide-enter-active {
  animation-duration: 0.3s;
  animation-fill-mode: both;
}

.topSlide-leave-active {
  animation-duration: 0.3s;
  animation-fill-mode: both;
}

イベントを使用して高さを取得する関数を作成します。

methods: {
  // 略
  beforeEnter(el) {
    el.style.height = '0'
  },

  enter(el) {
    el.style.height = el.scrollHeight + 'px'
  },

  beforeLeave(el) {
    el.style.height = el.scrollHeight + 'px'
  },

  leave(el) {
    el.style.height = '0'
  },
}

これでアコーディオンっぽくなりました!

アニメーションが適用された様子

全体のコード

今までの全体のコードはこんな感じです。

<template>
  <div class="accordion">
    <ul>
      <li v-for="(item, index) in getContents" :key="item.id">
        <button
          type="button"
          class="accordion__toggle"
          :class="{ 'is-active': isOpen[index] }"
          @click="handleToggle(index)"
        >
          <span v-text="item.name" />
        </button>
        <transition
          name="topSlide"
          @before-enter="beforeEnter"
          @enter="enter"
          @before-leave="beforeLeave"
          @leave="leave"
        >
          <p v-show="isOpen[index]" v-text="item.content" class="topSlide" />
        </transition>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isOpen: [],
    }
  },

  computed: {
    getContents() {
      const data = [
        {
          id: 1,
          name: 'アコーディオン1',
          content: 'コンテンツコンテンツコンテンツ',
        },
        {
          id: 2,
          name: 'アコーディオン2',
          content: 'コンテンツコンテンツコンテンツ',
        },
        {
          id: 3,
          name: 'アコーディオン3',
          content: 'コンテンツコンテンツコンテンツ',
        },
      ]
      return data
    },
  },

  created() {
    // アコーディオンの数だけ開閉フラグを作成
    this.isOpen = Array(this.getContents.length).fill(false)
  },

  methods: {
    // メニューを開閉する
    handleToggle(index) {
      this.isOpen.splice(index, 1, !this.isOpen[index])
    },

    // スライド開閉要素の高さ取得
    beforeEnter(el) {
      el.style.height = '0'
    },

    enter(el) {
      el.style.height = el.scrollHeight + 'px'
    },

    beforeLeave(el) {
      el.style.height = el.scrollHeight + 'px'
    },

    leave(el) {
      el.style.height = '0'
    },
  },
}
</script>

<style lang="scss" scoped>
.accordion {
  background-color: #fafafa;
  color: #444;
  width: 100%;
  position: fixed;
  top: 67px;
  left: 0;
  overflow: scroll;
  height: 100vh;
  padding: 36px 16px;
}

.accordion__toggle {
  font-size: 22px;
  font-weight: bold;
  line-height: 2.818;
  text-align: left;
  display: block;
  width: 100%;
  border-bottom: 1px solid #ebebeb;
  position: relative;

  &::before,
  &::after {
    content: '';
    display: inline-block;
    width: 18px;
    height: 3px;
    background-color: #444;
    position: absolute;
    top: 50%;
    right: 22px;
  }

  &::before {
    transform: translate(0, -50%);
  }

  &::after {
    transition: all 0.3s ease-in-out;
    transform: translate(0, -50%) rotate(90deg);
  }

  &.is-active {
    &::after {
      transform: translate(0, -50%) rotate(0deg);
    }
  }
}

.topSlide {
  transition: height 0.3s ease-in-out;
  overflow: hidden;
}

.topSlide-enter-active {
  animation-duration: 0.3s;
  animation-fill-mode: both;
}

.topSlide-leave-active {
  animation-duration: 0.3s;
  animation-fill-mode: both;
}
</style>

じつはこれはアコーディオンじゃない

アコーディオンといっているこの効果ですが、本当はアコーディオンとは言わないみたいですね。スライドトグルだったかな、、。
アコーディオンの定義はひとつ開いたら開いているものは閉じる、結果複数のコンテンツが開いていることがないもの、みたいです。

今回はアコーディオンの定義が曖昧な人が多かったのでこのような表現で記載しました。本当にアコーディオンを探していた方スミマセン・・!

アコーディオンにするなら

本物のアコーディオンを実装するのであればtrueに切り替える前にisOpenを初期化してしまえば良いです。

  handleToggle(index) {
    if (!this.isOpen[index]) {
      this.isOpen = Array(this.getContents.length).fill(false)
    }
    this.isOpen.splice(index, 1, !this.isOpen[index])
  },
アコーディオンの開閉の様子

終わりに

紛らわしい書き方をしてすみませんでした!
原理がわかると簡単にできるようになり応用も可能です。私はこの方法でチェックリストとかも作りました!
参考になればと思います!

4