# 09. CSS 與 jQuery 動畫 (transition)

# 摘要

  1. 切換 DOM 而非資料來改變畫面:使用 v-forv-if 設定要顯示的 DOM 元素。
  2. 使用 CSS 製作過場動畫:使用 <transition> 和 class 改變樣式。
  3. 使用 jQuery 製作過場動畫:使用 <transition> 和 hooks 觸發事件。

# 切換 DOM 而非資料來改變畫面

在第四單元的事件綁定,點擊按鈕時,中間的文字會改變,是因為內容在 Vue 的 v-htmlv-text 的控制下,這些內容能夠隨著資料的改變而被替換。

但如果在切換時要有過場動畫,使用 v-htmlv-text 替換資料無法達成,需要 DOM 的顯示跟隱藏才能做出效果。

原本這部分的 HTML 結構如下:

<div class="menuItem white" >
  <span class="number">{{ index + 1 }}</span>
  <span class="type">{{ today.type }}</span>
  <a :href="today.link" class="title">{{ today.title }}</a>
</div>

為了能在 DOM 上面加上動畫,要先用 v-for 把所有的資料都放上來,然後設定 v-if 讓條件符合者顯示,未符合者註解掉。

# 在 v-for 綁上 key 屬性

此外,Vue 在使用相同標籤元素的過場切換時,要使用 key 屬性,讓 Vue 可以辨識、區分他們,否則 Vue 只會替換其內部屬性而不會觸發過場效果。

參考:为什么使用v-for时必须添加唯一的key? (opens new window)

<div class="menuItem white" v-for="(item,i) in menu" v-if="index == i" :key="item.title">
  <span class="number">{{ i + 1 }}</span>
  <span class="type">{{ item.type }}</span>
  <a :href="item.link" class="title">{{ item.title }}</a>
</div>

這時候再用開發人員工具去看,會發現 menu 裡的所有資料都列了出來,只是大部分被註解掉。點擊按鈕時,不再是去改裡面的資料,而是將原本顯示的標籤註解掉,然後取消要顯示的資料的註解。

處理好之後,過場動畫的製作可以透過兩種方式:

  1. CSS
  2. JavaScript 函式庫 (這裡使用 jQuery)

# 使用 CSS 製作過場動畫

# transition 組件

在 Vue 裡面有提供一個叫 <transition> 的組件 (標籤),可以將被包起來的元素加上動畫效果。

首先,在要加上過場效果的地方用 <transition> 包起來。

<transition>
  <div class="menuItem white" v-for="(item,i) in menu" v-if="index == i" :key="item.title">
    <span class="number">{{ i + 1 }}</span>
    <span class="type">{{ item.type }}</span>
    <a :href="item.link" class="title">{{ item.title }}</a>
  </div>
</transition>

# 表示元件不同狀態的六個過場 class

然後,在 CSS 使用 Vue 制定的 class 寫下自己要的過場效果:

.fade-enter,
.fade-leave-to {
  opacity: 0;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 1s;
}

在元件進入/離開的過程中,Vue 制定了六個 class 來表示元件進入/離開的不同狀態:

  • v-enter
  • v-enter-active
  • v-enter-to
  • v-leave
  • v-leave-active
  • v-leave-to

這六個 class 的關係如下圖:

(圖片來源及詳細說明:Vue - 进入/离开 & 列表过渡 (opens new window))

# name 屬性

這些過場用的 class 前面的 v- 是預設前綴,用於沒有名字的 <transition>

如果要自定義動畫的名稱,在使用 <transition> 的時候,要加上 name 屬性。例如 <transition name="fade"> 。相對的,原本的 v-enter 也要替換為 fade-enter

<transition name="fade">
  <div class="menuItem white" v-for="(item,i) in menu" v-if="index == i" :key="item.title">
    <span class="number">{{ i + 1 }}</span>
    <span class="type">{{ item.type }}</span>
    <a :href="item.link" class="title">{{ item.title }}</a>
  </div>
</transition>

# mode 屬性

這時候去操作畫面看效果,會看到切換時畫面上同時出現兩筆資料,但這不是我們想要的,我們希望螢幕上一次只會出現一筆資料。

解決方式是在 <transition> 裡面設定 mode="out-in",讓上一筆資料先離開,下一筆資料才會出現。

  • mode="out-in": 先出再進,這樣畫面上只會有一筆資料。
  • mode="in-out": 先進再出,這樣畫面上還是會有兩筆資料。

# appear 屬性

如果希望載入網頁時,初始渲染的第一筆資料也能有過場效果,則在 <transition> 裡面加上 appear 屬性。

<transition name="fade" mode="out-in" appear>
  <div class="menuItem white" v-for="(item,i) in menu" v-if="index == i" :key="item.title">
    <span class="number">{{ i + 1 }}</span>
    <span class="type">{{ item.type }}</span>
    <a :href="item.link" class="title">{{ item.title }}</a>
  </div>
</transition>

# 使用 jQuery 製作過場動畫

先載入 jQuery,然後將原本 <transition> 裡面的 name 屬性移除 (因為沒有用到 CSS)。

# JavaScript Hooks

用程式寫動畫的原理是事件,Vue 設計了幾組事件,使用 hooks 的概念來觸發對應的函式。

<transition
  v-on:before-enter="beforeEnter"
  v-on:enter="enter"
  v-on:after-enter="afterEnter"
  v-on:enter-cancelled="enterCancelled"

  v-on:before-leave="beforeLeave"
  v-on:leave="leave"
  v-on:after-leave="afterLeave"
  v-on:leave-cancelled="leaveCancelled"
>
  <!-- ... -->
</transition>

這些事件的概念和 CSS 的六個過場 class 類似。

回到 HTML,先在 <transition> 綁定這些事件和會觸發的對應函式:

<transition 
  @before-enter="beforeEnter" 
  @enter="enter" 
  @after-enter="afterEnter" 
  @enter-cancelled="afterEnter" 
  @before-leave="beforeLeave" 
  @leave="leave" 
  @after-leave="afterLeave" 
  @leave-cancelled="afterLeave" 
  mode="out-in" 
  appear
>
  <div class="menuItem white" v-for="(item,i) in menu" v-if="index == i" :key="item.title">
    <span class="number">{{ i + 1 }}</span>
    <span class="type">{{ item.type }}</span>
    <a :href="item.link" class="title">{{ item.title }}</a>
  </div>
</transition>

然後到 Vue 的 methods 去寫函式:

methods: {
    changeIndex(index) {
      this.index = (this.index + index + this.total) % this.total
    },
    beforeEnter(el){
      $(el).css({opacity: 0})
    },
    enter(el,done){
      $(el).animate({opacity: 1}, 1000, done)
    },
    afterEnter(el){
      $(el).css({opacity: ''})
    },
    beforeLeave(el){
      $(el).css({opacity: 1})
    },
    leave(el,done){
      $(el).animate({opacity: 0}, 1000, done)
    },
    afterLeave(el){
      $(el).css({opacity: ''})
    }
}

說明:

  1. 在框架中,盡量避免手動讀取 DOM。可使用 el 參數來接收傳入的 DOM 物件。
  2. enterleave 的函式中,要使用 done 告訴程式這個動畫什麼時候完成。否則會被同步調用,過場會立即完成。
  3. 如果沒使用 afterEnter 清除樣式,那麼在 enter 過程中加上去的 opacity: 1 會殘留在那個元素裡,可能會造成意料之外的影響。

# v-bind:css="false"

用程式做動畫時,可能會和 CSS 的部分有衝突。為了避免受到 CSS 的影響,建議在 <transition> 加上 v-bind:css="false",以程式動畫為主。

<transition 
  @before-enter="beforeEnter" 
  @enter="enter" 
  @after-enter="afterEnter" 
  @enter-cancelled="afterEnter" 
  @before-leave="beforeLeave" 
  @leave="leave" 
  @after-leave="afterLeave" 
  @leave-cancelled="afterLeave" 
  :css="false"     // 避免程式的動畫效果受到 CSS 的影響
  mode="out-in" 
  appear
>
  <div class="menuItem white" v-for="(item,i) in menu" v-if="index == i" :key="item.title">
    <span class="number">{{ i + 1 }}</span>
    <span class="type">{{ item.type }}</span>
    <a :href="item.link" class="title">{{ item.title }}</a>
  </div>
</transition>