Shadow DOM 301

Konsep lanjutan & DOM API

Artikel ini membahas lebih banyak hal luar biasa yang dapat Anda lakukan dengan Shadow DOM. Artikel ini dibuat berdasarkan konsep yang dibahas dalam Shadow DOM 101 dan Shadow DOM 201.

Menggunakan beberapa shadow root

Jika Anda mengadakan pesta, ruangan akan terasa pengap jika semua orang berdesakan di ruangan yang sama. Anda ingin memiliki opsi untuk mendistribusikan sekelompok orang ke beberapa ruang. Elemen yang menghosting Shadow DOM juga dapat melakukan ini, yaitu, elemen tersebut dapat menghosting lebih dari satu root shadow sekaligus.

Mari kita lihat apa yang terjadi jika kita mencoba melampirkan beberapa shadow root ke host:

<div id="example1">Light DOM</div>
<script>
  var container = document.querySelector('#example1');
  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<div>Root 1 FTW</div>';
  root2.innerHTML = '<div>Root 2 FTW</div>';
</script>

Yang dirender adalah "Root 2 FTW", meskipun kita telah melampirkan shadow tree. Hal ini karena shadow tree terakhir yang ditambahkan ke host akan menang. Ini adalah stack LIFO dalam hal rendering. Memeriksa DevTools akan memverifikasi perilaku ini.

Jadi, apa gunanya menggunakan beberapa bayangan jika hanya bayangan terakhir yang diundang ke pesta rendering? Masukkan titik penyisipan bayangan.

Titik penyisipan shadow

"Titik penyisipan bayangan" (<shadow>) mirip dengan titik penyisipan normal (<content>) karena merupakan placeholder. Namun, alih-alih menjadi placeholder untuk konten host, elemen ini menjadi host untuk hierarki shadow lainnya. Ini adalah Inception Shadow DOM.

Seperti yang mungkin Anda bayangkan, hal-hal menjadi lebih rumit seiring Anda mempelajari lebih dalam. Oleh karena itu, spesifikasinya sangat jelas tentang apa yang terjadi saat beberapa elemen <shadow> sedang diputar:

Melihat kembali contoh awal kita, bayangan pertama root1 tidak dimasukkan ke dalam daftar undangan. Menambahkan titik penyisipan <shadow> akan mengembalikannya:

<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>

Ada beberapa hal menarik tentang contoh ini:

  1. "Root 2 FTW" masih dirender di atas "Root 1 FTW". Hal ini karena tempat kita menempatkan titik penyisipan <shadow>. Jika Anda ingin melakukan sebaliknya, pindahkan titik penyisipan: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Perhatikan bahwa sekarang ada titik penyisipan <content> di root1. Hal ini membuat node teks "Light DOM" ikut serta dalam proses rendering.

Apa yang dirender di <shadow>?

Terkadang, mengetahui shadow tree lama yang dirender di <shadow> akan berguna. Anda bisa mendapatkan referensi ke hierarki tersebut melalui .olderShadowRoot:

**root2.olderShadowRoot** === root1 //true

Mendapatkan shadow root host

Jika elemen menghosting Shadow DOM, Anda dapat mengakses shadow root termudanya menggunakan .shadowRoot:

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

Jika Anda khawatir orang lain akan melintasi bayangan Anda, tentukan ulang .shadowRoot menjadi null:

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

Sedikit trik, tetapi berhasil. Pada akhirnya, penting untuk diingat bahwa meskipun luar biasa, Shadow DOM belum dirancang untuk menjadi fitur keamanan. Jangan mengandalkannya untuk isolasi konten yang lengkap.

Membangun Shadow DOM di JS

Jika Anda lebih suka membuat DOM di JS, HTMLContentElement dan HTMLShadowElement memiliki antarmuka untuk itu.

<div id="example3">
  <span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();

var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);

 // HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);

var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);

// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>

Contoh ini hampir sama dengan contoh di bagian sebelumnya. Satu-satunya perbedaan adalah sekarang saya menggunakan select untuk menarik <span> yang baru ditambahkan.

Menggunakan titik penyisipan

Node yang dipilih dari elemen host dan "didistribusikan" ke dalam shadow tree disebut…drumroll…node terdistribusi. Elemen tersebut diizinkan untuk melewati batas shadow saat titik penyisipan mengundangnya masuk.

Hal yang secara konseptual aneh tentang titik penyisipan adalah bahwa titik tersebut tidak memindahkan DOM secara fisik. Node host tetap utuh. Titik penyisipan hanya memproyeksikan ulang node dari host ke shadow tree. Ini adalah masalah presentasi/rendering: "Pindahkan node ini ke sini" "Render node ini di lokasi ini".

Contoh:

<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';

var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>

Hasilnya? h2 bukan turunan dari shadow DOM. Hal ini mengarah pada informasi lain:

Element.getDistributedNodes()

Kita tidak dapat melintasi <content>, tetapi API .getDistributedNodes() memungkinkan kita mengkueri node yang didistribusikan di titik penyisipan:

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4>
</div>

<template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer>
</template>

<script>
var container = document.querySelector('#example4');

var root = container.createShadowRoot();

var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);

var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');
  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});
</script>

Element.getDestinationInsertionPoints()

Serupa dengan .getDistributedNodes(), Anda dapat memeriksa titik penyisipan tempat node didistribusikan dengan memanggil .getDestinationInsertionPoints()-nya:

<div id="host">
  <h2>Light DOM
</div>

<script>
  var container = document.querySelector('div');

  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';

  var h2 = document.querySelector('#host h2');
  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {
    console.log(contentEl);
  });
</script>

Alat: Shadow DOM Visualizer

Memahami keajaiban Shadow DOM itu sulit. Saya ingat saat mencoba memahaminya untuk pertama kali.

Untuk membantu memvisualisasikan cara kerja rendering Shadow DOM, saya telah membuat alat menggunakan d3.js. Kedua kotak markup di sebelah kiri dapat diedit. Jangan ragu untuk menempelkan markup Anda sendiri dan bereksperimen untuk melihat cara kerjanya dan bagaimana titik penyisipan mengalihkan node host ke dalam shadow tree.

Visualizer Shadow DOM
Luncurkan Shadow DOM Visualizer

Coba sekarang dan beri tahu kami pendapat Anda.

Model peristiwa

Beberapa peristiwa melintasi batas bayangan dan beberapa tidak. Jika peristiwa melintasi batas, target peristiwa akan disesuaikan untuk mempertahankan enkapsulasi yang disediakan oleh batas atas shadow root. Artinya, peristiwa ditargetkan ulang agar terlihat seolah-olah berasal dari elemen host, bukan elemen internal ke Shadow DOM.

Tindakan Play 1

  • Ini menarik. Anda akan melihat mouseout dari elemen host (<div data-host>) ke node biru. Meskipun merupakan node terdistribusi, node tersebut masih berada di host, bukan ShadowDOM. Menggerakkan mouse lebih jauh ke bawah ke warna kuning lagi akan menyebabkan mouseout pada node biru.

Tindakan Play 2

  • Ada satu mouseout yang muncul di host (di bagian paling akhir). Biasanya, Anda akan melihat peristiwa mouseout dipicu untuk semua blok kuning. Namun, dalam kasus ini, elemen tersebut bersifat internal untuk Shadow DOM dan peristiwa tidak menggelembung melalui batas atasnya.

Tindakan Play 3

  • Perhatikan bahwa saat Anda mengklik input, focusin tidak muncul pada input, tetapi pada node host itu sendiri. Targetnya telah diubah.

Acara yang selalu dihentikan

Peristiwa berikut tidak pernah melewati batas shadow:

  • batal
  • error
  • pilih
  • ubah
  • load
  • reset
  • ubah ukuran
  • scroll
  • selectstart

Kesimpulan

Semoga Anda setuju bahwa Shadow DOM sangatlah bermanfaat. Untuk pertama kalinya, kita memiliki enkapsulasi yang tepat tanpa beban tambahan <iframe> atau teknik lama lainnya.

Shadow DOM memang kompleks, tetapi layak ditambahkan ke platform web. Luangkan waktu untuk mempelajarinya. Pelajari. Ajukan pertanyaan.

Jika Anda ingin mempelajari lebih lanjut, lihat artikel pengantar Dominic Shadow DOM 101 dan artikel Shadow DOM 201: CSS & Styling saya.