Transition cho enter/leave & list trong Vuejs

Trong bài học trước chúng ta đã tìm hiểu cơ bản về transition trong Vuejs rồi phải không nào. Trong bài này chúng ta sẽ tiếp tục tìm hiểu cụ thể cách áp dụng một transition trong Vuejs. Cùng bắt đầu bài học thôi nào các bạn ey.

Transition khi render lần đầu tiên

Trong Vuejs nếu muốn áp dụng một transition ngay trong lần render đầu tiên của một node, chúng ta có thể dùng thuộc tính appear:

<transition appear>
  <!-- ... -->
</transition>

Theo mặc định các transition được chỉ định khi enter và leave sẽ được sử dụng. Nếu muốn, bạn cũng có thể dùng các class CSS tùy biến:

<transition
  appear
  appear-class="custom-appear-class"
  appear-to-class="custom-appear-to-class" (2.1.8+)
  appear-active-class="custom-appear-active-class"
>
  <!-- ... -->
</transition>

và dùng cả các hook JavaScript tùy biến:

<transition
  appear
  v-on:before-appear="customBeforeAppearHook"
  v-on:appear="customAppearHook"
  v-on:after-appear="customAfterAppearHook"
  v-on:appear-cancelled="customAppearCancelledHook"
>
  <!-- ... -->
</transition>

Transition giữa các phần tử web

Vuejs hỗ trợ transition giữa các phần tử thô (raw element) sử dụng v-if/v-else. Một trong các transition giữa hai phần tử thông dụng nhất là transition giữa một phần tử chứa danh sách (ul, ol, table…) và một thông điệp mô tả một danh sách rỗng:

<transition>
  <table v-if="searchResults.length > 0">
    <!-- ... -->
  </table>
  <p v-else>Không tìm ra kết quả nào.</p>
</transition>

Cách này cho kết quả tốt, thế nhưng có một điểm cần lưu ý sau:

  • Khi kích hoạt giữa các phần tử có cùng tên thẻ, bạn phải cho Vue biết rằng đây là các phần tử khác nhau bằng cách cung cấp các giá trị key duy nhất. Nếu không, trình biên dịch của Vue sẽ chỉ thay đổi nội dung của phần tử để hiệu quả hơn. Ngay cả khi về mặt kĩ thuật là không cần thiết, hãy chắc chắn rằng bạn dùng key cho các item trong một component <transition>.

Ví dụ:

<transition>
  <button v-if="isEditing" key="save">
    Lưu
  </button>
  <button v-else key="edit">
    Sửa
  </button>
</transition>

Trong những trường hợp này, chúng ta cũng có thể dùng thuộc tính key cho việc chuyển tiếp giữa các trạng thái khác nhau trong cùng một phần tử. Thay vì dùng v-if và v-else, ví dụ trên có thể được viết lại như sau:

<transition>
  <button v-bind:key="isEditing">
    Sửa
  </button>
</transition>

Thật ra bạn hoàn toàn có thể chuyển tiếp giữa một số lượng bất kì các phần tử bằng cách dùng nhiều v-if hoặc bind một phần tử đơn lẻ vào một thuộc tính động. Ví dụ:

<transition>
  <button v-if="docState === 'saved'" key="saved">
    Sửa
  </button>
  <button v-if="docState === 'edited'" key="edited">
    Lưu
  </button>
  <button v-if="docState === 'editing'" key="editing">
    Hủy
  </button>
</transition>

Ví dụ trên cũng có thể được viết thành:

<transition>
  <button v-bind:key="docState">
    
  </button>
</transition>
// ...
computed: {   buttonMessage: function () {
     switch (this.docState) {
       case 'saved': return 'Sửa'
       case 'edited': return 'Lưu'
       case 'editing': return 'Hủy' 
    }
   }
 }

Các chế độ transition

Mặc định của <transition> - quá trình enter và leave xảy ra đồng thời. Thỉnh thoảng đây là điều ta muốn, ví dụ khi chuyển tiếp giữa hai phần tử được sắp xếp chồng lên nhau. Tuy nhiên, không phải lúc nào chúng ta cũng muốn transition enter và leave xảy ra đồng thời. Vì thế, Vue cung cấp thêm một số chế độ transition thay thế:

  • in-out: Transition đi vào (in) của phần tử mới xảy ra trước, và sau khi hoàn tất mới đến lượt transition đi ra (out) của phần tử hiện tại.
  • out-in: Transition đi ra (out) của phần tử hiện tại xảy ra trước, và sau khi hoàn tất mới đến lượt transition đi vào (in) của phần tử mới.

Transition giữa các component

Transition giữa các component lại càng đơn giản hơn - chúng ta còn không cần dùng thuộc tính key. Thay vào đó, chúng ta wrap một component động: HTML

<transition name="component-fade" mode="out-in">
  <component v-bind:is="view"></component>
</transition>

VUEJS

new Vue({
   el: '#transition-components-demo',
   data: {     view: 'v-a'   },
   components: { 
    'v-a': {
       template: '<div>Component A</div>'
     },    
    'v-b': { 
       template: '<div>Component B</div>'
     }  
   }
 })

CSS

.component-fade-enter-active, .component-fade-leave-active {
   transition: opacity .3s ease;
 } 
.component-fade-enter, .component-fade-leave-to 
/* .component-fade-leave-active ở các phiên bản trước 2.1.8 */ {
   opacity: 0;
 }

Transition cho danh sách

Phần này chúng ta đã bàn về transition cho:

  • Các node đơn
  • Nhiều node khác nhau nhưng chỉ có một node được render mỗi lúc

Vậy nếu chúng ta có một danh sách chứa các item mà ta muốn render đồng thời, ví dụ với v-for, thì sao? Trong trường hợp này, ta sẽ dùng component <transition-group>. Trước khi xem ví dụ, có một số điều quan trọng mà bạn cần biết về component này:

  • Khác với <transition>, <transition-group> render một phần tử thật sự, mặc định là <span>. Bạn có thể thay đổi kiểu phần tử được render ra bằng thuộc tính tag.
  • Các phần tử bên trong <transition-group> bắt buộc phải có thuộc tính key duy nhất

Transition enter/leave cho danh sách

Bây giờ chúng ta sẽ xem một ví dụ về transition cho enter/leave với cùng các class CSS mà ta đã dùng trên đây: HTML

<div id="list-demo">
  <button v-on:click="add">Thêm</button>
  <button v-on:click="remove">Bớt</button>
  <transition-group name="list" tag="p">
    <span v-for="item in items" v-bind:key="item" class="list-item">
      
    </span>
  </transition-group>
</div>

VUEJS

new Vue({
   el: '#list-demo',
   data: {
     items: [1,2,3,4,5,6,7,8,9],
     nextNum: 10   
   },
   methods: {
     randomIndex: function () {
       return Math.floor(Math.random() * this.items.length)
     },
     add: function () {
       this.items.splice(this.randomIndex(), 0, this.nextNum++)
     },
     remove: function () {
       this.items.splice(this.randomIndex(), 1)
     },
   }
})

CSS

.list-item {
   display: inline-block;   margin-right: 10px; 
} 
.list-enter-active, .list-leave-active {
   transition: all 1s; 
} 
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
   opacity: 0;
   transform: translateY(30px); 
}

Kết quả hiển thị như sau:

Transition dịch chuyển trong danh sách

Component <transition-group> có một tính năng nữa. Không chỉ có thể animate enter và leave, nó còn có thể animate vị trí của các item. Khái niệm mới duy nhất bạn cần biết đến để sử dụng tính năng này là class v-move, được thêm vào khi item thay đổi vị trí. Tương tự như các class khác, prefix của v-mode chính là thuộc tính name, và bạn có thể sử dụng một class khác với thuộc tính move-class. Class này có ích nhất là để chỉ định thời lượng và hàm easing cho transition, như có thể thấy sau đây: HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>
<div id="flip-list-demo" class="demo">
  <button v-on:click="shuffle">Xáo trộn</button>
  <transition-group name="flip-list" tag="ul">
    <li v-for="item in items" v-bind:key="item">
      
    </li>
  </transition-group>
</div>

VUEJS

new Vue({
   el: '#flip-list-demo',
   data: {
     items: [1,2,3,4,5,6,7,8,9]   
   },
   methods: {
     shuffle: function () {
       this.items = _.shuffle(this.items)
     }
   }
})

CSS

.flip-list-move {
   transition: transform 1s;
 }

Hiển thị kết quả như sau:

Một điểm quan trọng cần lưu ý là các transition FLIP này không hoạt động đối với các phần tử có display: inline. Để thay thế, bạn có thể dùng display: inline-block hoặc đặt trong một flexbox. FLIP animation cũng không bị giới hạn chỉ trong một trục. Item trong một grid (lưới) đa chiều cũng có thể được animate:

Transition tuần tự

Bằng cách giao tiếp với các transition JavaScript thông qua các thuộc tính dữ liệu, ta có thể khiến cho các transition trong một danh sách diễn ra một cách tuần tự:

<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<div id="staggered-list-demo">
  <input v-model="query">
  <transition-group
    name="staggered-fade"
    tag="ul"
    v-bind:css="false"
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:leave="leave"
  >
    <li
      v-for="(item, index) in computedList"
      v-bind:key="item.msg"
      v-bind:data-index="index"
    >{{ item.msg }}</li>   </transition-group> </div>
new Vue({
   el: '#staggered-list-demo',
   data: {
     query: '',
     list: [
       { msg: 'Hai Bà Trưng' },
       { msg: 'Ngô Quyền' },
       { msg: 'Đinh Tiên Hoàng' },
       { msg: 'Lý Thường Kiệt' },
       { msg: 'Trần Hưng Đạo' }
      ] 
  },
   computed: {
     computedList: function () {
       var vm = this
       return this.list.filter(function (item) {
         return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1
       })
     }
   },
   methods: {
     beforeEnter: function (el) {
       el.style.opacity = 0
       el.style.height = 0
     },
     enter: function (el, done) {
       var delay = el.dataset.index * 150
       setTimeout(function () {
         Velocity(
           el,
           { opacity: 1, height: '1.6em' },
           { complete: done }
         )
       }, delay)
     },
     leave: function (el, done) { 
       var delay = el.dataset.index * 150
       setTimeout(function () {
         Velocity(
           el,
           { opacity: 0, height: 0 },
           { complete: done }
         )
       }, delay)
     }
   }
 })

Transition tái sử dụng được

Transition có thể được tái sử dụng trong hệ thống component của Vue. Để tạo ra một transition sử dụng lại được, bạn chỉ cần đặt một component <transition> hoặc <transition-group> ở phần tử root, sau đó truyền bất cứ component con nào vào. Đây là một ví dụ sử dụng một component template:

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) {
       // ...
     }
   }
 })

Và component chức năng (functional component) đặc biệt phù hợp cho nhiệm vụ này:

Vue.component('my-special-transition', {
   functional: true,
   render: function (createElement, context) {
     var data = {
       props: {
         name: 'very-special-transition',
         mode: 'out-in'       
       },
       on: {
         beforeEnter: function (el) {
           // ...         
         },
         afterEnter: function (el) {
           // ...
         }
       }
     }
     return createElement('transition', data, context.children)
   }
})

Transition động

Thậm chí ngay cả transition trong Vue cũng hướng dữ liệu! Ví dụ cơ bản nhất của một transition động bind thuộc tính name vào một property động.

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

Điều này có thể có ích khi bạn đã định nghĩa transition/animation CSS với quy chuẩn đặt tên cho class transition của Vue và muốn hoán chuyển giữa các transition/animation này. Tuy nhiên, thực tế thì bất kì thuộc tính transition nào cũng có thể được bind động. Và không chỉ có thuộc tính. Vì thật ra chỉ là các phương thức, các hook cho sự kiện có thể truy xuất đến bất kì dữ liệu nào trong ngữ cảnh hiện tại. Điều này có nghĩa là tùy vào trạng thái của component, các transition JavaScript của bạn có thể có các hành vi rất khác nhau.

<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<div id="dynamic-fade-demo" class="demo">
  Thời gian hiện ra: <input type="range" v-model="fadeInDuration" min="0" v-bind:max="maxFadeDuration">
  Thời gian mờ đi: <input type="range" v-model="fadeOutDuration" min="0" v-bind:max="maxFadeDuration">
  <transition
    v-bind:css="false"
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:leave="leave"
  >
    <p v-if="show">Xin chào</p>
  </transition>
  <button
    v-if="stop"
    v-on:click="stop = false; show = false"
  >Bắt đầu</button>
  <button
    v-else
    v-on:click="stop = true"
  >Dừng lại</button>
</div>
new Vue({
  el: '#dynamic-fade-demo',
  data: {
    show: true,
    fadeInDuration: 1000,
    fadeOutDuration: 1000,
    maxFadeDuration: 1500,
    stop: true
  },
  mounted: function () {
    this.show = false
  },
  methods: {
    beforeEnter: function (el) {
      el.style.opacity = 0
    },
    enter: function (el, done) {
      var vm = this
      Velocity(el,
        { opacity: 1 },
        {
          duration: this.fadeInDuration,
          complete: function () {
            done()
            if (!vm.stop) vm.show = false
          }
        }
      )
    },
    leave: function (el, done) {
      var vm = this
      Velocity(el,
        { opacity: 0 },
        {
          duration: this.fadeOutDuration,
          complete: function () {
            done()
            vm.show = true
          }
        }
      )
    }
  }
})

 

Bình luận