iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

0
自我挑戰組

跟 VueJS 認識的30天系列 第 17

[DAY17]跟 Vue.js 認識的30天 - Vue 過渡(轉場)及動畫效果下篇(`<transition-group>`) - 多個元素的過渡及列表過渡

這一篇就來接續寫完 Vue 過渡及動畫的內容啦!希望能順利寫完我的 Vue 筆記。

多個元素的切換轉場

以後就用 .v-*.transitionName-* 取代 .*-enter.*-enter-active.*-enter-to 等等。

先來看一下簡單的切換範例

<button @click="toggle=!toggle">toggle</button>
<transition name="toggleTransition">
  <div v-if="toggle">我是A</div>
  <div v-else>我是B</div>
</transition>

<style>
.toggleTransition-enter,
.toggleTransition-leave-to {
  opacity: 0;
}
.toggleTransition-enter-active,
.toggleTransition-leave-active {
  transition: all 1s;
}
.toggleTransition-enter-to,
.toggleTransition-leave {
  opacity: 1;
}
</style>

在上面這個範例中,會發現沒辦法產生出轉場效果,如同之前提到過的 Vue 為了渲染效能的問題,會在切換相同元素標籤的情況下僅替換該元素標籤內部的內容,也因此在上方的例子中, Vue 在切換時僅會切換標籤內的文字,而非整個元素 <div> ,而因為這些 class .v-*.transitionName-* 都是加在元素上的,而非文字,所以在只有文字被重新渲染的狀況下是不會有轉場效果的。

詳細的元素複用可以參考 DAY06 | 跟 Vue.js 認識的30天 - Vue 的條件渲染(v-ifv-show)

要怎麼讓切換是有動畫效果的呢?

第一種 - 加入 key 屬性

<button @click="toggle=!toggle">toggle</button>
<transition name="toggleTransition">
  <div v-if="toggle" key="A">我是A</div>
  <div v-else key="B">我是B</div>
</transition>

第二種 - 不同標籤

<button @click="toggle=!toggle">toggle</button>
<transition name="toggleTransition">
  <div v-if="toggle">我是A</div>
  <p v-else>我是B</p>
</transition>

上面 2 種方法都是同一個目的就是讓 Vue 知道這 2 個標籤是不同的,所以在切換時,要從元素那就替換掉,而非僅替換文字。

另外在文件中也提及了利用屬性 key 結合 v-bind 或是 computed 來切換元素,道理跟上面是相同的,就是利用屬性 key 的改變來形成不同的元素並得到動畫效果。

<transition name="toggleTransition">
  <!--利用 toggle 的布林值來形成 save 跟 edit 2種不同的元素-->
  <button :key="toggle">{{toggle ?'save' : 'edit'}}</button>
</transition>

過渡(轉場)模式

在上面的範例中會發現元素在切換的過程中,會有一個正在消失,另一個慢慢出現的過程,也因此有一段時間是 2 個元素同時存在的狀況,所以有可能造成了破版的情況。

這個就可以透過加入模式 mode 來解決, Vue 提供了 2 種模式可供選擇:

  • mode='in-out' :新元素先進入,完成後舊元素在離開。

  • mode='out-in' :舊元素先離開,完成後新元素在進入。

同樣使用上方例子,但來達成舊元素先離開,新元素在進入的效果(mode='out-in')。

<button @click="toggle=!toggle">toggle</button>
<transition name="toggleTransition" mode='out-in'>
  <div v-if="toggle" key="A">我是A</div>
  <div v-else key="B">我是B</div>
</transition>

模組過渡的方法很簡單,就是在動態組件外邊加入 <transition> 標籤並加入 .v-*.transition-* 等 class 樣式即可。

想了解動態組件可以先參考 DAY15 | 跟 Vue.js 認識的30天 - Vue 動態模組(Dynamic Components)

列表過渡(轉場)

先來知道文件中所說的,如何同時渲染整個列表,這時候要使用的是 <transition-group>

<transition-group><transition> 有幾點不同及要注意的地方:

  • <transition-group> 在渲染成 DOM 是會產生出一個 <span> 元素的,但可以透過屬性 tag 去改變這個元素。

  • 不能使用轉場模式的屬性(mode)。

  • 內部元素需要提供屬性 key

  • .v-*.transition-* 等 class 樣式是加在作用的那個元素上。

先來看看下面基礎的列表轉場範例 - 新增或移除數字

<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
  <span v-for="num in numGroup" v-bind:key="num" class="list-item">
    {{ num }}
  </span>
</transition-group>
<style>
.list-enter, .list-leave-to {
  opacity: 0;
}
.list-enter-active, .list-leave-active {
  transition: all 1s;
}
.list-enter-to, .list-leave {
  opacity: 1;
}
</style>
<script>
const vm = new Vue({
  el: "#vm",
  data: {
    numGroup: [1, 2, 3, 4, 5, 6],
    nextNum: 7
  },
  methods: {
    add() {
      this.numGroup.splice(
        _.random(0, this.numGroup.length-1),
        0,
        this.nextNum++ // i++ 會先回傳原本的值,之後才將值設定為+1
      );
    },
    remove() {
      this.numGroup.splice(
        _.random(0, this.numGroup.length-1),
        1);
    }
  }
});
</script>

這時候會發現再新增或移除過後,新數字陣列會瞬間移動到新位置,而沒有動畫感。如果要讓新數字陣列緩慢的移到新位置,那麼可以透過 class .v-move.transitionName-move 來設定移動速度。

<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
  <span v-for="num in numGroup" v-bind:key="num">
    {{ num }}
  </span>
</transition-group>
<style>
.list-enter, .list-leave-to {
  opacity: 0;
}
.list-enter-active, .list-leave-active {
  transition: all 1s;
}
.list-enter-to, .list-leave {
  opacity: 1;
}
.list-move {
  transition: all 3s;
}
</style>
<script>
const vm = new Vue({
  // 同上
});
</script>

使用了 .v-move.transitionName-move ,卻發現沒有任何變化,在 Vue 文件中有提到過:

.v-move.transitionName-move 是用FLIP達成動畫位移效果的。

需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline 。作为替代方案,可以设置为 display: inline-block 或者放置于 flex 中。

又來一個問題,那為什麼不能使用 display: inline 呢?先來看看FLIP文件內容:

https://ithelp.ithome.com.tw/upload/images/20210128/201275532UhgDr5VFC.png

簡單掃過會發現,FLIP有使用到 transform ,但是 display: inline 是無法使用 transform 效果的(可以參考Terminology - transformable element),也因此如果要使用 .v-move.transitionName-move 的元素就不能是 display: inline

也因此針對上方範例有 2 種解決辦法:

  • 改變 <transition-group> 內的標籤,不要使用 <span> 等行內元素。

  • 改變 <transition-group> 內的標籤屬性值為 display: inline-blockdisplay: block

<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
<!-- 改成 p 或者改變 CSS 屬性值-->
  <span v-for="num in numGroup" v-bind:key="num">
    {{ num }}
  </span>
</transition-group>
<style>
span {
  display: inline-block;
}
.list-enter, .list-leave-to {
  opacity: 0;
}
.list-enter-active, .list-leave-active {
  transition: all 1s;
}
.list-enter-to, .list-leave {
  opacity: 1;
}
.list-move {
  transition: all 3s;
}
</style>

變更元素或是加入 display: inline-block 後,會發現在加入數字時會有緩慢移動的動畫出現,但是在移除數字時還是不能出現這個效果,在 Vue 文件的範例中有提供一個方法來讓移除數字時能有動畫出現,那就是在 .list-leave-active 中加入 position:absolute 就可以解決了。

就查詢網上資料,如 stackoverflowGitHub,我認為是在移除的過程中(不加入 position:absolute )因為該移除元素(稱元素 A )仍然是有佔位的,直到動畫消失這個元素才會被真正移除,但在移除數字動畫開始之初,就會先透過 FLIP 計算元素 A 後面的元素需要移動多少位置,以便在這些後面元素中加入 .v-move ,但因為元素 A 是佔位的,所以 FLIP 會認為這些後面元素是不需移動的,所以並不會為這些元素加入 .v-move

https://ithelp.ithome.com.tw/upload/images/20210128/20127553MkxFxqB6cd.png

https://ithelp.ithome.com.tw/upload/images/20210128/20127553J6CTzLMX0l.png

為了證實想法,是否加入 position:absolute 就是為了讓元素 A 成為不佔位的元素,以便讓 FLIP 替後邊元素計算位移距離,所以利用另一個屬性 float 來試試,發現也是成功的,結論就是只要找到方法讓該被移除元素在被移除之初就成為不佔位的元素即可讓 FLIP 動畫成功。

動態過渡(轉場)

這裡利用 v-bind:name 來做一個切換動畫效果的範例。

<button v-on:click="show=!show">toggle</button>
<button v-on:click="changeTransition">改變動畫</button>
<p>{{ transitionSwiper }}</p>
<transition :name="transitionSwiper">
  <p v-if="show">Hello</p>
</transition>
<style>
/* animateA */
.animateA-enter, .animateA-leave-to {
  opacity: 0;
}
.animateA-enter-active, .animateA-leave-active{
  transition: all 3s;
}
.animateA-enter-to, .animateA-leave {
  opacity: 1;
}
/* animateB */
.animateB-enter, .animateB-leave-to {
  font-size: 13px;
}
.animateB-enter-active, .animateB-leave-active{
  transition: all 3s;
}
.animateB-enter-to, .animateB-leave {
  font-size: 30px;
}
</style>
<script>
const vm = new Vue({
  el: "#vm",
  data: {
    show: false,
    transitionSwiper: "animateA"
  },
  methods: {
    changeTransition(){
      this.transitionSwiper=this.transitionSwiper=='animateA' ? 'animateB' : 'animateA'
    }
  }
});
</script>

當按下改變動畫的按鈕後, <transition> 的屬性 name 就會改變,也因此動畫的樣式也會隨著改變,如上方範例,原本是套用 animateA 的動畫效果,經過改變後會套用 animateB 的效果。

Demo:DAY17 | 跟 Vue.js 認識的30天 - Vue 過渡及動畫效果下篇 - 多個元素的過渡及列表過渡

參考資料:

Vue.js - 进入/离开 & 列表过渡

stackoverflow

GitHub


上一篇
[DAY16]跟 Vue.js 認識的30天 - Vue 過渡(轉場)及動畫效果上篇(`<transition>`)
下一篇
[DAY18]跟 Vue.js 認識的30天 - Vue 混入(`mixin`)
系列文
跟 VueJS 認識的30天21

尚未有邦友留言

立即登入留言