Tại Cafedev, chúng tôi cung cấp hướng dẫn chi tiết từ cơ bản đến nâng cao về JavaScript, và hôm nay chúng ta sẽ khám phá cách kết hợp Shadow DOM với các sự kiện. Shadow DOM cho phép chúng ta encapsulate các chi tiết nội bộ của thành phần, trong khi sự kiện giúp chúng ta tương tác và xử lý các hành động của người dùng. Hãy cùng tìm hiểu cách sử dụng các sự kiện trong Shadow DOM để nâng cao khả năng phát triển ứng dụng web của bạn!

Ý tưởng phía sau cây Shadow là bao bọc các chi tiết triển khai nội bộ của một thành phần.
Giả sử một sự kiện nhấp chuột xảy ra bên trong shadow DOM của thành phần <user-card>. Nhưng các kịch bản trong tài liệu chính không biết về các chi tiết nội bộ của shadow DOM, đặc biệt nếu thành phần đó đến từ thư viện bên thứ ba.

Vì vậy, để giữ các chi tiết được bao bọc, trình duyệt chuyển tiếp sự kiện.

Các sự kiện xảy ra trong shadow DOM có phần tử host là mục tiêu, khi bị bắt ngoài thành phần.

Đây là một ví dụ đơn giản:


<user-card></user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `<p>
      <button>Click me</button>
    </p>`;
    this.shadowRoot.firstElementChild.onclick =
      e => alert("Inner target: " + e.target.tagName);
  }
});

document.onclick =
  e => alert("Outer target: " + e.target.tagName);
</script>

Nếu bạn nhấp vào nút, các thông điệp là:
1. Mục tiêu bên trong: BUTTON — trình xử lý sự kiện nội bộ nhận đúng mục tiêu, phần tử bên trong shadow DOM.
2. Mục tiêu bên ngoài: USER-CARD — trình xử lý sự kiện tài liệu nhận shadow host làm mục tiêu.

Việc chuyển tiếp sự kiện là một điều tuyệt vời, vì tài liệu ngoài không cần phải biết về các chi tiết nội bộ của thành phần. Từ góc nhìn của nó, sự kiện xảy ra trên .

Chuyển tiếp không xảy ra nếu sự kiện xảy ra trên một phần tử được slot, mà về mặt vật lý sống trong light DOM.

Ví dụ, nếu người dùng nhấp vào trong ví dụ dưới đây, mục tiêu của sự kiện chính là phần tử span này, cho cả trình xử lý shadow và light:


<user-card id="userCard">
*!*
  <span slot="username">Cafedev</span>
*/!*
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `<div>
      <b>Name:</b> <slot name="username"></slot>
    </div>`;

    this.shadowRoot.firstElementChild.onclick =
      e => alert("Inner target: " + e.target.tagName);
  }
});

userCard.onclick = e => alert(`Outer target: ${e.target.tagName}`);
</script>

Nếu một cú nhấp chuột xảy ra trên "Cafedev", thì cho cả trình xử lý bên trong và bên ngoài, mục tiêu là <span slot="username">. Đó là một phần tử từ light DOM, vì vậy không có sự chuyển tiếp.
Ngược lại, nếu cú nhấp chuột xảy ra trên một phần tử bắt nguồn từ shadow DOM, ví dụ như trên <b>Name</b>, thì khi nó nổi bọt ra khỏi shadow DOM, event.target của nó được đặt lại thành <user-card>.

1. Nổi bọt(bubbling), event.composedPath()

Để mục đích sự kiện bubbling, DOM phẳng được sử dụng.
Vì vậy, nếu chúng ta có một phần tử được slot, và một sự kiện xảy ra ở đâu đó bên trong nó, thì nó nổi bọt lên đến và tiếp tục lên trên.

Đường dẫn đầy đủ đến mục tiêu sự kiện gốc, với tất cả các phần tử shadow, có thể được lấy bằng cách sử dụng event.composedPath(). Như chúng ta có thể thấy từ tên của phương pháp, đường dẫn đó được lấy sau khi phân phối.

Trong ví dụ trên, DOM phẳng là:

<user-card id="userCard">
  #shadow-root
    <div>
      <b>Name:</b>
      <slot name="username">
        <span slot="username">John Smith</span>
      </slot>
    </div>
</user-card>

Vì vậy, đối với một cú nhấp chuột trên <span slot="username">, một cuộc gọi đến event.composedPath() trả về một mảng: [span, slot, div, shadow-root, user-card, body, html, document, window]. Đó chính xác là chuỗi cha từ phần tử mục tiêu trong DOM phẳng, sau khi phân phối.

Nếu shadow tree được tạo ra với {mode: 'closed'}, thì đường dẫn phân phối bắt đầu từ host: user-card và lên trên.

Đó là nguyên tắc tương tự như các phương pháp khác hoạt động với shadow DOM. Các chi tiết nội bộ của các cây đóng hoàn toàn bị ẩn.

2. event.composed

Hầu hết các sự kiện đều nổi bọt qua ranh giới shadow DOM. Có một số sự kiện không làm được điều đó.
Điều này được điều chỉnh bởi thuộc tính composed của đối tượng sự kiện. Nếu nó là true, thì sự kiện sẽ vượt qua ranh giới. Ngược lại, nó chỉ có thể được bắt từ bên trong shadow DOM.

Nếu bạn xem xét UI Events specification, hầu hết các sự kiện có composed: true:

  • blur, focus, focusin, focusout,
  • click, dblclick,
  • mousedown, mouseup, mousemove, mouseout, mouseover,
  • wheel,
  • beforeinput, input, keydown, keyup.

    Tất cả các sự kiện chạm và sự kiện con trỏ cũng có composed: true.
    Tuy nhiên, có một số sự kiện có composed: false:

  • mouseenter, mouseleave (chúng không nổi bọt chút nào),
  • load, unload, abort, error,
  • select,
  • slotchange.
    Những sự kiện này chỉ có thể được bắt trên các phần tử trong cùng một DOM, nơi mục tiêu sự kiện cư trú.

3. Custom events

Khi chúng ta phát tán các sự kiện tùy chỉnh, chúng ta cần đặt cả thuộc tính bubblescomposed thành true để sự kiện nổi bọt lên và ra ngoài thành phần.
Ví dụ, ở đây chúng ta tạo div#inner trong shadow DOM của div#outer và kích hoạt hai sự kiện trên đó. Chỉ có sự kiện với composed: true mới ra ngoài được tài liệu:

<div id="outer"></div>

<script>
outer.attachShadow({mode: 'open'});

let inner = document.createElement('div');
outer.shadowRoot.append(inner);

/*
div(id=outer)
  #shadow-dom
    div(id=inner)
*/

document.addEventListener('test', event => alert(event.detail));

inner.dispatchEvent(new CustomEvent('test', {
  bubbles: true,
*!*
  composed: true,
*/!*
  detail: "composed"
}));

inner.dispatchEvent(new CustomEvent('test', {
  bubbles: true,
*!*
  composed: false,
*/!*
  detail: "not composed"
}));
</script>

4. Summary

Các sự kiện chỉ vượt qua ranh giới shadow DOM nếu cờ composed của chúng được đặt thành true.
Các sự kiện tích hợp chủ yếu có composed: true, như được mô tả trong các thông số kỹ thuật liên quan:

  • UI Events https://www.w3.org/TR/uievents.
  • Touch Events https://w3c.github.io/touch-events.
  • Pointer Events https://www.w3.org/TR/pointerevents.
    Một số sự kiện tích hợp có composed: false:
  • mouseenter, mouseleave (cũng không nổi bọt),
  • load, unload, abort, error,
  • select,
  • slotchange.
    Những sự kiện này chỉ có thể được bắt trên các phần tử trong cùng một DOM.
    Nếu chúng ta phát tán một CustomEvent, thì chúng ta nên đặt rõ ràng composed: true.

Xin lưu ý rằng trong trường hợp các thành phần lồng nhau, một shadow DOM có thể được lồng vào một cái khác. Trong trường hợp đó, các sự kiện có composed sẽ nổi bọt qua tất cả các ranh giới shadow DOM. Vì vậy, nếu một sự kiện chỉ được dự định cho thành phần bao bọc ngay lập tức, chúng ta cũng có thể phát tán nó trên shadow host và đặt composed: false. Sau đó, nó ra ngoài shadow DOM của thành phần, nhưng sẽ không nổi bọt lên các DOM cấp cao hơn.

Hy vọng rằng bài viết trên của Cafedev đã giúp bạn hiểu rõ hơn về cách kết hợp Shadow DOM và các sự kiện trong JavaScript. Với những kiến thức này, bạn có thể tạo ra các thành phần web mạnh mẽ và có khả năng tương tác tốt hơn. Tiếp tục khám phá và áp dụng những kỹ thuật này để nâng cao kỹ năng lập trình của bạn. Chúc bạn thành công trên hành trình phát triển ứng dụng web của mình!

Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Javascrypt chi tiết, dễ hiểu từ cơ bản tới nâng cao + Full Bài Tập thực hành nâng cao trình dev

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!