Edited at

Vueで親コンポーネントを跨いだTransitionを実現する

More than 1 year has passed since last update.


想定する使い道

たとえばVueでカードゲームを実装する時、


  • 山札置き場

  • 手札

  • カード

とCompnentを定義するとします。

そして今「カード」は「山札置き場」の子で、「山札置き場」から「手札」に移動するとします。

この時カードは位置アニメーションして欲しいわけですが、vueのtransitiontransition groupでは実現できません。

なぜなら親Componentが山札から手札へ変わるから1です。


デモ

具体的にはこういうことです。

四分割された地が親Component、数字が書かれた四角が子Componentで、クリックしたら別の親Componentに移動します。

こちらで実際に試せます

vue-parent-change-transition.gif


実装

Vueのtransition-groupでも使われているFLIPというアイデアを使っています。

基本は

- before-leaveで現在の位置を覚えて

- after-enter現在の位置 - 直前の位置だけtranslateし、その差分だけ移動アニメーション

という感じです。


parent-change-transition.vue

<template>

<transition v-on:before-enter="beforeEnter" v-on:after-enter="afterEnter" v-on:before-leave="beforeLeave">
<slot/>
</transition>
</template>

<script>
//...

export default {
name: "parent-change-transition",
methods: {
beforeLeave (el) {
previousPosition[this.childId] = el.getBoundingClientRect()
},
beforeEnter (el) {
el.hidden = true
},
afterEnter (el) {
el.hidden = false
if (!previousPosition[this.childId]) return
// 現在位置と直前位置の差だけtranslateし、tweenさせる
}
}
}
</script>


しかしこれだけだとアニメーションするのは対象の子だけです。

つまり対象の子が移動した後、残された他の子が一瞬で移動してしまう為、何が起きたの分かりにくくなってしまいます。

最初はこの部分にだけリストトランジションを使えば良いのでは?と思いましたが、親に<transition-group>を使うと子の<transition>が無視される為上手くいきません。

そこで全ての子の位置を覚えておいて、前フレームと位置が変わったらアニメーションさせるという泥臭い実装が必要になります。


parent-change-transition.vue

<template>

<transition v-on:before-enter="beforeEnter" v-on:after-enter="afterEnter" v-on:before-leave="beforeLeave">
<slot/>
</transition>
</template>

<script>
let previousPosition = {}

export default {
name: "parent-change-transition",
props: {
idPropertyName: { type:String, default: 'id' },
duration: { type:Number, default: 300 }
},
data () {
return { intervalIndex: null }
},
mounted () {
this.startPositionInspection()
},
computed: {
childId () {
return this[[this.idPropertyName]]
}
},
methods: {
startPositionInspection () {
let p = this.$el.getBoundingClientRect()
this.intervalIndex = setInterval(() => {
let current = this.$el.getBoundingClientRect()
if (current.x === p.x && current.y === p.y) return
this.move(p, current)
}, 10)
},
move (previous, current) {
clearInterval(this.intervalIndex)
this.$el.animate([
{ transform: `translate(${previous.x - current.x}px, ${previous.y - current.y}px)` },
{ transform: 'translate(0, 0)' }
], { duration: this.duration }).addEventListener('finish', () => this.startPositionInspection())
},
beforeLeave (el) {
previousPosition[this.childId] = el.getBoundingClientRect()
},
beforeEnter (el) {
el.hidden = true
},
afterEnter (el) {
el.hidden = false
if (!previousPosition[this.childId]) return
this.move(previousPosition[this.childId], el.getBoundingClientRect())
}
}
}
</script>


使用例のコードなど: https://github.com/inamori/vue-parent-change-transition


npm

https://www.npmjs.com/package/vue-parent-change-transition

せっかくなので登録しました。





  1. 「カード」をフラットに並べて自分で座標管理すればtransitionでも良いですが、それは面倒くさいからやりたくない、という背景です