背景
Vue3のtransitionタグを用いたanimationの実装をした際の出来事です。
- 5種類の値に応じて5種類のcomponent表示を出し分ける(フェードイン・フェードアウトで)
という実装がありました。
Vue.jsのtransitionで実装しようとしたのですが、
transitionタグの中は、単一要素/コンポーネントでなければならないと怒られ、
仕方なくtransitionタグ、v-if、componentをソースコード内に5つ連発して書いた経緯があります。
その時に気持ち悪さをものすごく感じていたのですが、何せ時間がなくて、、、
さすがに次はtransitionタグ、v-if、componentを連発したくないので、改善策を考えてみました。
開発環境
macOS: Monterey 12.4
Vite: 2.9.2
Vue: 3.2.25
Typescript: 4.5.4
まずは最小構成で動かす
まずは最小構成でフェードイン・フェードアウトできるようにします。
ToggleAをクリックすると、textAがフェードインしたり、フェードアウトできるように実装します。
もちろんこれは動きます。
<script setup lang="ts">
import { ref } from "vue";
const isShowA = ref<boolean>(false);
</script>
<template>
<button @click="isShowA = !isShowA">
ToggleA
</button>
<transition name="fade" mode="out-in">
<div v-if="isShowA">
textA
</div>
</transition>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 1s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
エラーが出るケース
もう一つフェードイン・フェードアウトさせる文字を追加したいので、
同じ流れで処理を加えます。
<script setup lang="ts">
import { ref } from "vue";
const isShowA = ref<boolean>(false);
const isShowB = ref<boolean>(false);
</script>
<template>
<button @click="isShowA = !isShowA">
ToggleA
</button>
<button @click="isShowB = !isShowB">
ToggleB
</button>
<transition name="fade" mode="out-in">
<div v-if="isShowA">
textA
</div>
<div v-if="isShowB">
textB
</div>
</transition>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
すると単一要素/コンポーネントでなければならないとviteに怒られます。
なぜなら、isShowA, isShowBともにtrueになる可能性があり、transitionの中身が単一ではないパターンがあるからです。
[plugin:vite:vue] <Transition> expects exactly one child element or component.
自分がよくなかったと感じている実装
一個一個transitionで囲みました。
<script setup lang="ts">
import { ref } from "vue";
const isShowA = ref<boolean>(false);
const isShowB = ref<boolean>(false);
</script>
<template>
<button @click="isShowA = !isShowA">
ToggleA
</button>
<button @click="isShowB = !isShowB">
ToggleB
</button>
<transition name="fade" mode="out-in">
<div v-if="isShowA">
textA
</div>
</transition>
<transition name="fade" mode="out-in">
<div v-if="isShowB">
textB
</div>
</transition>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
条件の数だけtransitionタグを書く形です。
条件の数が5種類とかになると、
単純にtransitionタグを5つ書く羽目になっていました。
それ以上の数の条件があったとしたら地獄です。
それではここから解決策を考えていきます。
解決策: 条件に応じて動的にcomponentを切り替える
transitionタグの中身の記述をcomponent一つにして、条件に応じてcomponentを動的に切り替える事で、前述の問題をクリアする事ができます。
動的にcomponentを切り替える方法については公式ドキュメントを参照ください。
また、公式のtransitionのページにも記載はされていますが、
defineComponent、script setupの書き方の記載が現時点では無かったので書いてみました。
それでは、まずは一つのcomponentを表示できるようにしていきましょう。
はじめにcomponentAを定義します。
<template>
<p>textA</p>
</template>
componentAを表示できるようにします。
<script setup lang="ts">
import ComponentA from "@/components/componentA.vue"
import { ref } from "vue";
const currentComponent = ref(ComponentA)
</script>
<template>
<component :is="currentComponent"></component>
</template>
次に、componentBを定義します。
<template>
<p>textB</p>
</template>
次に、componentA, componentBを表示する条件と、その時に表示するcomponentをMapとして定義します。
これで条件をキーとしてcomponentを取り出せるようになります。
条件分岐を書かなくて済む方が好きなので私はMapを用いますが、ここは好みの問題と思います。
また、ユニオン型を用いて値に制約をかけます。
本題とは外れるので、詳しくは以下参照ください。
今回はA, Bという値の条件を設定していますが、
単純にbooleanに応じてcomponentを出し分けるパターンもあると思いますので、
プロジェクトによって柔軟に変えていきましょう。
<script setup lang="ts">
import ComponentA from "@/components/componentA.vue"
import ComponentB from "@/components/componentB.vue"
import { ref } from "vue";
type Condition = 'A' | 'B'
type Component = typeof ComponentA | typeof ComponentB
const componentMap = new Map<Condition, Component>([
['A', ComponentA],
['B', ComponentB],
])
let condition: Condition = 'A'
const currentComponent = ref<Component>(ComponentA)
</script>
<template>
<component :is="currentComponent"></component>
</template>
あとは、Toggleをクリックしたときに条件A, Bを切り替えるようにします。
そしてその条件A, Bをキーとして、ComponentA, ComponentBを取得し、切り替えます。
<script setup lang="ts">
import ComponentA from "@/components/componentA.vue"
import ComponentB from "@/components/componentB.vue"
import { ref } from "vue";
type Condition = 'A' | 'B'
type Component = typeof ComponentA | typeof ComponentB
const componentMap = new Map<Condition, Component>([
['A', ComponentA],
['B', ComponentB],
])
let condition: Condition = 'A'
const currentComponent = ref<Component>(ComponentA)
const changeComponent = (condition: Condition) => {
currentComponent.value = componentMap.get(condition)!
}
const changeCondition = () => {
if(condition === 'A'){
condition = 'B'
} else {
condition = 'A'
}
changeComponent(condition)
}
</script>
<template>
<button @click="changeCondition">
Toggle
</button>
<transition name="fade" mode="out-in">
<component :is="currentComponent"></component>
</transition>
</template>
<style scoped lang="scss">
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
これでToggleクリックの度に、textA, textBがフェードイン・フェードアウトを繰り返し、切り替わります。
実際は、Mapの定義は別tsファイルに切り出してexportする事になるため、
このvueファイル内はもう少しスッキリするでしょう。
まとめ
条件の数だけtransition、v-if、 componentを連発してしまった過去の経験を反省しつつ、
この経験を次に活かしていきたい所存です。