時間差トランジション

トランジションフックを使用することで、リストの要素を表示するタイミングをずらす時間差トランジションが行えます。 このコンテンツは、コードが長すぎて書籍から削ったものなので、若干説明も入っています。

デモ

使用している主な機能

リストトランジション 205ページ

トランジションフック 212ページ

算出プロパティ computed 120ページ

ソースコード

動的なリストを作成

まずは、トランジションを適用する動的なリストを準備しましょう。 リストへの要素の追加と削除、current プロパティの倍数の要素のみを抽出する機能を実装しています。

demo0.vue
<template>
  <div class="example">
    <p>
      <button @click="doAdd">追加</button>
      <button @click="current=1">すべて</button>
      <button @click="current=n" v-for="n in [3,5]" :key="n">
        {{n}}の倍数
      </button>
    </p>
    <transition-group tag="ul" class="list">
      <li v-for="(item, index) in filteredList"
        :data-index="index"
        :key="item"
        class="item"
        @click="doRemove(item)">{{ item }}</li>
    </transition-group>
  </div>
</template>

<script>
export default {
  data() {
    return {
      current: 1,
      list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    }
  },
  computed: {
    filteredList() {
      return this.list.filter(el => el % this.current === 0)
    }
  },
  methods: {
    doAdd() {
      const newNumber = Math.max.apply(null, this.list) + 1
      const index = Math.floor(Math.random() * (this.list.length + 1))
      this.list.splice(index, 0, newNumber)
    },
    doRemove(item) {
      this.list.splice(this.list.indexOf(item), 1)
    }
  }
}
</script>

<style src="./style.css" scoped></style>

描画上の要素のインデックスをトランジションフックから取得できるように<li>タグにdata-index属性を付与しています。

トランジション用の CSS では、「追加されるときは左からフェードイン」「削除されるときは右へフェードアウト」といったスタイルを定義しています。

style.css
/* 以下はボックス要素のスタイル */
.list {
  width: 240px;
  padding: 0;
}
.item {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  margin: 4px;
  width: 40px;
  height: 40px;
  background: #f5f5f5;
}
/* トランジション用スタイル */
.v-enter-active, .v-leave-active, .v-move {
  transition: opacity 1s, transform 1s;
}
.v-leave-active {
  position: absolute;
}
.v-enter {
  opacity: 0;
  transform: translateY(-20px);
}
.v-leave-to {
  opacity: 0;
  transform: translateY(20px);
}

フックでディレイを付与

リストトランジションでは、各要素に対してトランジションフックを行うことができます。 インデックス数値に依存したディレイを付与する関数を定義して、フックしましょう。

<transition-group tag="ul" class="list"
  @before-enter="beforeEnter"
  @after-enter="afterEnter"
  @enter-cancelled="afterEnter">
methods: {
  // ...
  // トランジション開始でインデックス*100ms分のディレイを付与
  beforeEnter(el) {
    el.style.transitionDelay = 100 * parseInt(el.dataset.index, 10) + 'ms'
  },
  // トランジション完了またはキャンセルでディレイを削除
  afterEnter(el) {
    el.style.transitionDelay = ''
  }
}

追加したコードは次のようになります。

demo1.vue
<template>
  <div class="example">
    <p>
      <button @click="doAdd">追加</button>
      <button @click="current=1">すべて</button>
      <button @click="current=n" v-for="n in [3,5]" :key="n">
        {{n}}の倍数
      </button>
    </p>
    <transition-group tag="ul" class="list"
      @before-enter="beforeEnter"
      @after-enter="afterEnter"
      @enter-cancelled="afterEnter">
      <li v-for="(item, index) in filteredList"
        :data-index="index"
        :key="item"
        class="item"
        @click="doRemove(item)">{{ item }}</li>
    </transition-group>
  </div>
</template>

<script>
export default {
  data() {
    return {
      current: 1,
      list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    }
  },
  computed: {
    filteredList() {
      return this.list.filter(el => el % this.current === 0)
    }
  },
  methods: {
    doAdd() {
      const newNumber = Math.max.apply(null, this.list) + 1
      const index = Math.floor(Math.random() * (this.list.length + 1))
      this.list.splice(index, 0, newNumber)
    },
    doRemove(item) {
      this.list.splice(this.list.indexOf(item), 1)
    },
    // トランジション開始でインデックス*100ms分のディレイを付与
    beforeEnter(el) {
      el.style.transitionDelay = 100 * parseInt(el.dataset.index, 10) + 'ms'
    },
    // トランジション完了またはキャンセルでディレイを削除
    afterEnter(el) {
      el.style.transitionDelay = ''
    }
  }
}
</script>

<style src="./style.css" scoped></style>
DEMO

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

要素の絞り込みを行うだけなら、これだけでも十分です。 しかし、このデモを見ると追加した要素がリストの後半になるほど、その分のディレイが適用されてしまっています。

要素を追加するときにディレイを調整

要素を追加するときだけ、ディレイを適用しないように機能を改良してみましょう。

data(){
  return {
    // ...
    // 追加を判断するためのフラグ
    addEnter: false
  }
},
methods: {
  // ...
  doAdd() {
    // 追加ならフラグを立てる
    this.addEnter = true
    const newNumber = Math.max.apply(null, this.list) + 1
    const index = Math.floor(Math.random() * (this.list.length + 1))
    this.list.splice(index, 0, newNumber)
  },
  // ...
  beforeEnter(el) {
    this.$nextTick(() => {
      if (!this.addEnter) {
        // 追加でなければディレイを付ける
        el.style.transitionDelay = 100 * parseInt(el.dataset.index, 10) + 'ms'
      } else {
        // 追加ならフラグを消すだけ
        this.addEnter = false
      }
    })
  }
}

修正したコードは次のようになります。

demo2.vue
<template>
  <div class="example">
    <p>
      <button @click="doAdd">追加</button>
      <button @click="current=1">すべて</button>
      <button @click="current=n" v-for="n in [3,5]" :key="n">
        {{n}}の倍数
      </button>
    </p>
    <transition-group tag="ul" class="list"
      @before-enter="beforeEnter"
      @after-enter="afterEnter"
      @enter-cancelled="afterEnter">
      <li v-for="(item, index) in filteredList"
        :data-index="index"
        :key="item"
        class="item"
        @click="doRemove(item)">{{ item }}</li>
    </transition-group>
  </div>
</template>

<script>
export default {
  data() {
    return {
      addEnter: false,
      current: 1,
      list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    }
  },
  computed: {
    filteredList() {
      return this.list.filter(el => el % this.current === 0)
    }
  },
  methods: {
    doAdd() {
      // 追加ならフラグを立てる
      this.addEnter = true
      const newNumber = Math.max.apply(null, this.list) + 1
      const index = Math.floor(Math.random() * (this.list.length + 1))
      this.list.splice(index, 0, newNumber)
    },
    doRemove(item) {
      this.list.splice(this.list.indexOf(item), 1)
    },
    // トランジション開始でインデックス*100ms分のディレイを付与
    beforeEnter(el) {
      this.$nextTick(() => {
        if (!this.addEnter) {
          // 追加でなければディレイを付与
          el.style.transitionDelay = 100 * parseInt(el.dataset.index, 10) + 'ms'
        } else {
          // 追加ならフラグを消すだけ
          this.addEnter = false
        }
      })
    },
    // トランジション完了またはキャンセルでディレイを削除
    afterEnter(el) {
      el.style.transitionDelay = ''
    }
  }
}
</script>

<style src="./style.css" scoped></style>
DEMO

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

これで新しく追加した時は、ディレイを付けずトランジションするようになりました!

コメント

スクロール量に応じた処理や、アニメーション効果はウェブサイトのデザインに拘るなら欠かせない部分です。 仮想 DOM を通して構築される DOM にアクセスする場合 nextTick をどこで使うかがキモになります。 nextTick の使い方については、CHAPTER3で詳しく説明しています。