Shadow DOM memungkinkan developer web membuat DOM dan CSS yang terpisah untuk komponen web
Ringkasan
Shadow DOM menghilangkan kerapuhan dalam membangun aplikasi web. Kerapuhan ini
berasal dari sifat global HTML, CSS, dan JS. Selama bertahun-tahun, kami telah
menciptakan banyak
sekali
alat
untuk mengatasi masalah tersebut. Misalnya, saat Anda menggunakan id/class HTML baru,
tidak ada yang tahu apakah id/class tersebut akan bertentangan dengan nama yang sudah ada yang digunakan oleh halaman.
Bug kecil muncul,
kekhususan CSS menjadi masalah besar (!important semuanya!), pemilih gaya
menjadi tidak terkontrol, dan
performa dapat menurun. Dan masih banyak lagi.
Shadow DOM memperbaiki CSS dan DOM. Fitur ini memperkenalkan gaya tercakup ke platform web. Tanpa alat atau konvensi penamaan, Anda dapat memaketkan CSS dengan markup, menyembunyikan detail implementasi, dan membuat komponen mandiri di vanilla JavaScript.
Pengantar
Shadow DOM adalah salah satu dari tiga standar Komponen Web: Template HTML, Shadow DOM, dan Elemen kustom. Impor HTML dulunya merupakan bagian dari daftar, tetapi sekarang dianggap tidak digunakan lagi.
Anda tidak perlu membuat komponen web yang menggunakan shadow DOM. Namun, saat melakukannya, Anda memanfaatkan manfaatnya (cakupan CSS, enkapsulasi DOM, komposisi) dan membuat elemen kustom yang dapat digunakan kembali, yang tangguh, sangat dapat dikonfigurasi, dan sangat dapat digunakan kembali. Jika elemen kustom adalah cara untuk membuat HTML baru (dengan JS API), shadow DOM adalah cara Anda menyediakan HTML dan CSS-nya. Kedua API ini digabungkan untuk membuat komponen dengan HTML, CSS, dan JavaScript mandiri.
Shadow DOM dirancang sebagai alat untuk membangun aplikasi berbasis komponen. Oleh karena itu, Next.js menghadirkan solusi untuk masalah umum dalam pengembangan web:
- DOM Terisolasi: DOM komponen bersifat mandiri (misalnya,
document.querySelector()tidak akan menampilkan node di shadow DOM komponen). - CSS tercakup: CSS yang ditentukan di dalam shadow DOM tercakup di dalamnya. Aturan gaya tidak bocor dan gaya halaman tidak merembes masuk.
- Komposisi: Desain API deklaratif berbasis markup untuk komponen Anda.
- Menyederhanakan CSS - DOM tercakup berarti Anda dapat menggunakan pemilih CSS sederhana, nama id/class yang lebih umum, dan tidak perlu khawatir tentang konflik penamaan.
- Produktivitas - Anggap aplikasi sebagai potongan DOM, bukan satu halaman besar (global).
Demo fancy-tabs
Di sepanjang artikel ini, saya akan merujuk ke komponen demo (<fancy-tabs>)
dan merujuk ke cuplikan kode dari komponen tersebut. Jika browser Anda mendukung API, Anda
akan melihat demo langsungnya di bawah ini. Atau, lihat sumber lengkap di GitHub.
Apa itu shadow DOM?
Latar belakang DOM
HTML mendukung web karena mudah digunakan. Dengan mendeklarasikan beberapa tag, Anda dapat membuat halaman dalam hitungan detik yang memiliki presentasi dan struktur. Namun, HTML saja tidak terlalu berguna. Manusia dapat dengan mudah memahami bahasa berbasis teks, tetapi mesin membutuhkan sesuatu yang lebih. Masukkan Document Object Model, atau DOM.
Saat memuat halaman web, browser melakukan banyak hal menarik. Salah satu hal yang dilakukannya adalah mengubah HTML penulis menjadi dokumen aktif. Pada dasarnya, untuk memahami struktur halaman, browser mengurai HTML (string teks statis) menjadi model data (objek/node). Browser mempertahankan hierarki HTML dengan membuat hierarki node ini: DOM. Hal yang menarik dari DOM adalah DOM merupakan representasi langsung halaman Anda. Tidak seperti HTML statis yang kita buat, node yang dihasilkan browser berisi properti, metode, dan yang terbaik dari semuanya… dapat dimanipulasi oleh program. Itulah sebabnya kita dapat membuat elemen DOM secara langsung menggunakan JavaScript:
const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);
menghasilkan markup HTML berikut:
<body>
<header>
<h1>Hello DOM</h1>
</header>
</body>
Semua itu baik dan bagus. Lalu, apa sebenarnya shadow DOM?
DOM… dalam bayangan
Shadow DOM hanyalah DOM biasa dengan dua perbedaan: 1) cara pembuatan/penggunaannya dan
2) perilakunya dalam kaitannya dengan bagian lain pada halaman. Biasanya, Anda membuat node DOM dan menambahkannya sebagai turunan dari elemen lain. Dengan shadow DOM, Anda
membuat hierarki DOM tercakup yang dilampirkan ke elemen, tetapi terpisah dari
turunan sebenarnya. Subhierarki tercakup ini disebut shadow tree. Elemen
yang dilampirkan adalah host bayangan. Apa pun yang Anda tambahkan di shadow akan menjadi
lokal untuk elemen hosting, termasuk <style>. Inilah cara shadow DOM
mencapai cakupan gaya CSS.
Membuat shadow DOM
Shadow root adalah fragmen dokumen yang dilampirkan ke elemen “host”.
Tindakan melampirkan shadow root adalah cara elemen mendapatkan shadow DOM-nya. Untuk
membuat shadow DOM untuk elemen, panggil element.attachShadow():
const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().
// header.shadowRoot === shadowRoot
// shadowRoot.host === header
Saya menggunakan .innerHTML untuk mengisi root shadow, tetapi Anda juga dapat menggunakan API DOM lainnya. Ini adalah web. Kita punya pilihan.
Spesifikasi menentukan daftar elemen yang tidak dapat menghosting shadow tree. Ada beberapa alasan mengapa suatu elemen mungkin ada dalam daftar:
- Browser sudah menghosting shadow DOM internalnya sendiri untuk elemen
(
<textarea>,<input>). - Tidak masuk akal jika elemen menghosting shadow DOM (
<img>).
Misalnya, hal berikut tidak akan berfungsi:
document.createElement('input').attachShadow({mode: 'open'});
// Error. `<input>` cannot host shadow dom.
Membuat shadow DOM untuk elemen kustom
Shadow DOM sangat berguna saat membuat elemen kustom. Gunakan shadow DOM untuk mengelompokkan HTML, CSS, dan JS elemen, sehingga menghasilkan "komponen web".
Contoh - elemen kustom melampirkan shadow DOM ke dirinya sendiri, yang mengenkapsulasi DOM/CSS-nya:
// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
// Attach a shadow root to <fancy-tabs>.
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
<div id="tabs">...</div>
<div id="panels">...</div>
`;
}
...
});
Ada beberapa hal menarik yang terjadi di sini. Pertama,
elemen kustom membuat shadow DOM-nya sendiri saat instance <fancy-tabs>
dibuat. Hal ini dilakukan di constructor(). Kedua, karena kita membuat
shadow root, aturan CSS di dalam <style> akan dicakupkan ke <fancy-tabs>.
Komposisi dan slot
Komposisi adalah salah satu fitur shadow DOM yang paling sedikit dipahami, tetapi bisa dibilang yang paling penting.
Dalam dunia pengembangan web, komposisi adalah cara kita membuat aplikasi,
secara deklaratif dari HTML. Berbagai elemen penyusun (<div>, <header>,
<form>, <input>) digabungkan untuk membentuk aplikasi. Beberapa tag ini bahkan berfungsi
bersama-sama. Komposisi adalah alasan elemen native seperti <select>,
<details>, <form>, dan <video> sangat fleksibel. Setiap tag tersebut menerima
HTML tertentu sebagai turunan dan melakukan sesuatu yang khusus dengannya. Misalnya,
<select> tahu cara merender <option> dan <optgroup> menjadi widget dropdown dan
multi-pilihan. Elemen <details> merender <summary> sebagai
panah yang dapat diperluas. Bahkan <video> tahu cara menangani turunan tertentu:
elemen <source> tidak dirender, tetapi memengaruhi perilaku video.
Sungguh ajaib!
Terminologi: Light DOM vs. shadow DOM
Komposisi shadow DOM memperkenalkan banyak dasar-dasar baru dalam pengembangan web. Sebelum membahas lebih lanjut, mari kita standarkan beberapa terminologi agar kita menggunakan istilah yang sama.
DOM Ringan
Markup yang ditulis pengguna komponen Anda. DOM ini berada di luar shadow DOM komponen. Elemen ini adalah turunan sebenarnya dari elemen tersebut.
<better-button>
<!-- the image and span are better-button's light DOM -->
<img src="gear.svg" slot="icon">
<span>Settings</span>
</better-button>
Shadow DOM
DOM yang ditulis oleh penulis komponen. Shadow DOM bersifat lokal untuk komponen dan menentukan struktur internalnya, CSS tercakup, dan mengenkapsulasi detail implementasi Anda. Elemen ini juga dapat menentukan cara merender markup yang ditulis oleh konsumen komponen Anda.
#shadow-root
<style>...</style>
<slot name="icon"></slot>
<span id="wrapper">
<slot>Button</slot>
</span>
Hierarki DOM yang diratakan
Hasil dari browser yang mendistribusikan DOM ringan pengguna ke DOM bayangan Anda, yang merender produk akhir. Hierarki yang di-flatten adalah yang pada akhirnya Anda lihat di DevTools dan yang dirender di halaman.
<better-button>
#shadow-root
<style>...</style>
<slot name="icon">
<img src="gear.svg" slot="icon">
</slot>
<span id="wrapper">
<slot>
<span>Settings</span>
</slot>
</span>
</better-button>
Elemen <slot>
Shadow DOM menyusun berbagai hierarki DOM menggunakan elemen <slot>.
Slot adalah placeholder di dalam komponen yang dapat diisi pengguna dengan
markup mereka sendiri. Dengan menentukan satu atau beberapa slot, Anda mengundang markup luar untuk dirender
di DOM bayangan komponen Anda. Pada dasarnya, Anda mengatakan "Render markup pengguna di sini".
Elemen diizinkan untuk "melintasi" batas shadow DOM saat <slot> mengundangnya
masuk. Elemen ini disebut node terdistribusi. Secara konseptual,
node terdistribusi mungkin tampak agak aneh. Slot tidak memindahkan DOM secara fisik; slot merendernya di lokasi lain di dalam shadow DOM.
Komponen dapat menentukan nol atau beberapa slot dalam shadow DOM-nya. Slot dapat kosong atau menyediakan konten pengganti. Jika pengguna tidak memberikan konten light DOM, slot akan merender konten penggantinya.
<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>
<slot>fallback content</slot> <!-- default slot with fallback content -->
<slot> <!-- default slot entire DOM tree as fallback -->
<h2>Title</h2>
<summary>Description text</summary>
</slot>
Anda juga dapat membuat slot bernama. Slot bernama adalah lubang tertentu di DOM bayangan yang dirujuk pengguna berdasarkan nama.
Contoh - slot di shadow DOM <fancy-tabs>:
#shadow-root
<div id="tabs">
<slot id="tabsSlot" name="title"></slot> <!-- named slot -->
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
Pengguna komponen mendeklarasikan <fancy-tabs> seperti ini:
<fancy-tabs>
<button slot="title">Title</button>
<button slot="title" selected>Title 2</button>
<button slot="title">Title 3</button>
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</fancy-tabs>
<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
<h2 slot="title">Title</h2>
<section>content panel 1</section>
<h2 slot="title" selected>Title 2</h2>
<section>content panel 2</section>
<h2 slot="title">Title 3</h2>
<section>content panel 3</section>
</fancy-tabs>
Jika Anda penasaran, hierarki yang di-flatten akan terlihat seperti ini:
<fancy-tabs>
#shadow-root
<div id="tabs">
<slot id="tabsSlot" name="title">
<button slot="title">Title</button>
<button slot="title" selected>Title 2</button>
<button slot="title">Title 3</button>
</slot>
</div>
<div id="panels">
<slot id="panelsSlot">
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</slot>
</div>
</fancy-tabs>
Perhatikan bahwa komponen kita dapat menangani berbagai konfigurasi, tetapi
hierarki DOM yang di-flatten tetap sama. Kita juga dapat beralih dari <button> ke
<h2>. Komponen ini dibuat untuk menangani berbagai jenis turunan… sama seperti <select>.
Gaya visual
Ada banyak opsi untuk menata gaya komponen web. Komponen yang menggunakan shadow DOM dapat diberi gaya oleh halaman utama, menentukan gayanya sendiri, atau menyediakan hook (dalam bentuk properti kustom CSS) bagi pengguna untuk mengganti setelan default.
Gaya yang ditentukan komponen
Fitur shadow DOM yang paling berguna adalah CSS tercakup:
- Pemilih CSS dari halaman luar tidak berlaku di dalam komponen Anda.
- Gaya yang ditentukan di dalam tidak akan bocor. Gaya ini dicakupkan ke elemen host.
Pemilih CSS yang digunakan di dalam shadow DOM berlaku secara lokal untuk komponen Anda. Dalam praktiknya, ini berarti kita dapat menggunakan nama id/class umum lagi, tanpa mengkhawatirkan konflik di tempat lain di halaman. Pemilih CSS yang lebih sederhana adalah praktik terbaik di dalam Shadow DOM. Aset juga baik untuk performa.
Contoh - gaya yang ditentukan dalam root bayangan bersifat lokal
#shadow-root
<style>
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
...
}
#tabs {
display: inline-flex;
...
}
</style>
<div id="tabs">
...
</div>
<div id="panels">
...
</div>
Stylesheet juga dicakupkan ke hierarki shadow:
#shadow-root
<link rel="stylesheet" href="styles.css">
<div id="tabs">
...
</div>
<div id="panels">
...
</div>
Pernahkah Anda bertanya-tanya bagaimana elemen <select> merender widget pilih beberapa (bukan
dropdown) saat Anda menambahkan atribut multiple:
<select multiple>
<option>Do</option>
<option selected>Re</option>
<option>Mi</option>
<option>Fa</option>
<option>So</option>
</select>
<select> dapat menata gaya dirinya sendiri secara berbeda berdasarkan atribut yang Anda
deklarasikan di dalamnya. Komponen web juga dapat menata gayanya sendiri, dengan menggunakan pemilih :host.
Contoh - komponen yang menata gayanya sendiri
<style>
:host {
display: block; /* by default, custom elements are display: inline */
contain: content; /* CSS containment FTW. */
}
</style>
Satu hal yang perlu diperhatikan dengan :host adalah aturan di halaman induk memiliki kekhususan yang lebih tinggi
daripada aturan :host yang ditentukan dalam elemen. Artinya, gaya luar lebih diutamakan. Hal ini memungkinkan pengguna mengganti gaya tingkat atas Anda dari luar. Selain itu, :host
hanya berfungsi dalam konteks root shadow, sehingga Anda tidak dapat menggunakannya di luar
DOM shadow.
Bentuk fungsional :host(<selector>) memungkinkan Anda menargetkan host jika cocok dengan <selector>. Ini adalah cara yang bagus bagi komponen Anda untuk mengenkapsulasi
perilaku yang bereaksi terhadap interaksi pengguna atau status atau gaya node internal berdasarkan
host.
<style>
:host {
opacity: 0.4;
will-change: opacity;
transition: opacity 300ms ease-in-out;
}
:host(:hover) {
opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
background: grey;
pointer-events: none;
opacity: 0.4;
}
:host(.blue) {
color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>
Menata gaya berdasarkan konteks
:host-context(<selector>) cocok dengan komponen jika komponen tersebut atau salah satu ancestor-nya
cocok dengan <selector>. Penggunaan umum untuk hal ini adalah pembuatan tema berdasarkan lingkungan
komponen. Misalnya, banyak orang menerapkan tema dengan menerapkan class ke
<html> atau <body>:
<body class="darktheme">
<fancy-tabs>
...
</fancy-tabs>
</body>
:host-context(.darktheme) akan menata gaya <fancy-tabs> jika merupakan turunan
dari .darktheme:
:host-context(.darktheme) {
color: white;
background: black;
}
:host-context() dapat berguna untuk membuat tema, tetapi pendekatan yang lebih baik adalah
membuat hook gaya menggunakan properti kustom CSS.
Menata gaya node terdistribusi
::slotted(<compound-selector>) cocok dengan node yang didistribusikan ke dalam
<slot>.
Misalnya, kita telah membuat komponen badge nama:
<name-badge>
<h2>Eric Bidelman</h2>
<span class="title">
Digital Jedi, <span class="company">Google</span>
</span>
</name-badge>
Shadow DOM komponen dapat menata gaya <h2> dan .title pengguna:
<style>
::slotted(h2) {
margin: 0;
font-weight: 300;
color: red;
}
::slotted(.title) {
color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
text-transform: uppercase;
}
*/
</style>
<slot></slot>
Jika Anda ingat dari sebelumnya, <slot> tidak memindahkan DOM ringan pengguna. Saat
node didistribusikan ke <slot>, <slot> akan merender DOM-nya, tetapi
node tetap berada di tempatnya secara fisik. Gaya yang diterapkan sebelum distribusi akan terus
diterapkan setelah distribusi. Namun, saat light DOM didistribusikan, light DOM dapat
mengambil gaya tambahan (yang ditentukan oleh shadow DOM).
Contoh lain yang lebih mendalam dari <fancy-tabs>:
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
border-radius: 3px;
padding: 16px;
height: 250px;
overflow: auto;
}
#tabs {
display: inline-flex;
-webkit-user-select: none;
user-select: none;
}
#tabsSlot::slotted(*) {
font: 400 16px/22px 'Roboto';
padding: 16px 8px;
...
}
#tabsSlot::slotted([aria-selected="true"]) {
font-weight: 600;
background: white;
box-shadow: none;
}
#panelsSlot::slotted([aria-hidden="true"]) {
display: none;
}
</style>
<div id="tabs">
<slot id="tabsSlot" name="title"></slot>
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
`;
Dalam contoh ini, ada dua slot: slot bernama untuk judul tab, dan slot untuk konten panel tab. Saat pengguna memilih tab, kita akan menebalkan pilihannya
dan menampilkan panelnya. Hal ini dilakukan dengan memilih node terdistribusi yang memiliki atribut selected. JS elemen kustom (tidak ditampilkan di sini) menambahkan atribut tersebut pada waktu yang tepat.
Menata gaya komponen dari luar
Ada beberapa cara untuk menata gaya komponen dari luar. Cara termudah adalah menggunakan nama tag sebagai pemilih:
fancy-tabs {
width: 500px;
color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
box-shadow: 0 3px 3px #ccc;
}
Gaya luar selalu lebih diutamakan daripada gaya yang ditentukan dalam shadow DOM. Misalnya,
jika pengguna menulis pemilih fancy-tabs { width: 500px; }, pemilih tersebut akan mengungguli
aturan komponen: :host { width: 650px;}.
Menata gaya komponen itu sendiri hanya akan memberikan hasil yang terbatas. Namun, apa yang terjadi jika Anda ingin menata gaya internal komponen? Untuk itu, kita memerlukan properti kustom CSS.
Membuat hook gaya menggunakan properti kustom CSS
Pengguna dapat mengubah gaya internal jika penulis komponen menyediakan hook gaya menggunakan properti kustom CSS. Secara konseptual, idenya mirip dengan
<slot>. Anda membuat "placeholder gaya" untuk diganti oleh pengguna.
Contoh - <fancy-tabs> memungkinkan pengguna mengganti warna latar belakang:
<!-- main page -->
<style>
fancy-tabs {
margin-bottom: 32px;
--fancy-tabs-bg: black;
}
</style>
<fancy-tabs background>...</fancy-tabs>
Di dalam shadow DOM-nya:
:host([background]) {
background: var(--fancy-tabs-bg, #9E9E9E);
border-radius: 10px;
padding: 10px;
}
Dalam hal ini, komponen akan menggunakan black sebagai nilai latar belakang karena
pengguna menyediakannya. Jika tidak, nilai defaultnya adalah #9E9E9E.
Topik lanjutan
Membuat root shadow tertutup (sebaiknya dihindari)
Ada jenis shadow DOM lain yang disebut mode "tertutup". Saat Anda membuat
shadow tree tertutup, JavaScript luar tidak akan dapat mengakses DOM internal
komponen Anda. Hal ini mirip dengan cara kerja elemen native seperti <video>.
JavaScript tidak dapat mengakses shadow DOM <video> karena browser
mengimplementasikannya menggunakan shadow root mode tertutup.
Contoh - membuat shadow tree tertutup:
const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div
API lain juga terpengaruh oleh mode tertutup:
Element.assignedSlot/TextNode.assignedSlotakan menampilkannullEvent.composedPath()untuk peristiwa yang terkait dengan elemen di dalam DOM shadow, menampilkan []
Berikut ringkasan saya tentang alasan Anda tidak boleh membuat komponen web dengan
{mode: 'closed'}:
Rasa aman yang semu. Tidak ada yang dapat menghentikan penyerang untuk membajak
Element.prototype.attachShadow.Mode tertutup mencegah kode elemen kustom Anda mengakses shadow DOM-nya sendiri. Itu benar-benar gagal. Sebagai gantinya, Anda harus menyimpan referensi untuk nanti jika ingin menggunakan hal-hal seperti
querySelector(). Hal ini sepenuhnya mengalahkan tujuan awal mode tertutup.customElements.define('x-element', class extends HTMLElement { constructor() { super(); // always call super() first in the constructor. this._shadowRoot = this.attachShadow({mode: 'closed'}); this._shadowRoot.innerHTML = '<div class="wrapper"></div>'; } connectedCallback() { // When creating closed shadow trees, you'll need to stash the shadow root // for later if you want to use it again. Kinda pointless. const wrapper = this._shadowRoot.querySelector('.wrapper'); } ... });Mode tertutup membuat komponen Anda kurang fleksibel bagi pengguna akhir. Saat Anda membuat komponen web, akan ada saatnya Anda lupa menambahkan fitur. Opsi konfigurasi. Kasus penggunaan yang diinginkan pengguna. Contoh umumnya adalah lupa menyertakan hook gaya yang memadai untuk node internal. Dengan mode tertutup, pengguna tidak dapat mengganti setelan default dan mengubah gaya. Kemampuan untuk mengakses internal komponen sangat membantu. Pada akhirnya, pengguna akan melakukan fork pada komponen Anda, mencari komponen lain, atau membuat komponen mereka sendiri jika komponen Anda tidak melakukan apa yang mereka inginkan :(
Menggunakan slot di JS
Shadow DOM API menyediakan utilitas untuk menggunakan slot dan node terdistribusi. Hal ini berguna saat membuat elemen kustom.
peristiwa slotchange
Peristiwa slotchange diaktifkan saat node slot yang didistribusikan berubah. Misalnya, jika pengguna menambahkan/menghapus turunan dari DOM ringan.
const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
console.log('light dom children changed!');
});
Untuk memantau jenis perubahan lain pada DOM ringan, Anda dapat menyiapkan
MutationObserver
di konstruktor elemen.
Elemen apa yang dirender dalam slot?
Terkadang, Anda perlu mengetahui elemen apa yang terkait dengan slot. Panggil
slot.assignedNodes() untuk menemukan elemen yang dirender slot. Opsi
{flatten: true} juga akan menampilkan konten pengganti slot (jika tidak ada node
yang didistribusikan).
Sebagai contoh, misalnya shadow DOM Anda terlihat seperti ini:
<slot><b>fallback content</b></slot>
| Penggunaan | Telepon | Hasil |
|---|---|---|
| <my-component>teks komponen</my-component> | slot.assignedNodes(); |
[component text] |
| <my-component></my-component> | slot.assignedNodes(); |
[] |
| <my-component></my-component> | slot.assignedNodes({flatten: true}); |
[<b>fallback content</b>] |
Slot mana yang ditetapkan untuk elemen?
Anda juga dapat menjawab pertanyaan sebaliknya. element.assignedSlot memberi tahu
Anda slot komponen mana yang ditetapkan ke elemen Anda.
Model peristiwa Shadow DOM
Saat peristiwa muncul dari shadow DOM, targetnya disesuaikan untuk mempertahankan enkapsulasi yang disediakan shadow DOM. Artinya, peristiwa ditargetkan ulang agar terlihat seperti berasal dari komponen, bukan elemen internal dalam shadow DOM Anda. Beberapa peristiwa bahkan tidak menyebar keluar dari shadow DOM.
Peristiwa yang melintasi batas bayangan adalah:
- Peristiwa Fokus:
blur,focus,focusin,focusout - Peristiwa Mouse:
click,dblclick,mousedown,mouseenter,mousemove, dll. - Peristiwa Roda:
wheel - Peristiwa Input:
beforeinput,input - Peristiwa Keyboard:
keydown,keyup - Peristiwa Komposisi:
compositionstart,compositionupdate,compositionend - DragEvent:
dragstart,drag,dragend,drop, dll.
Tips
Jika shadow tree terbuka, pemanggilan event.composedPath() akan menampilkan array
node yang dilalui peristiwa.
Menggunakan peristiwa kustom
Peristiwa DOM kustom yang diaktifkan pada node internal di shadow tree tidak akan
keluar dari batas shadow kecuali jika peristiwa tersebut dibuat menggunakan
flag composed: true:
// Inside <fancy-tab> custom element class definition:
selectTab() {
const tabs = this.shadowRoot.querySelector('#tabs');
tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}
Jika composed: false (default), konsumen tidak akan dapat memproses peristiwa
di luar root shadow Anda.
<fancy-tabs></fancy-tabs>
<script>
const tabs = document.querySelector('fancy-tabs');
tabs.addEventListener('tab-select', e => {
// won't fire if `tab-select` wasn't created with `composed: true`.
});
</script>
Menangani fokus
Jika Anda ingat dari model peristiwa shadow DOM, peristiwa yang diaktifkan
di dalam shadow DOM disesuaikan agar terlihat seperti berasal dari elemen hosting.
Misalnya, Anda mengklik <input> di dalam shadow root:
<x-focus>
#shadow-root
<input type="text" placeholder="Input inside shadow dom">
Peristiwa focus akan terlihat seperti berasal dari <x-focus>, bukan <input>.
Demikian pula, document.activeElement akan menjadi <x-focus>. Jika shadow root
dibuat dengan mode:'open' (lihat mode tertutup), Anda juga akan
dapat mengakses node internal yang mendapatkan fokus:
document.activeElement.shadowRoot.activeElement // only works with open mode.
Jika ada beberapa level shadow DOM yang berperan (misalnya elemen kustom dalam
elemen kustom lain), Anda harus menelusuri shadow root secara rekursif untuk
menemukan activeElement:
function deepActiveElement() {
let a = document.activeElement;
while (a && a.shadowRoot && a.shadowRoot.activeElement) {
a = a.shadowRoot.activeElement;
}
return a;
}
Opsi lain untuk fokus adalah opsi delegatesFocus: true, yang memperluas
perilaku fokus elemen dalam shadow tree:
- Jika Anda mengklik node di dalam shadow DOM dan node tersebut bukan area yang dapat difokuskan, area pertama yang dapat difokuskan akan menjadi fokus.
- Saat node di dalam shadow DOM mendapatkan fokus,
:focusakan diterapkan ke host selain elemen yang difokuskan.
Contoh - cara delegatesFocus: true mengubah perilaku fokus
<style>
:focus {
outline: 2px solid red;
}
</style>
<x-focus></x-focus>
<script>
customElements.define('x-focus', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
const root = this.attachShadow({mode: 'open', delegatesFocus: true});
root.innerHTML = `
<style>
:host {
display: flex;
border: 1px dotted black;
padding: 16px;
}
:focus {
outline: 2px solid blue;
}
</style>
<div>Clickable Shadow DOM text</div>
<input type="text" placeholder="Input inside shadow dom">`;
// Know the focused element inside shadow DOM:
this.addEventListener('focus', function(e) {
console.log('Active element (inside shadow dom):',
this.shadowRoot.activeElement);
});
}
});
</script>
Hasil
Di atas adalah hasil saat <x-focus> difokuskan (klik pengguna, tab,
focus(), dll.), "Teks DOM Bayangan yang Dapat Diklik" diklik, atau <input> internal
difokuskan (termasuk autofocus).
Jika Anda menetapkan delegatesFocus: false, berikut yang akan Anda lihat:
delegatesFocus: false dan <input> internal difokuskan.
delegatesFocus: false dan <x-focus>
mendapatkan fokus (misalnya, memiliki tabindex="0").
delegatesFocus: false dan "Teks Shadow DOM yang dapat diklik"
diklik (atau area kosong lainnya dalam shadow DOM elemen diklik).
Tips dan trik
Selama bertahun-tahun, saya telah mempelajari beberapa hal tentang penulisan komponen web. Kami berharap beberapa tips ini akan berguna bagi Anda untuk menulis komponen dan men-debug shadow DOM.
Menggunakan pembatasan CSS
Biasanya, tata letak/gaya/gambar komponen web cukup mandiri. Gunakan
penampungan CSS di :host untuk meningkatkan
performa:
<style>
:host {
display: block;
contain: content; /* Boom. CSS containment FTW. */
}
</style>
Mereset gaya yang dapat diwariskan
Gaya yang dapat diwariskan (background, color, font, line-height, dll.) terus
diwariskan di shadow DOM. Artinya, elemen tersebut menembus batas shadow DOM secara default. Jika Anda ingin memulai dari awal, gunakan all: initial; untuk mereset
gaya yang dapat diwariskan ke nilai awalnya saat melewati batas shadow.
<style>
div {
padding: 10px;
background: red;
font-size: 25px;
text-transform: uppercase;
color: white;
}
</style>
<div>
<p>I'm outside the element (big/white)</p>
<my-element>Light DOM content is also affected.</my-element>
<p>I'm outside the element (big/white)</p>
</div>
<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
<style>
:host {
all: initial; /* 1st rule so subsequent properties are reset. */
display: block;
background: white;
}
</style>
<p>my-element: all CSS properties are reset to their
initial value using <code>all: initial</code>.</p>
<slot></slot>
`;
</script>
Menemukan semua elemen kustom yang digunakan oleh halaman
Terkadang, menemukan elemen kustom yang digunakan di halaman akan berguna. Untuk melakukannya, Anda perlu melintasi shadow DOM secara berulang dari semua elemen yang digunakan di halaman.
const allCustomElements = [];
function isCustomElement(el) {
const isAttr = el.getAttribute('is');
// Check for <super-button> and <button is="super-button">.
return el.localName.includes('-') || isAttr && isAttr.includes('-');
}
function findAllCustomElements(nodes) {
for (let i = 0, el; el = nodes[i]; ++i) {
if (isCustomElement(el)) {
allCustomElements.push(el);
}
// If the element has shadow DOM, dig deeper.
if (el.shadowRoot) {
findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
}
}
}
findAllCustomElements(document.querySelectorAll('*'));
Membuat elemen dari <template>
Daripada mengisi shadow root menggunakan .innerHTML, kita dapat menggunakan <template> deklaratif. Template adalah placeholder yang ideal untuk mendeklarasikan struktur
komponen web.
Lihat contoh di "Elemen kustom: membangun komponen web yang dapat digunakan kembali".
Dukungan histori & browser
Jika Anda telah mengikuti komponen web selama beberapa tahun terakhir, Anda akan
mengetahui bahwa Chrome 35+/Opera telah mengirimkan shadow DOM versi lama selama
beberapa waktu. Blink akan terus mendukung kedua versi secara paralel selama beberapa
waktu. Spesifikasi v0 menyediakan metode yang berbeda untuk membuat shadow root
(element.createShadowRoot, bukan element.attachShadow v1). Memanggil
metode yang lebih lama akan terus membuat shadow root dengan semantik v0, sehingga kode v0
yang ada tidak akan rusak.
Jika Anda tertarik dengan spesifikasi v0 lama, lihat artikel html5rocks: 1, 2, 3. Ada juga perbandingan yang bagus tentang perbedaan antara shadow DOM v0 dan v1.
Dukungan browser
Shadow DOM v1 diluncurkan di Chrome 53 (status), Opera 40, Safari 10, dan Firefox 63. Edge telah memulai pengembangan.
Untuk mendeteksi fitur shadow DOM, periksa keberadaan attachShadow:
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
Polyfill
Hingga dukungan browser tersedia secara luas, polyfill shadydom dan shadycss memberi Anda fitur v1. Shady DOM meniru cakupan DOM dari Shadow DOM dan shadycss mem-polyfill properti kustom CSS dan cakupan gaya yang disediakan API native.
Instal polyfill:
bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss
Gunakan polyfill:
function loadScript(src) {
return new Promise(function(resolve, reject) {
const script = document.createElement('script');
script.async = true;
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
loadScript('/bower_components/shadydom/shadydom.min.js')
.then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
.then(e => {
// Polyfills loaded.
});
} else {
// Native shadow dom v1 support. Go to go!
}
Lihat https://github.com/webcomponents/shadycss#usage untuk mengetahui petunjuk tentang cara melakukan shim/scope pada gaya Anda.
Kesimpulan
Untuk pertama kalinya, kita memiliki primitif API yang melakukan pencakupan CSS dan
DOM yang tepat, serta memiliki komposisi yang sebenarnya. Jika digabungkan dengan API komponen web lainnya seperti elemen kustom, shadow DOM menyediakan cara untuk membuat komponen yang benar-benar dienkapsulasi tanpa peretasan atau menggunakan beban lama seperti <iframe>.
Jangan salah paham. Shadow DOM memang rumit. Namun, ini adalah hal yang layak dipelajari. Luangkan waktu untuk mempelajarinya. Pelajari dan ajukan pertanyaan.
Bacaan lebih lanjut
- Perbedaan antara Shadow DOM v1 dan v0
- "Introducing Slot-Based Shadow DOM API" dari Blog WebKit.
- Web Components and the future of Modular CSS oleh Philip Walton
- "Elemen kustom: membuat komponen web yang dapat digunakan kembali" dari WebFundamentals Google.
- Spesifikasi Shadow DOM v1
- Spesifikasi elemen kustom v1
FAQ
Dapatkah saya menggunakan Shadow DOM v1 sekarang?
Ya, dengan polyfill. Lihat Dukungan browser.
Fitur keamanan apa yang disediakan shadow DOM?
Shadow DOM bukan fitur keamanan. Ini adalah alat ringan untuk menentukan cakupan CSS
dan menyembunyikan hierarki DOM dalam komponen. Jika Anda menginginkan batas keamanan yang sebenarnya, gunakan <iframe>.
Apakah komponen web harus menggunakan shadow DOM?
Tidak. Anda tidak perlu membuat komponen web yang menggunakan shadow DOM. Namun, membuat elemen kustom yang menggunakan Shadow DOM berarti Anda dapat memanfaatkan fitur seperti cakupan CSS, enkapsulasi DOM, dan komposisi.
Apa perbedaan antara shadow root terbuka dan tertutup?
Lihat Root bayangan tertutup.