Làm Chủ Động Tác Chuyển Tiếp Trong Vue 3: Phân Tích Chi Tiết Transition Và TransitionGroup

1. Cơ chế hoạt động của Component Transition

Component Transition là một công cụ tích hợp sẵn trong hệ sinh thái Vue 3, chuyên dùng để xử lý các hiệu ứng hình học khi phần tử DOM được thêm vào hoặc loại bỏ khỏi cây tài liệu. Bạn không cần thực hiện quy trình đăng ký toàn cục mà có thể sử dụng nó ngay lập tức. Điểm lưu ý quan trọng là thành phần này chỉ chấp nhận một nút gốc duy nhất bên trong default slot.

Khi trạng thái hiển thị của phần tử bị thay đổi, framework sẽ tự động giám sát và quản lý vòng đời của hiệu ứng:

  • Nhận diện xem có áp dụng CSS class liên quan hay không.
  • Kiểm tra sự hiện diện của các hook JavaScript.
  • Mặc định sẽ chờ một khung hình tiếp theo trước khi thao tác nếu không phát hiện yếu tố nào ở trên.

Các trường hợp kích hoạt quá trình chuyển tiếp bao gồm việc thay đổi điều kiện v-if, v-show, hoặc cập nhật thuộc tính key trên phần tử con.

Hệ thống lớp tên (Class Names)

Để kiểm soát hành vi của động tác, Vue cung cấp một bộ sáu lớp tên mặc định đóng vai trò như các điểm neo thời gian. Những class này sẽ tự động được gắn vào phần tử gốc tại các mốc thời gian cụ thể:

  1. Giai đoạn xuất hiện: Sử dụng v-enter-from cho trạng thái ban đầu, v-enter-active cho quá trình diễn ra (nơi bạn đặt duration và easing), và v-enter-to cho trạng thái cuối cùng.
  2. Giai đoạn biến mất: Tương tự, ta có v-leave-from, v-leave-active và v-leave-to.

Bạn có thể tùy chỉnh tiền tố của các class này bằng cách gán giá trị cho thuộc tính name. Ví dụ, nếu đặt name="morph", các class sẽ chuyển từ tiền tố v- sang morph-. Nếu cần thiết, bạn cũng có thể hoàn toàn tự định nghĩa tên class thông qua các props riêng biệt như enter-active-class để tích hợp thư viện bên thứ ba.

<Transition :name="effectType">
  <div :class="containerClass" :style="{ display: isVisible ? 'block' : 'none' }">
    Nội dung động
  </div>
</Transition>

Kết hợp CSS Transition và Animation

Phương thức phổ biến nhất là khai báo thuộc tính transition trong CSS ở giai đoạn active. Điều này cho phép duyệt mượt mà giữa hai trạng thái opactiy hoặc transform. Tuy nhiên, nếu bạn sử dụng @keyframes (animation), cần lưu ý rằng sự kiện kết thúc sẽ khác biệt.

Trong trường hợp muốn đồng thời sử dụng cả transition và animation, Vue cần biết rõ nên lắng nghe sự kiện nào để xác định động tác đã xong. Lúc này, thuộc tính type trở nên bắt buộc để phân biệt giữa 'animation' hoặc 'transition'. Ngoài ra, nếu định dạng CSS nhắm vào phần tử hậu duệ (child element) thay vì phần tử trực tiếp, bạn cần khai báo rõ ràng thời gian sống của hiệu ứng thông qua prop :duration để tránh việc DOM bị xóa sớm hơn dự kiến.

Can thiệp bằng JavaScript Hooks

Về mặt lập trình, bạn có thể nắm quyền kiểm soát chi tiết hơn thông qua các callback events. Khi đó, việc tắt cơ chế tự động phát hiện CSS (:css="false") giúp tăng hiệu suất nếu bạn viết logic thuần JS.

Cảnh báo: Khi sử dụng pure JS mode, bạn bắt buộc phải gọi hàm callback done() thủ công trong các hook @enter và @leave. Nếu quên điều này, Vue sẽ đợi vô hạn dẫn đến lỗi treo giao diện.
<script setup lang="ts">
import { ref } from 'vue';

const isVisible = ref(false);

// Kích hoạt hiệu ứng vào đầu vào
const startEntrance = (element: HTMLElement, finishCallback: () => void) => {
  // Thiết lập trạng thái ban đầu bằng JS nếu cần
  const requestStep = window.requestAnimationFrame(() => {
    finishCallback();
  });
};

// Kết thúc hiệu ứng
const endEntrance = () => {
  console.log('Đã hoàn tất nhập cảnh');
};
</script>

<template>
  <Transition :css="false" @enter="startEntrance" @after-enter="endEntrance">
    <div v-if="isVisible" class="card">Thông tin cá nhân</div>
  </Transition>
</template>

Tùy chọn nâng cao

  • Hiệu ứng lúc đầu: Thêm prop appear để áp dụng hiệu ứng ngay cả khi trang vừa load lần đầu.
  • Trật tự ưu tiên: Sử dụng mode="out-in" để đảm bảo phần tử cũ rời đi hoàn toàn trước khi phần tử mới xuất hiện, tránh xung đột chồng chéo layout.
  • Key độc nhất: Thay đổi giá trị để ép Vue coi đó là một phần tử mới, từ đó kích hoạt lại chu kỳ chuyển tiếp ngay cả khi nội dung text thay đổi.

2. Xử lý danh sách với TransitionGroup

Khác với Transition chỉ xử lý đơn thể, component TransitionGroup được thiết kế để xử lý mảng dữ liệu lặp lại qua v-for. Nó hỗ trợ các hiệu ứng chèn, xóa và di chuyển vị trí của các mục trong danh sách một cách liền mạch.

Thiết lập thẻ chứa (Container)

Theo mặc định, nhóm này không tạo ra một thẻ wrapper bao ngoài. Để đảm bảo ngữ pháp HTML đúng chuẩn (ví dụ nhóm các thẻ li phải nằm trong ul), hãy sử dụng prop tag:

<TransitionGroup name="list-flow" tag="ul" id="task-list">
  <li v-for="item in taskList" :key="item.id" class="task-item">
    {{ item.title }}
  </li>
</TransitionGroup>

Hiệu ứng dịch chuyển (Move Class)

Đây là điểm nổi bật nhất của Group. Khi vị trí của các phần tử thay đổi do việc thêm/xóa, class v-move (hoặc name-move nếu đã đặt tên) sẽ được áp dụng lên các phần tử đang di chuyển. Để đạt hiệu quả tốt nhất, các phần tử sắp bị xóa nên được đặt position: absolute nhằm giải phóng chỗ trống trong luồng tài liệu, giúp các phần tử còn lại trượt填补 khoảng trống tự nhiên.

Dưới đây là ví dụ hoàn chỉnh về quản lý danh sách sản phẩm với hiệu ứng mượt mà:

<template>
  <div class="product-container">
    <button @click="addItem()">Thêm sản phẩm</button>
    <button @click="removeItem()">Xóa sản phẩm</button>

    <TransitionGroup name="fly-in" tag="div" class="grid-layout">
      <div 
        v-for="product in products" 
        :key="product.productId" 
        class="product-card"
      >
        <h3>{{ product.name }}</h3>
        <p>Giá: {{ product.price }} VNĐ</p>
      </div>
    </TransitionGroup>
  </div>
</template>

<script setup>
import { ref } from 'vue';

// Dữ liệu mẫu
const products = ref([
  { productId: 101, name: 'Laptop', price: 20000000 },
  { productId: 102, name: 'Chuột', price: 350000 },
  { productId: 103, name: 'Bàn phím', price: 1200000 }
]);

const addItem = () => {
  const count = products.value.length;
  products.value.unshift({ 
    productId: Date.now(), 
    name: 'Mô hình mới', 
    price: 0 
  });
};

const removeItem = () => {
  if (products.value.length > 0) {
    products.value.pop();
  }
};
</script>

<style scoped>
.product-card {
  background: white;
  padding: 15px;
  margin: 10px;
  border-radius: 8px;
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

/* Cấu hình chuyển động */
.fly-in-move,
.fly-in-enter-active,
.fly-in-leave-active {
  transition: transform 0.4s ease, opacity 0.4s ease;
}

/* Trạng thái khi mới xuất hiện */
.fly-in-enter-from {
  opacity: 0;
  transform: scale(0.8) translateY(20px);
}

/* Trạng thái khi đang rời đi - cần absolute để không làm vỡ layout */
.fly-in-leave-active {
  position: absolute;
  width: 100%; /* Giữ nguyên kích thước bề rộng */
}
</style>

Thẻ: Vue3 css-animation javascript-hooks transition-component ui-interaction

Đăng vào ngày 18 tháng 10 lúc 03:38