Transition cho State (trạng thái) trong Vue.js

Trong bài trước chúng ta đã tìm hiểu về transition và animate trong Vue aps dùng với enter/leave,list thế nhưng để animate  và transition cho State (trạng thái), cho dữ liệu thì sao? Cùng theo dõi bài học dưới đây để tìm câu trả lời các bạn nhé!

Animate cho dữ liệu

Xét các ví dụ sau:

  • các con số và phép tính
  • màu sắc được hiển thị
  • vị trí của các node SVG
  • kích cỡ và các thuộc tính khác của các phần tử web

Có thể thấy đuộc tất cả những thông tin này đều hoặc đã được lưu trữ sẵn dưới dạng số liệu, hoặc có thể được chuyển đổi thành số liệu. Một khi làm vậy, chúng ta có thể animate những thay đổi trạng thái này bằng cách dùng những thư viện bên thứ ba, kết hợp với hệ thống phản ứng (reactivity) và component của Vue.

Animate cho trạng thái bằng watcher

Watcher cho phép chúng ta animate các thay đổi từ bất kì thuộc tính dạng số nào sang một thuộc tính khác. Điều này nói một cách trừu tượng thì nghe có vẻ phức tạp, vì thế chúng ta hãy xem ví dụ với Greensock sau

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>

<div id="animated-number-demo">
  <input v-model.number="number" type="number" step="20">
  <p>{{ animatedNumber }}</p>
</div>
new Vue({
  el: '#animated-number-demo',
  data: {
    number: 0,
    tweenedNumber: 0
  },
  computed: {
    animatedNumber: function() {
      return this.tweenedNumber.toFixed(0);
    }
  },
  watch: {
    number: function(newValue) {
      TweenLite.to(this.$data, 0.5, { tweenedNumber: newValue });
    }
  }
})

Kết quả như sau:

Khi bạn cập nhật con số trong input trên đây, thay đổi sẽ được animate. Ví dụ này khá ổn, nhưng nếu dữ liệu không phải là một con số trực tiếp mà là một cái gì đó khác, ví dụ như màu CSS thì sao? Dưới đây là một cách thực hiện điều này khi tích hợp với Tween.js và Color.js:

HTML

<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>
<script src="https://cdn.jsdelivr.net/npm/color-js@1.0.3"></script>
<div id="example-7">
  <input
    v-model="colorQuery"
    v-on:keyup.enter="updateColor"
    placeholder="Nhập vào một màu"
  >
  <p>
    <span
      v-bind:style="{ backgroundColor: tweenedCSSColor }"
      class="example-7-color-preview"
    ></span>
  </p>
  <p>{{ tweenedCSSColor }}</p>
</div>

VUEJS

var Color = net.brehaut.Color

new Vue({
  el: '#example-7',
  data: {
    colorQuery: '',
    color: {
      red: 0,
      green: 0,
      blue: 0,
      alpha: 1
    },
    tweenedColor: {}
  },
  created: function () {
    this.tweenedColor = Object.assign({}, this.color)
  },
  watch: {
    color: function () {
      function animate () {
        if (TWEEN.update()) {
          requestAnimationFrame(animate)
        }
      }


      new TWEEN.Tween(this.tweenedColor)
        .to(this.color, 750)
        .start()


      animate()
    }
  },
  computed: {
    tweenedCSSColor: function () {
      return new Color({
        red: this.tweenedColor.red,
        green: this.tweenedColor.green,
        blue: this.tweenedColor.blue,
        alpha: this.tweenedColor.alpha
      }).toCSS()
    }
  },
  methods: {
    updateColor: function () {
      this.color = new Color(this.colorQuery).toRGB()
      this.colorQuery = ''
    }
  }
})

CSS

.example-7-color-preview {
  display: inline-block;
  width: 50px;
  height: 50px;
}

Kiểm tra kết quả như sau:

Transition động cho trạng thái

Cũng giống như component transition của Vue, transition cho dữ liệu cũng có thể được cập nhật trong thời gian thực (real time), và việc này đặc biệt hữu ích khi tạo các prototype (bản thử nghiệm, khuôn mẫu). Ngay cả khi sử dụng một đa giác SVG đơn giản, bạn cũng có thể đạt được nhiều hiệu ứng mà nếu không sử dụng transition bạn có thể sẽ phải tốn kha khá thời gian thử đi thử lại mới tưởng tượng ra được.

Xem ví dụ dưới đây:

Code hoàn thiện của ví dụ trên như sau:

HTML

<div id="app">
  <svg width="200" height="200">
    <polygon :points="points"></polygon>
    <circle cx="100" cy="100" r="90"></circle>
  </svg>
  <label>Sides: {{ sides }}</label>
  <input 
    type="range" 
    min="3" 
    max="500" 
    v-model.number="sides"
  >
  <label>Minimum Radius: {{ minRadius }}%</label>
  <input 
    type="range" 
    min="0" 
    max="90" 
    v-model.number="minRadius"
  >
  <label>Update Interval: {{ updateInterval }} milliseconds</label>
  <input 
    type="range" 
    min="10" 
    max="2000"
    v-model.number="updateInterval"
  >
</div>

VUEJS

new Vue({
  el: '#app',
  data: function () {
      var defaultSides = 10
      var stats = Array.apply(null, { length: defaultSides })
        .map(function () { return 100 })
      return {
        stats: stats,
        points: generatePoints(stats),
      sides: defaultSides,
      minRadius: 50,
      interval: null,
      updateInterval: 500
    }
  },
  watch: {
      sides: function (newSides, oldSides) {
        var sidesDifference = newSides - oldSides
      if (sidesDifference > 0) {
          for (var i = 1; i <= sidesDifference; i++) {
            this.stats.push(this.newRandomValue())
        }
      } else {
        var absoluteSidesDifference = Math.abs(sidesDifference)
          for (var i = 1; i <= absoluteSidesDifference; i++) {
            this.stats.shift()
        }
      }
    },
    stats: function (newStats) {
            TweenLite.to(
          this.$data, 
        this.updateInterval / 1000, 
        { points: generatePoints(newStats) }
        )
    },
    updateInterval: function () {
        this.resetInterval()
    }
  },
  mounted: function () {
      this.resetInterval()
  },
  methods: {
    randomizeStats: function () {
        var vm = this
        this.stats = this.stats.map(function () {
          return vm.newRandomValue()
      })
    },
    newRandomValue: function () {
        return Math.ceil(this.minRadius + Math.random() * (100 - this.minRadius))
    },
    resetInterval: function () {
        var vm = this
        clearInterval(this.interval)
      this.randomizeStats()
        this.interval = setInterval(function () { 
          vm.randomizeStats()
      }, this.updateInterval)
    }
  }
})


function valueToPoint (value, index, total) {
  var x     = 0
  var y     = -value * 0.9
  var angle = Math.PI * 2 / total * index
  var cos   = Math.cos(angle)
  var sin   = Math.sin(angle)
  var tx    = x * cos - y * sin + 100
  var ty    = x * sin + y * cos + 100
  return { x: tx, y: ty }
}


function generatePoints (stats) {
    var total = stats.length
    return stats.map(function (stat, index) {
    var point = valueToPoint(stat, index, total)
    return point.x + ',' + point.y
  }).join(' ')
}

CSS

svg { display: block; }
polygon { fill: #41B883; }
circle {
  fill: transparent;
  stroke: #35495E;
}
input[type="range"] {
  display: block;
  width: 100%;
  margin-bottom: 15px;
}
          

Sắp xếp transtion vào component

Quản lí nhiều transition cho trạng thái có thể làm độ phức tạp của một đối tượng hoặc component Vue tăng lên một cách nhanh chóng. Rất may mắn  nhiều animation có thể được trích xuất ra thành các component con chuyên dụng. Chúng ta hãy thử làm chuyện này với ví dụ animate số nguyên ở trên:

HTML

<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>
<div id="example-8">
  <input v-model.number="firstNumber" type="number" step="20"> +
  <input v-model.number="secondNumber" type="number" step="20"> =
  {{ result }}
  <p>
    <animated-integer v-bind:value="firstNumber"></animated-integer> +
    <animated-integer v-bind:value="secondNumber"></animated-integer> =
    <animated-integer v-bind:value="result"></animated-integer>
  </p>
</div>

VUEJS

// Giờ thì logic tween phức tạp này có thể được dùng lại giữa
// hai số nguyên bất kì nào mà chúng ta muốn animte trong ứng dụng.
// Component cũng cung cấp một giao diện rõ ràng để thiết lập
// thêm nhiều transition động cũng như các kĩ thuật transition
// phức tạp.
Vue.component('animated-integer', {
  template: '<span>{{ tweeningValue }}</span>',
  props: {
    value: {
      type: Number,
      required: true
    }
  },
  data: function () {
    return {
      tweeningValue: 0
    }
  },
  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(animate)
        }
      }


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


      animate()
    }
  }
})


// Đối tượng Vue chính bây giờ không còn gì phức tạp cả!
new Vue({
  el: '#example-8',
  data: {
    firstNumber: 20,
    secondNumber: 40
  },
  computed: {
    result: function () {
      return this.firstNumber + this.secondNumber
    }
  }
})

Kết quả như sau:

Bên trong các component child, chúng ta có thể kết hợp bất kì kĩ thuật transition nào đã được bàn đến  với các kĩ thuật được hệ thống transition có sẵn của Vue cung cấp. Với hai công cụ này, thật sự những điều chúng ta không làm được là rất ít. Chúc các bạn thành công!

Bình luận