想定する使い道
たとえばVueでカードゲームを実装する時、
- 山札置き場
- 手札
- カード
とCompnentを定義するとします。
そして今「カード」は「山札置き場」の子で、「山札置き場」から「手札」に移動するとします。
この時カードは位置アニメーションして欲しいわけですが、vueのtransitionやtransition groupでは実現できません。
なぜなら親Componentが山札から手札へ変わるから1です。
デモ
具体的にはこういうことです。
四分割された地が親Component、数字が書かれた四角が子Componentで、クリックしたら別の親Componentに移動します。
こちらで実際に試せます
実装
Vueのtransition-groupでも使われているFLIPというアイデアを使っています。
基本は
-
before-leave
で現在の位置を覚えて -
after-enter
で現在の位置 - 直前の位置
だけtranslateし、その差分だけ移動アニメーション
という感じです。
<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>
が無視される為上手くいきません。
そこで全ての子の位置を覚えておいて、前フレームと位置が変わったらアニメーションさせるという泥臭い実装が必要になります。
<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
せっかくなので登録しました。
-
「カード」をフラットに並べて自分で座標管理すれば
transition
でも良いですが、それは面倒くさいからやりたくない、という背景です ↩