Vue.js (14) - 過場效果及動畫

Vue.js

Vue.js 可以在新增、更新或移除 DOM 時使用 CSS 顯示過場效果及動畫,讓元素或元件產生漸進變化。動畫效果可以自己設定,也可以使用第三方的函式庫。
Vue 提供了對 CSS transition 過場屬性封裝的元件,可以讓元素或元件顯示「進入」和「離開」的過場效果或動畫。

使用方法

Vue 提供 6 個特殊的 class 名稱:
  1. v-enter:元素一開始的狀態。在元素被新增時觸發,在下一個影格立即移除。
  2. v-enter-active:元素被新增時的狀態。在元素被新增前加入,然後在整個動畫中使用它,最後在動畫結時被移除。
  3. v-enter-to:元素新增狀態的結束。在元素被新增後觸發,在 v-enter-active 動畫結束後被移除。這是 v2.1.8 新增的狀態,原有的 v-enter 被它取代。
  4. v-leave:元素被刪除前的初始狀態。在刪除時立即觸發,在下一個影格立即移除。
  5. v-leave-active:元素被刪除時的狀態。在元素被移除前加入,然後在整個動畫中使用它,最後在動畫結束時被移除。
  6. v-leave-to:元素刪除狀態的結束。在元素被刪除後觸發,在 v-leave-active 動畫結束後移除。這是 v2.1.8 新增的狀態,原有的 v-leave 被它取代。
直接看圖比較快: vue transition (圖片來源:https://vuejs.org/v2/guide/transitions.html#Transition-Classes)

簡單的說 Enter 的部分 v-enter 是頭 (0),v-enter-to 是尾 (1),那 v-enter-active 就是從頭到尾 (0~1)。這裡的 0 及 1 是指你所設定的數值,這裡舉例透明度為 0 ~ 1。Leave 的行為和 Enter 相反,當然你可以自行指定,只是通常來說,會讓頭尾相接,形成一個循環。

當你要對某個元素/元件加入過場效果或動畫時,請用 Vue 提供的 <transition name="xxx"> 來包住這個元素,記住!一個 <transition> 只能包含一個根元素/元件,如果有多個元素,必須將它們放在根元素之下,並且 v-ifv-show 只能放在這個根元素或元件上。

其中的 name="xxx" 屬性,它會用來配對 CSS 中以它為開頭的特殊 class 名稱,例如 v-enter 會配對 xxx-enter,也就是說,v 這個前置字會被換成這個 name 的值。

CSS 過場效果

依照前面的使用方法,直接來看範例:
HTML<div id="vm">
  <transition name="fade">
    <div v-show="show">
      <h1>Hello~</h1>
    </div>
  </transition>
</div>

<script>
new Vue({
  el: '#vm',
  data: {
    show: true
  },
  mounted: function () {
    var self = this;
    setInterval(function () {
      self.show = !self.show;
    }, 1000);
  }
});
</script>

<style>
.fade-enter-active, 
.fade-leave-active {
  transition: opacity .8s;
}

.fade-enter, 
.fade-leave-to {
  opacity: 0;
}
</style>
執行結果: vue transition 你也可以使用變形 transform 來做更炫的效果 (程式碼無須更動):
CSS.fade-enter-active, 
.fade-leave-active {
  transition: all .8s ease;
}

.fade-enter {
  transform: translateX(50px);
  opacity: 0;
}

.fade-leave-to {
  transform: translateX(-50px);
  opacity: 0;
}
執行結果: vue transition transform

CSS 動畫

你也可以使用 CSS 動畫:
CSS.bounce-enter-active {
  animation: bounce-in .5s;
}

.bounce-leave-active {
  animation: bounce-in .5s reverse;
}

@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.5);
  }
  100% {
    transform: scale(1);
  }
}
HTML 的部份改成:
<transition name="bounce">
執行結果: vue transition animation

自訂過場 Class

除了 Vue 提供的那 6 個 class ,你也可自定,一樣是 6 個:
  • enter-class
  • enter-active-class
  • enter-to-class (v2.1.8 新增)
  • leave-class
  • leave-active-class
  • leave-to-class (v2.1.8 新增)
這樣一來,你就可以使用第三方函式庫了,例如 Animate.css
HTML<!-- 在 head 中加入 -->
<link href="https://unpkg.com/animate.css@3.5.2/animate.min.css" rel="stylesheet" type="text/css">

<!-- 修改如下 -->
<transition 
    name="my-transition"
    enter-active-class="animated rollIn"
    leave-active-class="animated zoomOutUp">
    ...略
執行結果: vue transition animation.css

在 2 個元素間過場

你可以在使用 v-ifv-else 時同時讓兩個元素產生過場效果:
HTML<div id="vm">
  <transition name="fade">
    <div v-if="show">
      <h1>Hello~</h1>
    </div>
    <p v-else>
      哈囉~
    </p>
  </transition>
</div>
執行結果: vue transition if-else 效果怪怪的,因為兩個元素同時執行過場時,會佔用另一個元素的空間,Vue 提供了解決方法,只要加上 mode 屬性即可,它有 2 個值:
  • in-out:新元素先執行過場進入,完成後目前元素才執行過場離開。
  • out-in:目前元素先執行過場離開,完成後新元素才執行過場進入。這個值比較常用。
修改後如下:
<transition name="fade" mode="out-in">
執行結果: vue transition out-in mode 這裡有一點要注意, v-ifv-else 用在兩個不同的元素上,如前例是可以執行的,但如果用在相同類型的元素上,就必須額外加上 key 屬性讓 Vue 可以區別:
HTML<transition name="fade" mode="out-in">
    <p v-if="show" key="en">
      Hello~
    </p>
    <p v-else key="tw">
      哈囉~
    </p>
</transition>

針對大量元素的過場

前面的例子都是針對單一元素,由 v-ifv-show 來控制的元素,如果要針對由 v-for 產生的大量元素,就要改為使用 <transition-group> 元件。

<transition-group> 元件預設會有一個 <span> 的根元素,它會包住由 v-for 產生的多個元素當成其子元素。你可以改變這個預設的根元素,加上一個 tag 屬性即可,例如:
HTML<transition-group name="fade">
  <p v-for="...略" :key="...略">...略</p>
</transition-group>
瀏覽器會看到:
<span>
  <p></p>
  <p></p>
  ...略
</span>
指定 tag 屬性後:
HTML<transition-group name="fade" tag="div">
  <p v-for="...略" :key="...略">...略</p>
</transition-group>
瀏覽器會看到:
<div>
  <p></p>
  <p></p>
  ...略
</div>
另外,規則和 <transition> 一樣,只要是相同元素就必須提供 key,所以由 v-for 產生的子元素全都要加上 key 屬性。key 屬性的值必須為數字或字串,否則 Vue 會發出警告訊息。
使用範例:
HTML<div id="vm">
  <transition-group name="fade" tag="div">
    <span v-for="n in numbers" :key="n">{{ n }} </span>
  </transition-group>
</div>

<script>
new Vue({
  el: '#vm',
  data: {
    numbers: [1,2,3,4,5],
    next: 6,
    flow: 1
  },
  methods: {
    randomIndex: function () {
      return Math.floor(Math.random() * this.numbers.length)
    },
    insert: function () {
      var r = this.randomIndex();
      this.numbers.splice(r, 0, this.next++);
    },
    remove: function () {
      var r = this.randomIndex();
      this.numbers.splice(r, 1);
      this.next--;
    },
    timer: function () {
      var len = this.numbers.length;
      if (len >= 9) {
        this.flow = -1;
      } else if (len <= 1) {
        this.flow = 1;
      }
      //
      if (this.flow > 0) { 
        this.insert();
      } else {
        this.remove();
      }
    }
  },
  mounted: function () {
    setInterval(this.timer, 1000);
  }
});
</script>

<style>
.fade-enter-active, 
.fade-leave-active {
  transition: all .5s ease;
}

.fade-enter {
  transform: translateX(50px);
  opacity: 0;
}

.fade-leave-to {
  transform: translateX(-50px);
  opacity: 0;
}
</style>
執行結果: vue transition group 你可能有發現效果怪怪的,當元素被加入或移除時,旁邊的其他元素會立即被擠開或被佔住位置,感覺像是瞬間跳到定位而不是漸漸到位。

要解決這個問題,要在子元素中加入針對它的 CSS,修改後如下:
...略
<span v-for="n in numbers" :key="n" class="fade-item">...略
CSS 的部份改成:
CSS.fade-item {
  transition: all .5s;
  display: inline-block;
  margin-right: 5px;
}

.fade-enter-active,
.fade-leave-active {
  transition: all .5s ease;
}

.fade-leave-active {
  position: absolute;
}

.fade-enter {
  transform: translateX(50px);
  opacity: 0;
}

.fade-leave-to {
  transform: translateX(-50px);
  opacity: 0;
}
執行結果: vue transition group smooth 這裡必須注意的是,子元素不能能設定為 display: inline,可以用 display: inline-block 來代替。

建立可重覆使用的過場效果

方法很簡單,就是使用元件,這個元件的根元素就是 <transition><transition-group> ,並使用插槽 <slot> 就可以了,例如:
HTML<div id="vm">
  <list>
    <span v-for="n in numbers" :key="n" class="fade-item">{{ n }} </span>
  </list>
</div>

<script>
Vue.component('list', {
  template: '<transition-group name="fade" tag="div">\
    <slot></slot>\
  </transition-group>'
})

new Vue({...略
</script>
Vue.js (12) - 實戰 CRUD 使用 Laravel + Vue 這一篇來舉例:

1. 建立 List.vue

透過 Vue.js (13) - 建立 Laravel Artisan 指令產生 Vue 元件檔 建立的指令,我們可以快速產生 List.vue,請執行:
php artisan make:vue List --nojs
把前面範例中的內容搬進來:
HTML<template>
  <transition-group name="fade" tag="div">
    <slot></slot>
  </transition-group>
</template>

<script>
export default {
  data() {
    return {
    }
  }
}
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: all .5s ease;
}

.fade-leave-active {
  position: absolute;
}

.fade-enter {
  transform: translateX(50px);
  opacity: 0;
}

.fade-leave-to {
  transform: translateX(-50px);
  opacity: 0;
}
</style>

2. 修改 resources/assets/js/components/Post.vue:

<List id="list">
  <div v-for="post in posts" :key="post.id" class="fade-item">
    ...略
  </div>
</List>
用我們建立的 List 元件把文章清單包起來,記得加上 :key 屬性,接著註冊元件:
<script>
import List from './List';

export default {  
  components: { 
    List
  },
  // ...略
}
</script>
我們原本的做法是在新增、更新及刪除文章時,都直接重載全部的文章,這裡要稍做修改。
app/Http/Controllers/PostController.php
首先,修改 PostController.php ,當新增成功時回傳該筆記錄:
function apiCreatePost(Request $request) {
  // ...略
  return response()->json(['ok' => $ok, 'thepost' => $post], 200);
}
接著回到 Post.vue 繼續修改。
發佈的部分
publish: function () {
  // ...略
  if (response.data['ok']) {
  self.posts.push(response.data['thepost']);
  // self.init();
    // ...略
將原本收到新增成功時重新載入全部文章的動作,改成直接將新增成功的那一筆資料 push 到陣列中。
儲存的部份
save: function () {
  // ...略
  if (response.data['ok']) {
    let i;
    let len = self.posts.length;
    for (i = 0; i < len; i++) {
      if (self.posts[i].id == self.post.id) {
        self.posts[i].title = self.post.title;
        self.posts[i].body = self.post.body;
        break;
      }
    }
    // self.init();
    // ...略
現在不在全部重載,只要更新本地端的資料即可。
刪除的部份
remove: function (id) {
  // ...略
  if (response.data['ok']) {
    let i;
    let len = self.posts.length;
    for (i = 0; i < len; i++) {
      if (self.posts[i].id == id) {
        self.posts.splice(i, 1);
        break;
    }
  }
  // self.init();
}
現在不在全部重載,改為刪除本地資料陣列中的該項目。
CSS 的部份
CSS<style scoped>
.fade-item {
  transition: all .5s ease;
}

.fade-item h1 {
  margin-top: 0;
}

#list {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 240px;
  padding: 20px;
  margin: 20px 0;
  overflow: scroll;
}

#form {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 20px;
  box-shadow: 0 6px 20px 0 #333333;
}
</style>
為了讓過場效果正常,表單現在被固定在下方,文章清單在上方並且當文章過多時顯示捲動列。<h1> 在測試的時候因為有 margin-top 屬性,會影響過場位置的判斷,過場會怪怪的,所以將它的值為改為 0。

這樣就完成了。記得執行自動打包指令 yarn run watch,就能在瀏覽器上看到結果了。
執行結果: vue transition curd app

過場的其他做法

Vue.js 官方還有提供使用 JavaScript 的方式建立的過場效果,更加靈活,但也意謂著更加複雜,完整內容可以在官網 Transition Effects 文件中找到說明。
本文網址:https://blog.tonycube.com/2017/06/vuejs-14-transition-animation.html
Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀

2 則留言

留言小提醒:
1.回覆時間通常在晚上,如果太忙可能要等幾天。
2.請先瀏覽一下其他人的留言,也許有人問過同樣的問題。
3.程式碼請先將它編碼後再貼上。(線上編碼:http://bit.ly/1DL6yog)
4.文字請加上標點符號及斷行,難以閱讀者恕難回覆。
5.感謝您的留言,您的問題也可能幫助到其他有相同問題的人。