- vừa được xem lúc

Tạo sự kiện tùy chỉnh thực sự cho Web Components

0 0 1

Người đăng: Vinh Phạm

Theo Viblo Asia

Khi xây dựng web components, việc giao tiếp giữa các thành phần và ứng dụng cha là vô cùng quan trọng. Trong khi các sự kiện DOM có sẵn như clickchange hoạt động tốt cho những tương tác tiêu chuẩn, sự kiện tùy chỉnh mang đến cho bạn khả năng tạo ra những kiểu sự kiện có ý nghĩa và mang tính ngữ nghĩa, phù hợp với thành phần của bạn.

Trong bài viết này, chúng ta sẽ tìm hiểu cách mở rộng lớp Event của JavaScript để tạo ra các sự kiện tùy chỉnh vừa mạnh mẽ vừa dễ bảo trì.

Vì sao nên mở rộng lớp Event?

Trước khi đi sâu vào cách thực hiện, hãy cùng tìm hiểu lý do bạn muốn mở rộng lớp Event. Việc này cho phép bạn:

  • Tạo sự kiện với các thuộc tính và phương thức tùy chỉnh.
  • Triển khai xử lý sự kiện type-safe (an toàn kiểu dữ liệu).
  • Tạo lớp sự kiện tái sử dụng được trên nhiều thành phần.
  • Thêm các tùy chọn mặc định có ý nghĩa cho sự kiện.

Nếu bạn quyết định rằng việc tạo các lớp con của sự kiện không phải cách tiếp cận mong muốn, bạn vẫn có thể gán kiểu dữ liệu rõ ràng cho sự kiện để cải thiện trải nghiệm lập trình.

Mở rộng sự kiện tùy chỉnh cơ bản

Hãy bắt đầu với một ví dụ đơn giản. Đây là cách bạn có thể tạo một sự kiện tùy chỉnh cho thành phần giỏ hàng:

class CartItemAddedEvent extends Event { // strongly type the event target declare target: ShoppingCart; // define the parameters of the event using the constructor constructor(item, quantity = 1) { // pass the event name and any options to the base Event class super('cart-item-added', { bubbles: true, cancelable: true, composed: true }); // initialize public property values this.item = item; this.quantity = quantity; this.timestamp = new Date(); } /** Get total price for added item */ get totalPrice(): number { return this.item.price * this.quantity; }
} // Usage in a web component
class ShoppingCart extends HTMLElement { addItem(item, quantity) { // Add item to cart logic here... // Dispatch custom event const event = new CartItemAddedEvent(item, quantity); this.dispatchEvent(event); }
}

Tài liệu hóa các sự kiện

Để tài liệu hóa các sự kiện tùy chỉnh này, bạn có thể sử dụng các thẻ JSDoc @event hoặc @fires trong phần tài liệu của lớp thành phần. Trong phần mô tả, bạn có thể cung cấp thêm kiểu dữ liệu. Tất cả thông tin này sẽ được thêm vào Custom Element Manifest, rất hữu ích khi tạo tích hợp và định nghĩa kiểu cho nhiều môi trường.

/** * ... other documentation * * @event {CartItemAddedEvent} cart-item-added - emitted when an item is added to the cart */
class ShoppingCart extends HTMLElement {...}

Đăng ký sự kiện trong Global Event Map

Để tự động suy luận kiểu dữ liệu với addEventListener, bạn đăng ký sự kiện tùy chỉnh trong global event map:

declare global { interface GlobalEventHandlersEventMap { 'cart-item-added': CartItemAddedEvent; }
}

Sự kiện tùy chỉnh nâng cao với xác thực

Đây là ví dụ nâng cao hơn với xác thực và xử lý lỗi:

type ValidationResult = { isValid: boolean; errors: string[];
}; class FormValidationEvent extends Event { // strongly type the event target declare target: CustomForm; constructor( fieldName: string, value: string, validationResult: ValidationResult ) { super('form-validation', { bubbles: true, cancelable: false, composed: true }); this.fieldName = fieldName; this.value = value; this.validationResult = validationResult; this.isValid = validationResult.isValid; this.errors = validationResult.errors || []; } static createSuccess(fieldName, value): FormValidationEvent { return new FormValidationEvent(fieldName, value, { isValid: true, errors: [] }); } static createError(fieldName, value, errors): FormValidationEvent { return new FormValidationEvent(fieldName, value, { isValid: false, errors: Array.isArray(errors) ? errors : [errors] }); } hasError(errorType: 'tooLong' | 'invalidType' | 'required'): boolean { return this.errors.some(error => error.type === errorType); } getErrorMessage(): string { return this.errors.map(error => error.message).join(', '); }
} // Usage in a form component
class CustomForm extends HTMLElement { validateField(fieldName, value) { const validationResult = this.runValidation(fieldName, value); // using static methods to instantiate specific event variations const event = validationResult.isValid ? FormValidationEvent.createSuccess(fieldName, value) : FormValidationEvent.createError(fieldName, value, validationResult.errors); this.dispatchEvent(event); }
}

Tạo hệ thống phân cấp sự kiện

Bạn có thể mở rộng các sự kiện tùy chỉnh để hỗ trợ hệ thống thành phần phức tạp và đặt mặc định ý nghĩa trong lớp cơ sở:

// Base event class
class ComponentEvent extends Event { constructor(type, options = {}) { super(type, { bubbles: true, cancelable: true, composed: true, ...options }); this.timestamp = new Date(); this.componentId = options.componentId || null; }
} // Specific event types
class ComponentLoadedEvent extends ComponentEvent { constructor(componentId, loadTime) { super('component-loaded', { componentId }); this.loadTime = loadTime; }
} class ComponentErrorEvent extends ComponentEvent { constructor(componentId, error) { super('component-error', { componentId }); this.error = error; this.severity = error.severity || 'error'; } get isCritical() { return this.severity === 'critical'; }
}

Ưu và nhược điểm của việc mở rộng Event

Giống như mọi quyết định kỹ thuật khác, việc chọn tạo lớp sự kiện tùy chỉnh hay chỉ dùng Event hoặc CustomEvent đều có những đánh đổi:

Ưu điểm

✅ An toàn kiểu dữ liệu và IntelliSense

  • Hỗ trợ hoàn hảo kiểm tra kiểu và gợi ý trong TypeScript/IDE hiện đại.

✅ Tái sử dụng

  • Có thể import và dùng lại trên nhiều thành phần, đảm bảo nhất quán.

✅ Logic tùy chỉnh

  • Dễ dàng thêm logic thay vì phải lặp lại trong từng thành phần.

✅ API phong phú

  • Các phương thức và getter giúp API rõ ràng, dễ dùng.

✅ Dễ debug

  • Tên lớp sự kiện rõ ràng xuất hiện trong stack trace.

✅ Kiểm tra instance

  • Có thể kiểm tra loại sự kiện qua instanceof:
someDiv.addEventListener('my-event', e => { if (e instanceof MyCustomEvent) { // logic for handling the event }
});

Nhược điểm

❌ Tăng kích thước bundle

  • Mỗi lớp sự kiện sẽ góp phần làm bundle JavaScript lớn hơn (dù thường không đáng kể).

❌ Độ phức tạp cao hơn

  • Cần tìm hiểu hệ thống sự kiện tùy chỉnh.

❌ Thuộc tính lẫn lộn

  • Thuộc tính/method của bạn nằm chung với thuộc tính tiêu chuẩn của Event, đôi khi kém rõ ràng.

❌ Dễ over-engineering

  • Rất dễ tạo hệ thống sự kiện quá phức tạp mà không mang lại giá trị thực.

❌ Tương thích trình duyệt

  • Các trình duyệt cũ có thể không hỗ trợ tốt (nhưng không phải vấn đề lớn với web component hiện đại).

❌ Tốn bộ nhớ hơn

  • Tạo nhiều instance có thể tốn RAM hơn so với object literals (thường không đáng kể).

❌ Khó tạo tiện ích trừu tượng

  • Nếu bạn muốn tạo phương thức emit tái sử dụng, sẽ khó hơn vì cần khởi tạo class sự kiện cụ thể. Ví dụ một cách tiếp cận phổ biến:
class BaseComponent extends HTMLElement { emit<T = unknown>(name: string, detail?: T) { this.dispatchEvent(new CustomEvent<T>(name, { detail, bubbles: true, composed: true })); }
} type CustomClickDetails = { message: string;
}; class CustomButton extends BaseComponent { constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { const button = document.createElement('button'); button.textContent = 'Click Me'; // emit a custom event when the button is clicked button.addEventListener('click', () => { this.emit<CustomClickDetails>('custom-click', { message: 'Button was clicked!' }); }); this.shadowRoot?.appendChild(button); // emit a custom event when the component has loaded this.emit('loaded'); }
}

Những điều cần lưu ý

✅ Giữ cho mọi thứ đơn giản.

  • Chỉ tạo sự kiện tùy chỉnh nếu chúng thực sự cần thiết.

✅ Thêm dữ liệu liên quan.

  • Cung cấp các thuộc tính mà người nghe sự kiện cần.

✅ Không lặp lại thuộc tính/method có thể lấy từ target.

✅ Đặt mặc định hợp lý.

✅ Luôn tài liệu hóa rõ ràng: Khi nào sự kiện được phát? Dữ liệu gồm những gì?

Kết luận

Mở rộng lớp Event trong JavaScript là một kỹ thuật mạnh mẽ để tạo hệ thống sự kiện giàu tính ngữ nghĩa trong web components. Mặc dù có thêm độ phức tạp, lợi ích về an toàn kiểu dữ liệu, tái sử dụng và API rõ ràng có thể vượt trội so với chi phí, đặc biệt trong ứng dụng quy mô lớn.

Hãy sử dụng kỹ thuật này một cách thận trọng – không phải tương tác nào cũng cần lớp sự kiện tùy chỉnh. Tuy nhiên, khi bạn cần dữ liệu và hành vi sự kiện phức tạp, mở rộng Event mang đến giải pháp sạch sẽ và dễ bảo trì.

Nếu bạn thấy việc mở rộng Event phù hợp, hãy bắt đầu với những lớp đơn giản và phát triển dần hệ thống sự kiện khi thư viện thành phần của bạn lớn hơn.

Cảm ơn các bạn đã theo dõi!

Bình luận

Bài viết tương tự

- vừa được xem lúc

The Twelve-Factor App, cẩm nang gối đầu giường trong xây dựng application (Phần 1)

Giới thiệu. Ngày nay các phần mềm được triển khai dưới dạng các dịch vụ, chúng được gọi là các web apps hay software-as-a-service (SaaS).

0 0 46

- vừa được xem lúc

8 Sai lầm phổ biến khi lập trình Android

1. Hard code.

0 0 207

- vừa được xem lúc

Popular interview question: What is the difference between Process and Thread? 10 seconds a day

Video được đăng tại channel Tips Javascript

0 0 44

- vừa được xem lúc

Thuật toán và ứng dụng - P1

Mục đích series. . Những bài toán gắn liền với thực tế. Từ đó thấy được tầm quan trọng của thuật toán trong lập trình.

0 0 47

- vừa được xem lúc

Tác dụng của Docker trong quá trình học tập

Docker bây giờ gần như là kiến thức bắt buộc đối với các anh em Dev và Devops, nhưng mà đối với sinh viên IT nói chung vẫn còn khá mơ hồ và không biết tác dụng thực tế của nó. Hôm nay mình sẽ chia sẻ

0 0 53

- vừa được xem lúc

Làm giàu trong ngành IT

Hầu như mọi người đều đi làm để kiếm tiền, ít người đi làm vì thấy cái nghề đó thú vị lắm. Bây giờ vất cho mình 100 tỷ bảo mình bỏ nghề thì mình cũng bỏ thôi.

0 0 57