5
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-08-16

背景

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を切り替える方法については公式ドキュメントを参照ください。
また、公式のtransitionのページにも記載はされていますが、
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を用いますが、ここは好みの問題と思います。

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

今回は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を連発してしまった過去の経験を反省しつつ、
この経験を次に活かしていきたい所存です。

5
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0