---
# System prepended metadata

title: '[ 想入門，我陪你 ] Re Vue 重頭說起｜Day 13：動畫與進退場'
tags: [Alex 宅幹嘛, Vue, 'Re:Vue 重頭說起']

---

# [ 想入門，我陪你 ] Re Vue 重頭說起｜Day 13：動畫與進退場

###### tags: `Vue`、`Re:Vue 重頭說起`、`Alex 宅幹嘛`

### Transitioning Single Elements/Components

#### CSS Transitions

以下四個情況會觸發動畫效果

* Conditional rendering (using **v-if**)
* Conditional display (using **v-show**)
* Dynamic components
* Component root nodes

6個狀態: 進場開始 - 正在進場 - 進場到哪 - 退場開始 - 正在退場 - 退場到哪

![](https://i.imgur.com/KKT8mac.png)

```htmlmixed=
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Day 13</title>
    <style>
    .fade-enter {
        opacity: 0
    }
    
    .fade-enter-active {
        // 一個class只能跑一次 transition
        // transition: all 0.5s 比較不保險
        transition: opacity 0.5s
    }
    
    .fade-enter-to {
        opacity: 1
    }
    
    .fade-leave {
        opacity: 1
    }
    
    .fade-leave-active {
      transition: opacity 0.5s
    }
    
    .fade-leave-to {
        opacity: 0
    }
    </style>
  </head>
  <body>
    <div id="app">
        <!-- transition component 只能包一個 DOM，放兩個以上動畫會壞掉 -->
        <transition name="fade">
            <p v-if="show">Hello World</p>
        </transition>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
        new Vue({
            el: '#app',
            data() {
                return {
                    show: true,
                }
            }
        })
    </script>
  </body>
</html>
```

#### CSS Animation

```htmlmixed=
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Day 13</title>
    <style>
        /* 用 animation 就不用指定開始跟結束 */
        .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);
          }
        }
    </style>
  </head>
  <body>
    <div id="app">
        <!-- transition component 只能包一個 DOM，放兩個以上動畫會壞掉 -->
        <transition name="bounce">
            <p v-if="show">Hello World</p>
        </transition>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
        new Vue({
            el: '#app',
            data() {
                return {
                    show: true,
                }
            }
        })
    </script>
  </body>
</html>
```

#### Custom Transition Classes

概念就是DOM自動幫你加上 Class，適合配上第三方套件的 class (animate.css)

> 不用寫 name !
```htmlmixed=
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Day 13</title>
    <style>
        /* 用 animation 就不用指定開始跟結束 */
        .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);
          }
        }
    </style>
  </head>
  <body>
    <div id="app">
        <!-- transition component 只能包一個 DOM，放兩個以上動畫會壞掉 -->
        <transition
            enter-active-class="__animated__tada"
            leave-active-class="__animated__bounceOutRight"
        >
            <p v-if="show">Hello World</p>
        </transition>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
        new Vue({
            el: '#app',
            data() {
                return {
                    show: true,
                }
            }
        })
    </script>
  </body>
</html>
```

> animate css 版本升級到 4.xx.xx版，所以跟Vue官網上的class name範例不太ㄧ樣

#### Using Transitions and Animations Together

在 Transitions 可以用 [type](https://vuejs.org/v2/api/#transition)

:::info
40:00
**type** - string, Specifies the type of transition events to wait for to determine transition end timing. Available values are "**transition**" and "**animation**". By default, it will automatically detect the type that has a longer duration.
:::

#### Explicit Transition Durations 

=> 自訂動畫時間，Vue 幫你拔掉動畫的 class

> New in 2.2.0+

```htmlmixed=
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Day 13</title>
    <style>
        /* 用 animation 就不用指定開始跟結束 */
        .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);
          }
        }
    </style>
  </head>
  <body>
    <div id="app">
        <!-- transition component 只能包一個 DOM，放兩個以上動畫會壞掉 -->
        <transition name="bounce" :duration="1000">
            <p v-if="show">Hello World</p>
        </transition>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
        new Vue({
            el: '#app',
            data() {
                return {
                    show: true,
                }
            }
        })
    </script>
  </body>
</html>
```

#### JavaScript Hooks


el: 得到正在處理動畫的element，至樣就不用使用 querySelector

done: 在每個階段有 done 這個callback表示完成

加入 Jquery(也可以搭配 Velocity)
![](https://i.imgur.com/zdHZrQO.jpg)


```htmlmixed=
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Day 13</title>
    <style>
        /* 用 animation 就不用指定開始跟結束 */
        .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);
          }
        }
    </style>
  </head>
  <body>
    <div id="app">
        <!-- transition component 只能包一個 DOM，放兩個以上動畫會壞掉 -->
        <transition
            appear
            @before-enter="beforeEnterHandler"
            @after-enter="afterEnterHandler"          
            @enter="enterHandler"
            @leave="leaveHandler" :css="false"
        >
            <p v-if="show">Hello World</p>
        </transition>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
        new Vue({
            el: '#app',
            data() {
                return {
                    show: true,
                }
            },
            methods: {
                beforeEnterHandler(el, done) {
                // 要先做，不但當 fadeIn 時若是 1，動畫會看起來沒出現
                    $(el).css({ opacity: 0 });
                },
                beforeEnterHandler(el, done) {
                    // $(el).removeAttr('style');
                    $(el).css({ opacity: "" });
                },
                enterHandler(el, done) {
                    //$(el).fadeIn(400, done); 
                    $(el).fadeTo(400, 1, done);
                },
                leaveHandler(el, done) {
                    $(el).fadeOut(400, done);
                },
            }
        })
    </script>
  </body>
</html>
```

:::warning
When using JavaScript-only transitions, **the done callbacks are required for the enter and leave hooks**. Otherwise, the hooks will be called synchronously and the transition will finish immediately
:::
> done 若不加上，動畫會立即結束

:::danger
It’s also a good idea to explicitly add v-bind:css="false" for **JavaScript-only** transitions so that Vue can skip the CSS detection. This also **prevents CSS rules** from accidentally interfering with the transition.
:::


因為 transition 的 css 屬性預設是 true，所以當要使用 js hook 做動畫時，要改成 false
[Doc](https://vuejs.org/v2/api/#transition)
:::warning
**css** - boolean, Whether to apply CSS transition classes. Defaults to **true**. If set to **false**, will only trigger JavaScript hooks registered via component events.
:::

> 加上 appear 可以讓網頁loading時，進入畫面也有動畫效果

#### Transitions on Initial Render
一進場，開畫面就想要進場效果(appear)

> 也可以用 v-on:appear 加上自訂的 class

#### Transitioning Between Elements


```htmlmixed=
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Day 13</title>
    <style>
        /* 用 animation 就不用指定開始跟結束 */
        .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);
          }
        }
    </style>
  </head>
  <body>
    <div id="app">
        <!-- transition component 只能包一個 DOM，放兩個以上動畫會壞掉 -->
        <transition
            appear
            @before-enter="beforeEnterHandler"
            @after-enter="afterEnterHandler"          
            @enter="enterHandler"
            @leave="leaveHandler" :css="false"
        >
            <!-- 這樣沒動畫 -->
            <p v-if="show">Hello World</p>
            <p v-if="!show">GoogleByb World</p>
            <!-- 這樣沒動畫 -->
            <p v-if="show">Hello World</p>
            <p v-else>GoogleByb World</p>
            <!-- 這樣有動畫 -->
            <p v-if="show">Hello World</p>
            <div v-else>GoogleByb World</p>
            <!-- 這樣有動畫 -->
            <p v-if="show" key="1">Hello World</p>
            <p v-else key="2">GoogleByb World<p/>
        </transition>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
        new Vue({
            el: '#app',
            data() {
                return {
                    show: true,
                }
            },
            methods: {
                beforeEnterHandler(el, done) {
                // 要先做，不但當 fadeIn 時若是 1，動畫會看起來沒出現
                    $(el).css({ opacity: 0 });
                },
                beforeEnterHandler(el, done) {
                    // $(el).removeAttr('style');
                    $(el).css({ opacity: "" });
                },
                enterHandler(el, done) {
                    //$(el).fadeIn(400, done); 
                    $(el).fadeTo(400, 1, done);
                },
                leaveHandler(el, done) {
                    $(el).fadeOut(400, done);
                },
            }
        })
    </script>
  </body>
</html>
```

> 如果Vue是同一個 tag，他只會換掉Content，所以要額外加 key，這樣DOM才會變換觸發動畫


雖然只有一個 Element，但是 key 不一樣，所以Vue把他當作2個Elment操作，動畫照樣出現

```htmlmixed=
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Day 13</title>
    <style>
        /* 用 animation 就不用指定開始跟結束 */
        .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);
          }
        }
    </style>
  </head>
  <body>
    <div id="app">
        <!-- transition component 只能包一個 DOM，放兩個以上動畫會壞掉 -->
        <transition
            appear
            @before-enter="beforeEnterHandler"
            @after-enter="afterEnterHandler"          
            @enter="enterHandler"
            @leave="leaveHandler" :css="false"
        >
            <p :key="show">Hello World: {{ show }}</p>
        </transition>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
        new Vue({
            el: '#app',
            data() {
                return {
                    show: true,
                }
            },
            methods: {
                beforeEnterHandler(el, done) {
                // 要先做，不但當 fadeIn 時若是 1，動畫會看起來沒出現
                    $(el).css({ opacity: 0 });
                },
                beforeEnterHandler(el, done) {
                    // $(el).removeAttr('style');
                    $(el).css({ opacity: "" });
                },
                enterHandler(el, done) {
                    //$(el).fadeIn(400, done); 
                    $(el).fadeTo(400, 1, done);
                },
                leaveHandler(el, done) {
                    $(el).fadeOut(400, done);
                },
            }
        })
    </script>
  </body>
</html>
```

![](https://i.imgur.com/j79dyr3.jpg)

所以這招可以省Code如下

```htmlmixed=
<transition>
  <button v-if="docState === 'saved'" key="saved">
    Edit
  </button>
  <button v-if="docState === 'edited'" key="edited">
    Save
  </button>
  <button v-if="docState === 'editing'" key="editing">
    Cancel
  </button>
</transition>
```

等同

```htmlmixed=
<transition>
  <button v-bind:key="docState">
    {{ buttonMessage }}
  </button>
</transition>


computed: {
  buttonMessage: function () {
    switch (this.docState) {
      case 'saved': return 'Edit'
      case 'edited': return 'Save'
      case 'editing': return 'Cancel'
    }
  }
}
```

#### Transition Modes

預設效果，兩個同時跑，一個退一進

##### out-in 先出再進 1:18:00
```htmlmixed=
    <transition
        appear
        @before-enter="beforeEnterHandler"
        @after-enter="afterEnterHandler"          
        @enter="enterHandler"
        @leave="leaveHandler"
        :css="false"
        mode="out-in"
    >
        <p :key="show">Hello World: {{ show }}</p>
    </transition>
```

##### in-out 先進再出 1:18:25
```htmlmixed=
    <transition
        appear
        @before-enter="beforeEnterHandler"
        @after-enter="afterEnterHandler"          
        @enter="enterHandler"
        @leave="leaveHandler"
        :css="false"
        mode="in-out"
    >
        <p :key="show">Hello World: {{ show }}</p>
    </transition>
```

### Transitioning Between Components

transition 內放 component，用 is 指定 dynamic component


```htmlmixed=
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Day 13</title>
    <style>
        /* 用 animation 就不用指定開始跟結束 */
        .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);
          }
        }
    </style>
  </head>
  <body>
    <div id="app">
        <!-- transition component 只能包一個 DOM，放兩個以上動畫會壞掉 -->
        <transition
            appear
            @before-enter="beforeEnterHandler"
            @after-enter="afterEnterHandler"          
            @enter="enterHandler"
            @leave="leaveHandler" :css="false"
        >
            <!-- 類似 router 的 <view> -->
            <component v-bind:is="view"></component>
        </transition>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
        new Vue({
            el: '#app',
            data() {
                return {
                    show: true,
                     view: 'v-a'
                }
            },
            methods: {
                beforeEnterHandler(el, done) {
                // 要先做，不但當 fadeIn 時若是 1，動畫會看起來沒出現
                    $(el).css({ opacity: 0 });
                },
                beforeEnterHandler(el, done) {
                    // $(el).removeAttr('style');
                    $(el).css({ opacity: "" });
                },
                enterHandler(el, done) {
                    //$(el).fadeIn(400, done); 
                    $(el).fadeTo(400, 1, done);
                },
                leaveHandler(el, done) {
                    $(el).fadeOut(400, done);
                },
            },

            components: {
                'v-a': {
                  template: '<div>Component A</div>'
                },
                'v-b': {
                  template: '<div>Component B</div>'
                }
              }
            }
        })
    </script>
  </body>
</html>
```

![](https://i.imgur.com/UAl0j7L.png)

![](https://i.imgur.com/dAaP5FP.png)

#### List Transitions

- List 被 for loop 出來，所以是多個DOM，無法用 **<transition>**，要用 **<transition-group>**
- <transition-group> 本身是一個 tag或會產生一個 tag **<span>**


```htmlmixed=
  <transition-group name="list" tag="div">
    <span v-for="item in items" v-bind:key="item" class="list-item">
      {{ item }}
    </span>
  </transition-group>
```

#### List Move Transitions 

針對 List 排序/順序做調整

```htmlmixed=
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Day 13</title>
    <style>
    .fade-enter {
        opacity: 0
    }
    .fade-enter-active {
        transition: opacity 1s;
    }
    .fade-move {
        transition: transform 1s;
    }
    .fade-enter-to {
        opacity: 1
    }
    .fade-leave {
        opacity: 1
    }
    .fade-leave-active {
        transition: opacity 1s;
    }
    .fade-leave-to {
        opacity: 0
    }
    </style>
  </head>
  <body>
    <div id="app">
      <button @click="clickHandler">Toggle</button>
      <transition-group name="fade" tag="div">
        <p v-for="num in items" :key="num">
          {{ num }}
        </p>
      </transition-group>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
        new Vue({
            el: '#app',
            data() {
                return {
                    items: [1,2,3,4,5,6,7,8,9]
                }
            },
            methods: {
               // 打亂排序
                shuffle: function () {
                // _.shuffle(this.items) 這樣寫不會動到原始得資料，所以畫面不變
                  this.items = _.shuffle(this.items)
                },
                clickHandler() {
                    
                },
            },
        })
    </script>
  </body>
</html>
```

> 加上 xxxname-move 的 class 來達成 **移動的特效**

:::danger
可以把上面範例的 p tag 改成 span tag 動畫就會無效，所以要把 span 加上 css inline-block
One important note is that these FLIP transitions do not work with elements set to **display: inline**. As an alternative, you can use **display: inline-block** or place elements in a flex context.
:::

[shuffle](https://lodash.com/docs/2.4.2#shuffle)

#### Staggering List Transitions
1:42:40
#### Reusable Transitions
1:45:11
雖然透過 class 操作動畫已經達到 Reusable 的效果，但是娑要更 reusable 可以包一個客製化的component (slot)

```htmlmixed=
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Day 13</title>
    <style>
    .fade-enter {
        opacity: 0
    }
    .fade-enter-active {
        transition: opacity 1s;
    }
    .fade-move {
        transition: transform 1s;
    }
    .fade-enter-to {
        opacity: 1
    }
    .fade-leave {
        opacity: 1
    }
    .fade-leave-active {
        transition: opacity 1s;
    }
    .fade-leave-to {
        opacity: 0
    }
    </style>
  </head>
  <body>
    <div id="app">
        <my-special-transition>
            <div>
                我是 Child 子元件
            </div>
        </my-special-transition>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
        //  全域載入 Component，使用時就不用每隻檔案要 import
        Vue.component('my-special-transition', {
          template: '\
            <transition\
              name="very-special-transition"\
              mode="out-in"\
              v-on:before-enter="beforeEnter"\
              v-on:after-enter="afterEnter"\
            >\
              <slot></slot>\
            </transition>\
          ',
          methods: {
            beforeEnter: function (el) {
              // ...
            },
            afterEnter: function (el) {
              // ...
            }
          }
        })
        new Vue({
            el: '#app',
            data() {
            },
            methods: {
            },
        })
    </script>
  </body>
</html>
```

> 好處：不用再指定 mode / name /appear給動態模組

> And functional components are especially well-suited to this task
> 
#### Dynamic Transitions
1:49:40
* 
* 常用在輪播範例
* name可以動態綁定

輪播會同時需要跑2種動畫
![](https://i.imgur.com/VgB6nBd.png)

```htmlmixed=
<transition v-bind:name="transitionName">
  <!-- ... -->
</transition>

> 範例請看 Doc
```

### State Transitions (常用)

1:52:40

#### Animating State with 

做狀態的動畫效果，可以用到此法的案例：

* numbers and calculations
* colors displayed
* the positions of SVG nodes
* the sizes and other properties of elements

核心概念，設定兩個數字，一個是原始數字，一個是跑動畫的數字，我們修改原始數字，但是顯示跟跑動是動畫數字

第一個範例用 GSAP 做 數字遞增動畫特效，But 套件很肥 (1:55:20)
第二個範例用 GSAP 做 輸入色票產生色塊，But 套件很肥 (1:57:04)

#### Dynamic State Transitions
SVG 操作 => 自己看

#### Organizing Transitions into Components
官方提供很好的模組作法 2:00:13

```htmlmixed=
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Day 13</title>
  <body>
    <div id="app">
        <animated-integer :value="value"></animated-integer>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <!--  加入  tween.js  -->
    <script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>
    <script>
        // 會動的數字         
        Vue.component('animated-integer', {
          template: '<span>{{ tweeningValue }}</span>',
          props: {
            // 原始資料
            value: {
              type: Number,
              required: true
            }
          },
          data: function () {
            return {
              // 跑動資料
              tweeningValue: 0
            }
          },
          // 當 value 便的時候幫我跑資料
          watch: {
            value: function (newValue, oldValue) {
              this.tween(oldValue, newValue)
            }
          },
          mounted: function () {
            this.tween(0, this.value)
          },
          methods: {
            tween: function (startValue, endValue) {
              var vm = this
              function animate () {
                if (TWEEN.update()) {
                  // requestAnimationFrame的目的是讓 new TWEEN.Tween 跑
                  requestAnimationFrame(animate)
                }
              }

              new TWEEN.Tween({ tweeningValue: startValue })
                .to({ tweeningValue: endValue }, 500)
                .onUpdate(function () {
                  vm.tweeningValue = this.tweeningValue.toFixed(0)
                })
                .start()

              animate()
            }
          }
        })

        new Vue({
            el: '#app',
            data() {
                return {
                    value: 0,
                }
            },
            methods: {
            },
        })
    </script>
  </body>
</html>
```

> 讓你不用寫計時器，動畫交給 tween js


#### Bringing Designs to Life

用 Vue 控制 svg 與 DOM 做動畫，但如果要效能好用canvas