🪗
アニメーションで開閉する Vue.js アコーディオンコンポーネント
はじめに
タイトルの通り、Vue.js
で使えるアコーディオンコンポーネントを紹介します。
このようなコンポーネントはよく使うのですが、毎回考えて書くのは若干面倒なので記事としてまとめておきます。
プリプロセッサ
コード内では、以下のプリプロセッサを使っています。
- pug
- TypeScript
- sass
コンポーネントのコード
アニメーションはvue
の組み込みコンポーネントであるtransition
を使って実装します。
これにより、閉じている時は要素自体がDOM
から消えるので、色々とスッキリします。
shrinkable-container.vue
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from 'vue'
const props = defineProps<{
shrunk?: boolean
}>()
const innerShrunk = ref(props.shrunk)
const inner = ref<HTMLDivElement>()
const innerHeightCss = ref('')
const initialized = ref(false)
const calcHeight = () => {
if (!inner.value) return
innerHeightCss.value = inner.value.getBoundingClientRect().height + 'px'
initialized.value = true
}
watch(
() => props.shrunk,
async () => {
if (!initialized.value) {
innerShrunk.value = props.shrunk
await nextTick()
}
calcHeight()
await nextTick()
innerShrunk.value = props.shrunk
}
)
onMounted(calcHeight)
</script>
<template lang="pug">
transition
.shrinkable-container(
v-if="!innerShrunk"
)
.inner(
ref="inner"
)
slot
</template>
<style lang="sass" scoped>
.v-enter-active,
.v-leave-active
transition: height 0.5s
.v-enter-from,
.v-leave-to
height: 0
.v-enter-to,
.v-leave-from
height: v-bind(innerHeightCss)
.shrinkable-container
overflow: clip
width: fit-content
&.initial
transition: none
</style>
使い方
test-component.vue
<script setup lang="ts">
import { ref } from 'vue'
import ShrinkableContainer from './shrinkable-container.vue'
const isOpen = ref(false)
</script>
<template lang="pug">
.test
button(
@click="isOpen = !isOpen"
) Toggle Open
ShrinkableContainer(
:shrunk="!isOpen"
)
.content Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vel augue a quam iaculis tristique non vel est. Cras eu interdum sapien, eu bibendum felis. Suspendisse ac aliquet velit. Nulla lorem elit, euismod id massa facilisis, convallis dictum lectus. Nulla suscipit viverra risus, sit amet varius nisl mollis sit amet. Maecenas rutrum suscipit mi quis porttitor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus cursus nibh quis sapien varius mollis. Morbi id purus sodales, vulputate nibh non, mollis eros. Proin feugiat mi vel porttitor tempor.
</template>
gridを使うともっと楽?
今回は高さが可変な要素の高さを毎回計算してアニメーションする方式にしていますが、display:grid
を使ったもっと簡単な実装も存在するようです。
ただ、こちらを試したところ開閉のアニメーション時に謎の隙間ができるという現象に遭遇したため、使用を断念しました…
この方法ではコンテナの中にインナーとなる要素を設けていますが、コンテナのアニメーションとインナー要素のアニメーションがなぜかずれてしまうようです。
CodePen上で動かしているデモがありますが、CSS
のコード文末に以下を追加して動かすと分かりやすいです。
期待値としては赤のラインと黄色のラインの上下端がピッタリくっついて動いて欲しいのですが、実際には隙間ができてしまいます。
.accordion-body {
border: solid 1px red;
transition: 1s grid-template-rows ease;
}
.accordion-body > div {
border: solid 1px yellow;
}
この実装で期待通りに動けば、CSS
のみで完結するようになるのでとても便利なのですが…
おわりに
いつかまとめようと思って随分と時間が経ってしまいましたが、このコードが皆様および未来の自分の作業時間を1msec
でも短縮できれば幸いです。
Discussion