Vue3 transitionタグ内で条件に応じてComponentを切り替える

# 背景
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がフェードインしたり、フェードアウトできるように実装します。
もちろんこれは動きます。
``` sample.vue
<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>
```

# エラーが出るケース
もう一つフェードイン・フェードアウトさせる文字を追加したいので、
同じ流れで処理を加えます。

``` sample.vue
<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で囲みました。
``` sample.vue
<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を切り替える方法については[公式ドキュメント][1]を参照ください。
また、公式の[transition][4]のページにも記載はされていますが、
defineComponent、script setupの書き方の記載が現時点では無かったので書いてみました。


それでは、まずは一つのcomponentを表示できるようにしていきましょう。
はじめにcomponentAを定義します。
``` componentA.vue
<template>
  <p>textA</p>
</template>
```

componentAを表示できるようにします。
``` sample.vue
<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を定義します。
``` componentB.vue
<template>
  <p>textB</p>
</template>
```

次に、componentA, componentBを表示する条件と、その時に表示するcomponentをMapとして定義します。
これで条件をキーとしてcomponentを取り出せるようになります。
条件分岐を書かなくて済む方が好きなので私はMapを用いますが、ここは好みの問題と思います。

また、ユニオン型を用いて値に制約をかけます。
本題とは外れるので、詳しくは以下参照ください。
- [Map][2]
- [ユニオン型][3]


今回はA, Bという値の条件を設定していますが、
単純にbooleanに応じてcomponentを出し分けるパターンもあると思いますので、
プロジェクトによって柔軟に変えていきましょう。
``` sample.vue
<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を取得し、切り替えます。
``` sample.vue
<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を連発してしまった過去の経験を反省しつつ、
この経験を次に活かしていきたい所存です。



[1]:https://v3.ja.vuejs.org/guide/component-basics.html#%E5%8B%95%E7%9A%84%E3%81%AA%E3%82%B3%E3%83%B3%E3%83%9B%E3%82%9A%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88

[2]:https://typescriptbook.jp/reference/builtin-api/map
[3]:https://typescriptbook.jp/reference/values-types-variables/union
[4]:https://v3.ja.vuejs.org/guide/transitions-enterleave.html#%E5%8D%98%E4%B8%80%E8%A6%81%E7%B4%A0-%E3%82%B3%E3%83%B3%E3%83%9B%E3%82%9A%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AE%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B7%E3%82%99%E3%82%B7%E3%83%A7%E3%83%B3