Custom Element với Shadow DOM

- 8 mins

Lần trước mình đã viết về Shadow DOM nên để đầy đủ hơn mình sẽ giới thiệu về Custom Element và cách phối hợp với Shadow DOM.

Custom Element

Custom Element là phương pháp giúp các nhà phát triển front-end (front-end developers) có thể tạo ra các element mới (new HTML tags) hoặc mở rộng/kế thừa (extends) các element cũ. Kế thừa element ở đây có thể là từ một Custom Element hoặc một Native Element. Vào thời điểm mình viết bài này, việc kế thừa một Native Element vãn chưa được các browser hỗ trợ nên mình chỉ nói về cách tạo element thôi.

Cách tạo một Element mới

Element mới nghĩa là một element có tên (tag name) do chính chúng ta định ra hay còn là “Creating an autonomous custom element”. Chữ “autonomous” nghĩa là “tự trị” vì element này không chỉ hỗ trợ việc hiển thị nội dung mà còn chứa cả các phương thức vận hành bên trong (instance methods).

Ở đây ta cần lưu ý là Custom Element hoạt động không cần include bất kỳ một thư viện hay các framework Javascript nào cả. Các lib/framework front-end hiện tại có thể làm được điều tương tự nhưng nó là Virtual DOM chứ không phải một Custom Element.

Với Custom Element ta có thể tạo một Element mới như sau (Custom Element v1):

// Javascript, ES2015
class MyCountdown extends HTMLElement{
  // ...
}

customElements.define('my-countdown', MyCountdown)
<!-- HTML -->
<my-countdown></my-countdown>

Trong ví dụ trên, ta có thể định nghĩa một Element mới thông qua việc kế thừa một HTMLElement. Hàm define trong API customElements sẽ “đăng ký” cho trình duyệt hiểu my-countdown là cái gì.

Sở dĩ mình nói “đăng ký” là do Custom Element v0 sử dụng hàm document.registerElement. Trong bài này mình sẽ dùng luôn v1 cho tiện.

Quy tắc đặt tên (tag name)

Mặc dù nói là một Element do chúng ta tự định nghĩa ra, nhưng cách đặt tag name của nó cũng có một số nguyên tắc như sau:

Như vậy my-countdown trong ví dụ của mình là hợp lệ. Để đơn giản các bạn chỉ cần nhớ tag name phải có “-” và viết thường. Ngoài ra chúng ta có thể đặt các unicode emoji như icon-:heart_eyes:.

Khi sử dụng trong HTML, một Custom Element không thể dùng dưới dạng thể tự đóng (<my-countdown />) và phải luôn là một cặp mở đóng (<my-countdown></my-countdown>).

Cơ chế Upgrade

Bình thường HTML rất dễ dãi. Điều đó có nghĩa là nó có thể tiếp nhận bất kỳ một element nào dù nó không có trong list element chuẩn. Các trình duyệt sẽ xem các element lạ này như một div bình thường và không báo lỗi gì cả.

<my-countdown>I am a custom element. Waiting for upgrading...</my-countdown>
// Kết quả vẫn là: I am a custom element. Waiting for upgrading...

Nhưng tới khi trình duyệt nhận được thông tin đăng ký của element (document.registerElement) thì element sẽ được parse lại, quá trình này gọi là upgrade element.

customElements.define('my-countdown', class MyCountdown extends HTMLElement{
  connectedCallback() {
    this.innerHTML = '<em>I am upgraded</em>'
  }
})

Bây giờ ta sẽ nhận được kết quả là “I am upgraded”. Việc này cũng khá dễ hiểu vì trong môi trường thực tế, ta thường tách phần Javascript thành các file được load từ bên ngoài. Việc này giúp cho trình duyệt nếu chưa kịp nhận thông tin element thì vẫn không bị lỗi.

Ở đây có một điểm thú vị đó là khi một Element khi chưa được upgrade, dựa vào quy cách đặt tên chúng sẽ phân làm 2 loại:

// Đây thực sự là một unknown element vì tên không hợp lệ
document.createElement('myelement') instanceof HTMLUnknownElement === true // true

// Đây là undefined element, trình duyệt hiểu là nó sẽ được upgrade
document.createElement('my-element') instanceof HTMLUnknownElement === true // false

Việc upgrade element chỉ diễn ra với các element đang có trong cây DOM (đã được insert vào document). Một element khi đã được upgraded thì chúng không thể downgrade. Và đương nhiên là chúng chỉ được define một lần duy nhất.

Các lifecycle methods

Bản chất khi khai báo một Custom Element chúng ta sẽ dùng cú pháp class của ES5. Vì thế hàm dựng dưới đây là hoàn toàn hợp lệ:

class MyCountdown extends HTMLElement{
  constructor() {
    super() // hàm này bắt buộc phải gọi đầu tiên trong constructor
    // TODO: set up listeners, init variables
  }
}

Ngoài ra chúng còn có một số callback function hay lifecycle methods như sau:

Method Gọi khi
connectedCallback Được gọi khi element được insert vào DOM. Ở đây chúng ta có thể đặt gọi các phương thức load resource cho việc render element.
disconnectedCallback Được gọi khi element bị removed khỏi DOM. Chúng ta có thể dừng các tác vụ hoặc remove các listeners/events ra khỏi element.
attributeChangedCallback(attrName, oldVal, newVal) Được gọi khi có một attribute mới được thêm vào/xoá đi trên element hoặc giá trị của attribute thay đổi. Lưu ý là chỉ các attribute có trong observedAttributes mới nhận được.
adoptedCallback Được gọi khi element bị di chuyển (move) tới một document mới (vd như document.adoptNode(el)).

Cơ chế lắng nghe thay đổi trên Attribute

Một Element sẽ có tag name và các attributes nếu có. Custom Element cũng không ngoại lệ. Các attribute chính là phương thức để khỏi tạo các giá trị cho Element hoặc là cầu nối cho sự thay đổi giá trị giữa bên ngoài và bên trong Element. Để làm được điều này chúng ta sẽ sử dụng thêm hàm static observedAttributes:

// JS
class MyCountdown extends HTMLElement{

  static get observedAttributes() {
    return ['number'];
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    // bây giờ mỗi khi attribute number thay đổi giá trị thì hàm này sẽ gọi
  }
}
<!-- HTML -->
<my-countdown number=30></my-countdown>

Ví dụ một Custom Element hoàn chỉnh

See the Pen dVOWxX by Viet Tran (@viettranx) on CodePen.

Trong ví dụ trên my-countdown là một Custom Element có một timer gọi đến hàm onTick sau mỗi 1000ms để giảm giá trị của number. Để tiện minh hoạ mình đã map việc tăng/giảm number vào attribute number. Attribute number có đăng ký lắng nghe nên hàm attributeChangedCallback được gọi, sau đó mình chỉ việc gọi hàm render là xong.

Vì là một element “tự trị” nên button “set to 50” không thể trỏ thẳng vào thay đổi biến this.number của my-countdown được, mà phải thông qua attribute number.

Phối hợp với Shadow DOM

OK, tới đây chúng ta đã biết cách tạo và sử dụng Custom Element rồi. Tuy nhiên phần nội dung sau khi element được upgrade sẽ không được “bảo vệ”, các CSS và JS sẽ có thể tác động từ bên ngoài vì cơ bản chúng chỉ là những DOM bình thường được render ra thêm mà thôi. Và đây chính là “đất diễn” của Shadow DOM.

Từ đó my-countdown element của mình sẽ được viết lại như sau:

See the Pen Create a custom element with Shadow DOM by Viet Tran (@viettranx) on CodePen.

Kiểm tra lại với Chrome

custom-element-shadow-dom

Kết

Việc sử dụng Custom Element sẽ giúp chúng ta định nghĩa được các element mới hay còn được gọi là component mới. Kết hợp với Shadow DOM ta sẽ có một component “đao thương bất nhập” vì tính chất Isolated mà ta đã tìm hiểu từ bài trước Giới thiệu Shadow DOM - Tương lai của Front End.

Custom Element API cũng chính là nền tảng của Web Component và thư viện Polymer nhằm giúp build những component có tính tái sử dụng và hiệu năng cao, vì chúng được coi là Native Component.

Tham khảo

Một số tài liệu về custom element

Viet Tran

Viet Tran

A Man who is developing apps with Red Bull

comments powered by Disqus
rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora