23 Oktober 2020

Shadow DOM dan events

Ide di balik pohon bayangan adalah untuk mengenkapsulasi detail implementasi internal suatu komponen.

Misalkan, event klik terjadi di dalam shadow DOM dari komponen <user-card>. Namun skrip di dokumen utama tidak mengetahui internal shadow DOM, terutama jika komponen tersebut berasal dari pustaka pihak ketiga.

Jadi, untuk menjaga detailnya tetap terenkapsulasi, browser menargetkan ulang event tersebut.

Event yang terjadi di shadow DOM memiliki elemen host sebagai target, saat tertangkap di luar komponen.

Berikut contoh sederhananya:

<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>

Jika anda mengklik tombol tersebut, pesan yang ditampilkan adalah:

  1. Inner target: BUTTONevent handler internal mendapatkan target yang benar, yaitu elemen di dalam shadow DOM.
  2. Outer target: USER-CARD – dokumen event handler mendapatkan shadow host sebagai target.

Penargetan ulang event adalah hal yang bagus untuk dimiliki, karena dokumen luar tidak harus tahu tentang komponen internal. Dari sudut pandang ini, event tersebut terjadi di <user-card>.

Penargetan ulang tidak terjadi jika peristiwa terjadi pada slotted elemen, yang secara fisik berada di light DOM.

Misalnya, jika pengguna mengklik <span slot =" username "> pada contoh di bawah, target event persis elemen span ini, untuk shadow dan light handler:

<user-card id="userCard">
  <span slot="username">John Smith</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>

Jika klik terjadi pada "John Smith", untuk inner dan outer handler, targetnya adalah <span slot =" username ">. Itu adalah elemen dari light DOM, jadi tidak ada penargetan ulang.

Di sisi lain, jika klik terjadi pada elemen yang berasal dari shadow DOM, misalnya pada <b> Name </b>, lalu, saat ia menggelembung keluar dari shadow DOM, event.target-nya disetel ulang ke <user-card>.

Bubbling, event.composedPath()

Untuk tujuan event bubbling, DOM yang diratakan digunakan.

Jadi, jika kita memiliki slotted elemen, dan sebuah event terjadi di suatu tempat di dalamnya, maka event itu menggelembung ke <slot> dan ke atasnya.

Jalur lengkap ke target event asli, dengan semua elemen bayangan, bisa diperoleh menggunakan event.composedPath(). Seperti yang dapat kita lihat dari nama method, jalur tersebut diambil setelah komposisi.

Dalam contoh di atas, DOM yang diratakan adalah:

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

Jadi, untuk klik pada <span slot="username">, panggilan ke event.composedPath() mengembalikan sebuah array: [span, slot, div, shadow-root, User-card, body, html, document, window]. Itu persis seperti rantai induk dari elemen target di DOM yang diratakan, setelah komposisi.

Shadow tree details are only provided for {mode:'open'} trees

Jika pohon bayangan dibuat dengan {mode: 'closed'}, maka jalur yang dibuat dimulai dari host: user-card dan ke atas.

Itu prinsip yang sama untuk methods lain yang bekerja dengan shadow DOM. Bagian dalam pohon tertutup benar-benar tersembunyi.

event.composed

Sebagian besar event berhasil menggelembung melewati batas shadow DOM. Ada beberapa event yang tidak.

Ini diatur oleh properti event objek kompos. Jika itu true, maka event tersebut melewati batas. Jika tidak, event itu hanya bisa ditangkap dari dalam shadow DOM.

Jika anda melihat UI Events specification, sebagian besar event memiliki composed: true:

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

Semua event sentuh dan event penunjuk juga memiliki composed: true.

Ada beberapa event yang memiliki compose: false:

  • mouseenter, mouseleave (they do not bubble at all),
  • load, unload, abort, error,
  • select,
  • slotchange.

Event ini hanya dapat ditangkap pada elemen dalam DOM yang sama, tempat target event berada.

Event kustom

Saat kita mengirimkan event khusus, kita perlu mengatur properti bubble dan composed ke true agar dapat menggelembung dan keluar dari komponen.

Misalnya, di sini kita membuat div#inner di shadow DOM dari div#outer dan memicu dua event di atasnya. Hanya satu dengan composed: true yang membuatnya berada di luar dokumen:

<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>

Ringkasan

Event hanya melewati batas shadow DOM jika flag composed disetel ke true.

Event bawaan sebagian besar memiliki composed: true, seperti yang dijelaskan dalam spesifikasi yang relevan:

Beberapa event bawaan yang memiliki composed: false:

  • mouseenter, mouseleave (juga tidak menggelembung),
  • load, unload, abort, error,
  • select,
  • slotchange.

Events ini hanya dapat ditangkap pada elemen dalam DOM yang sama.

Jika kita mengirimkan CustomEvent, maka kita harus secara eksplisit mengatur composed: true.

Harap diperhatikan bahwa dalam kasus komponen bertingkat, satu shadow DOM dapat bertumpuk ke dalam yang lain. Dalam kasus tersebut, gelembung peristiwa yang disusun melalui semua batas shadow DOM. Jadi, jika sebuah peristiwa hanya ditujukan untuk komponen penutup langsung, kita juga dapat mengirimkannya ke host bayangan dan menyetel composed: false. Kemudian keluar dari komponen shadow DOM, tetapi tidak akan mengarah ke DOM tingkat yang lebih tinggi.

Peta tutorial

komentar

baca ini sebelum berkomentar…
  • Jika Anda memiliki saran apa yang harus ditingkatkan - silakan kunjungi kirimkan Github issue atau pull request sebagai gantinya berkomentar.
  • Jika Anda tidak dapat memahami sesuatu dalam artikel – harap jelaskan.
  • Untuk menyisipkan beberapa kata kode, gunakan tag <code>, untuk beberapa baris – bungkus dengan tag <pre>, untuk lebih dari 10 baris – gunakan sandbox (plnkr, jsbin, < a href='http://codepen.io'>codepen…)