4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-06-09

想定する使い道

たとえば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でも良いですが、それは面倒くさいからやりたくない、という背景です

4
4
0

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
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?