Shadow DOM memungkinkan developer web membuat DOM dan CSS yang terkategori untuk komponen web
Ringkasan
Shadow DOM menghilangkan kerentanan dalam membuat aplikasi web. Keringkihan tersebut bersumber dari sifat global HTML, CSS, dan JS. Selama bertahun-tahun, kami telah
membuat begitu banyak jumlah
alat
untuk mengatasi masalah. Misalnya, saat Anda menggunakan id/class HTML baru,
Anda tidak akan diberi tahu apakah id/class tersebut akan mengalami konflik dengan nama yang ada yang digunakan oleh halaman.
Bug halus muncul,
kekhususan CSS menjadi masalah besar (!important
semua hal), pemilih
gaya berkembang tidak terkontrol, dan
performa dapat terpengaruh. Daftarnya
terus bertambah.
Shadow DOM memperbaiki CSS dan DOM. API ini memperkenalkan gaya bercakupan ke platform web. Tanpa alat atau konvensi penamaan, Anda dapat memaketkan CSS dengan markup, menyembunyikan detail implementasi, dan menulis komponen mandiri penulis dalam JavaScript biasa.
Pengantar
Shadow DOM adalah salah satu dari tiga standar Komponen Web: Template HTML, Shadow DOM, dan Elemen kustom. Impor HTML sebelumnya merupakan bagian dari daftar, tetapi kini dianggap tidak digunakan lagi.
Anda tidak perlu menulis komponen web yang menggunakan shadow DOM. Namun, jika melakukannya, Anda akan memanfaatkan manfaatnya (pelingkupan CSS, enkapsulasi DOM, komposisi) dan membuat elemen kustom yang dapat digunakan kembali, yang tangguh, sangat mudah dikonfigurasi, dan sangat mudah digunakan kembali. Jika elemen khusus menjadi cara untuk membuat HTML baru (dengan JS API), shadow DOM adalah cara Anda menyediakan HTML dan CSS-nya. Kedua API bergabung untuk membuat komponen dengan HTML, CSS, dan JavaScript mandiri.
Shadow DOM dirancang sebagai alat untuk mem-build aplikasi berbasis komponen. Oleh karena itu, ini memberikan 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 bercakupan: CSS yang didefinisikan di dalam shadow DOM akan menjadi cakupannya. Aturan gaya tidak bocor dan gaya halaman tidak terpengaruh (bleed in).
- Komposisi: Merancang API deklaratif yang berbasis markup untuk komponen Anda.
- Menyederhanakan CSS - DOM bercakupan berarti Anda dapat menggunakan pemilih CSS sederhana, nama id/class yang lebih generik, dan tidak perlu khawatir dengan konflik penamaan.
- Produktivitas - Pertimbangkan aplikasi dalam beberapa potongan DOM, bukan satu halaman (global) yang besar.
Demo fancy-tabs
Sepanjang artikel ini, saya akan merujuk ke komponen demo (<fancy-tabs>
)
dan merujuk cuplikan kode dari sana. Jika browser Anda mendukung API, Anda
akan melihat demonya langsung di bawah ini. Jika tidak, lihat sumber lengkapnya di GitHub.
Apa yang dimaksud dengan shadow DOM?
Latar belakang DOM
HTML menjadi kekuatan web karena mudah digunakan. Dengan mendeklarasikan beberapa tag, Anda dapat menulis halaman dalam hitungan detik yang memiliki presentasi dan struktur. Namun, HTML itu sendiri tidaklah seberguna itu. Orang mudah memahami bahasa berbasis teks, tetapi mesin memerlukan lebih dari itu. Masuk ke Document Object Model, atau DOM.
Saat browser memuat halaman web, ia melakukan banyak hal menarik. Salah satu hal yang dilakukannya adalah mengubah HTML penulis menjadi dokumen live. Pada dasarnya, untuk memahami struktur halaman, browser mem-parse HTML (string teks statis) ke dalam model data (objek/simpul). Browser mempertahankan hierarki HTML dengan membuat hierarki node ini: DOM. Yang keren tentang DOM adalah karena ia merupakan representasi langsung dari halaman Anda. Tidak seperti HTML statis yang kita tulis, node yang dihasilkan browser berisi properti, metode, dan yang paling penting, 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 memang bagus. Lalu, apa itu shadow DOM?
DOM… dalam bayangan
Shadow DOM hanyalah DOM biasa dengan dua perbedaan: 1) cara pembuatan/penggunaannya dan
2) cara perilakunya sehubungan dengan bagian halaman selebihnya. Biasanya, Anda membuat node DOM dan menambahkannya sebagai turunan elemen lain. Dengan shadow DOM, Anda membuat hierarki DOM bercakupan yang dilampirkan ke elemen, tetapi terpisah dari turunannya yang sebenarnya. Subpohon bercakupan ini disebut pohon bayangan. Elemen
tempat melampirkannya adalah host bayangan. Apa pun yang Anda tambahkan dalam bayangan akan bersifat lokal untuk elemen hosting, termasuk <style>
. Seperti inilah cara shadow DOM
menghasilkan cakupan gaya CSS.
Membuat shadow DOM
Root bayangan adalah fragmen dokumen yang dilampirkan ke elemen "host".
Tindakan melampirkan root bayangan adalah cara elemen mendapatkan shadow DOM-nya. Untuk
membuat shadow DOM bagi sebuah 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 bayangan, tetapi Anda juga dapat menggunakan API DOM
lainnya. Inilah web. Kita punya pilihan.
Spesifikasi menentukan daftar elemen yang tidak dapat menghosting hierarki bayangan. Ada beberapa alasan suatu elemen berada 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 membagi-bagi 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. Yang pertama adalah bahwa elemen kustom membuat shadow DOM-nya sendiri saat instance <fancy-tabs>
dibuat. Hal ini dilakukan di constructor()
. Kedua, karena kita membuat
root bayangan, aturan CSS di dalam <style>
akan dicakupkan ke <fancy-tabs>
.
Komposisi dan slot
Komposisi adalah salah satu fitur yang paling kurang dipahami pada shadow DOM, tetapi mungkin menjadi yang paling penting.
Dalam dunia pengembangan web kita, 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 saling
berfungsi. Komposisilah yang membuat elemen native seperti <select>
,
<details>
, <form>
, dan <video>
jadi begitu fleksibel. Masing-masing tag tersebut menerima
HTML tertentu sebagai turunan dan melakukan sesuatu yang spesial dengannya. Misalnya,
<select>
mengetahui cara merender <option>
dan <optgroup>
menjadi widget dropdown dan
multi-pilih. Elemen <details>
merender <summary>
sebagai
panah yang dapat diperluas. Bahkan <video>
mengetahui cara menangani turunan tertentu:
elemen <source>
tidak dirender, tetapi memengaruhi perilaku video.
Betapa ajaib!
Terminologi: light DOM vs. shadow DOM
Komposisi Shadow DOM memperkenalkan sekumpulan dasar-dasar baru dalam pengembangan web. Sebelum melangkah lebih jauh, mari kita standardkan beberapa terminologi agar kita berbicara dalam bahasa yang sama.
Light DOM
Markup yang ditulis pengguna komponen Anda. DOM ini berada di luar shadow DOM komponen. Inilah turunan sebenarnya dari elemen.
<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 mendefinisikan struktur internalnya, CSS bercakupan, dan mengenkapsulasi detail implementasi Anda. Ia 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 light DOM pengguna ke dalam shadow DOM Anda, yang merender produk akhir. Flattened tree 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 menggabungkan berbagai hierarki DOM menggunakan elemen <slot>
.
Slot adalah placeholder di dalam komponen Anda yang dapat diisi oleh pengguna dengan markup mereka sendiri. Dengan menentukan satu atau beberapa slot, Anda mengundang markup luar untuk merender
dalam shadow DOM 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 boleh jadi terlihat agak ganjil. Slot tidak secara fisik memindah DOM; mereka
merendernya di lokasi lain dalam shadow DOM.
Sebuah komponen dapat menentukan nol atau beberapa slot dalam shadow DOM-nya. Slot dapat kosong atau menyediakan konten penggantian. Jika pengguna tidak menyediakan 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 spesifik dalam shadow DOM Anda yang dirujuk pengguna melalui 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>
Dan jika Anda ingin tahu, hierarki yang dipadatkan 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 diratakan tetap sama. Kita juga dapat beralih dari <button>
ke
<h2>
. Komponen ini ditulis untuk menangani berbagai jenis turunan… seperti
yang dilakukan <select>
.
Penataan gaya
Ada banyak opsi untuk menata gaya komponen web. Komponen yang menggunakan DOM bayangan dapat diberi gaya oleh halaman utama, menentukan gayanya sendiri, atau menyediakan hook (dalam bentuk properti khusus CSS) bagi pengguna untuk mengganti default.
Gaya yang ditentukan komponen
Warisan fitur paling berguna dari shadow DOM adalah CSS bercakupan:
- Pemilih CSS dari halaman luar tidak berlaku di dalam komponen Anda.
- Gaya yang ditentukan di dalam tidak akan bocor keluar (bleed out). Gaya itu akan menjadi cakupan elemen host.
Pemilih CSS yang digunakan di dalam shadow DOM berlaku secara lokal untuk komponen Anda. Pada prakteknya, ini berarti kita dapat menggunakan lagi nama-nama id/class, tanpa khawatir terjadi konflik di halaman. Pemilih CSS yang lebih sederhana adalah praktik terbaik dalam Shadow DOM. Kinerjanya juga bagus.
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 dijadikan cakupan pohon bayangan:
#shadow-root
<link rel="stylesheet" href="styles.css">
<div id="tabs">
...
</div>
<div id="panels">
...
</div>
Pernahkah membayangkan bagaimana elemen <select>
merender widget multi-pilih (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 sendirinya secara berbeda berdasarkan atribut yang Anda
deklarasikan padanya. Komponen web juga dapat menata gayanya sendiri, dengan menggunakan pemilih
:host
.
Contoh - komponen yang menata gaya untuk dirinya sendiri
<style>
:host {
display: block; /* by default, custom elements are display: inline */
contain: content; /* CSS containment FTW. */
}
</style>
Satu gotcha dengan :host
adalah bahwa aturan di halaman induk memiliki kekhususan yang lebih tinggi
daripada aturan :host
yang ditentukan dalam elemen. Artinya, gaya luar yang menang. Hal ini
memungkinkan pengguna mengganti gaya visual tingkat atas dari luar. Selain itu, :host
hanya berfungsi dalam konteks root bayangan, sehingga Anda tidak dapat menggunakannya di luar
shadow DOM.
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 pada interaksi pengguna atau status atau simpul internal gaya 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>
Gaya visual 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 tema berdasarkan
lingkungan komponen. Misalnya, banyak orang mengatur 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
.darktheme
:
:host-context(.darktheme) {
color: white;
background: black;
}
:host-context()
dapat berguna untuk pengaturan tema, tetapi pendekatan yang lebih baik adalah
membuat hook gaya menggunakan properti khusus 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 bahasan sebelumnya, <slot>
tidak memindahkan light DOM milik pengguna. Saat
node didistribusikan ke dalam <slot>
, <slot>
akan merender DOM-nya, tetapi
node secara fisik tetap diam. Gaya yang diterapkan sebelum distribusi akan terus
diterapkan setelah distribusi. Namun, saat didistribusikan, light DOM dapat
menerima gaya tambahan (gaya 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 mencetak tebal pilihan mereka
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 menang dari gaya yang didefinisikan dalam shadow DOM. Misalnya,
jika pengguna menulis pemilih fancy-tabs { width: 500px; }
, pemilih tersebut akan mengalahkan
aturan komponen: :host { width: 650px;}
.
Menata gaya komponen itu sendiri hanya akan membawa Anda sejauh ini. Namun, apa yang terjadi jika Anda ingin menata gaya internal komponen? Untuk itu, kita memerlukan properti kustom CSS.
Membuat hook gaya menggunakan properti khusus CSS
Pengguna dapat menyesuaikan gaya internal jika penulis komponen menyediakan hook gaya
menggunakan properti khusus CSS. Secara konseptual, idenya mirip dengan
<slot>
. Anda dapat 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 telah menyediakannya. Jika tidak, nilai defaultnya adalah #9E9E9E
.
Topik lanjutan
Membuat root bayangan tertutup (sebaiknya dihindari)
Ada ragam shadow DOM lain yang disebut mode "tertutup". Saat Anda membuat
pohon bayangan 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 root bayangan mode tertutup.
Contoh - membuat hierarki bayangan tertutup:
const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div
API lainnya juga dipengaruhi oleh mode tertutup:
Element.assignedSlot
/TextNode.assignedSlot
menampilkannull
Event.composedPath()
untuk peristiwa yang terkait dengan elemen di dalam shadow DOM, akan menampilkan []
Berikut adalah ringkasan mengapa Anda jangan sampai membuat komponen web dengan
{mode: 'closed'}
:
Rasa keamanan semu. Tidak ada yang dapat menghentikan penyerang untuk membajak
Element.prototype.attachShadow
.Mode tertutup mencegah kode elemen kustom Anda mengakses DOM bayangan-nya sendiri. Itu berarti gagal total. Sebagai gantinya, Anda harus menyembunyikan referensi untuk nanti jika ingin menggunakan hal-hal seperti
querySelector()
. Hal ini sepenuhnya mengalahkan tujuan awal dari 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 mem-build komponen web, ada saatnya Anda lupa menambahkan fitur. Opsi konfigurasi. Kasus penggunaan yang diinginkan pengguna. Contoh umum adalah lupa menyertakan hook gaya visual yang memadai untuk node internal. Dengan mode tertutup, tidak ada cara bagi pengguna untuk mengganti default dan memodifikasi gaya. Kemampuan mengakses internal komponen akan sangat membantu. Pada akhirnya, pengguna akan membuat fork komponen Anda, menemukan komponen lain, atau membuat komponen mereka sendiri jika komponen tersebut tidak melakukan apa yang mereka inginkan :(
Menggunakan slot di JS
Shadow DOM API menyediakan utilitas untuk menggunakan slot dan node terdistribusi. Ini sudah siap pakai saat menulis elemen khusus.
peristiwa slotchange
Peristiwa slotchange
diaktifkan saat node terdistribusi slot berubah. Misalnya, jika pengguna menambahkan/menghapus turunan dari light DOM.
const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
console.log('light dom children changed!');
});
Untuk memantau jenis perubahan lain pada light DOM, Anda dapat menyiapkan
MutationObserver
dalam konstruktor elemen.
Elemen apa saja yang sedang dirender dalam slot?
Terkadang ada gunanya mengetahui elemen apa saja yang terkait dengan sebuah slot. Panggil
slot.assignedNodes()
untuk menemukan elemen apa saja yang sedang dirender slot. Opsi
{flatten: true}
juga akan menampilkan konten penggantian slot (jika tidak ada node
yang didistribusikan).
Sebagai contoh, anggaplah shadow DOM Anda seperti ini:
<slot><b>fallback content</b></slot>
Penggunaan | Telepon | Hasil |
---|---|---|
<my-component>component text</my-component> | slot.assignedNodes(); |
[component text] |
<my-component></my-component> | slot.assignedNodes(); |
[] |
<my-component></my-component> | slot.assignedNodes({flatten: true}); |
[<b>fallback content</b>] |
Ke slot manakah elemen ditetapkan?
Anda juga dapat menjawab pertanyaan sebaliknya. element.assignedSlot
memberi tahu
Anda slot komponen mana yang ditetapkan untuk elemen Anda.
Model peristiwa Shadow DOM
Saat peristiwa muncul dari shadow DOM, targetnya akan disesuaikan untuk mempertahankan enkapsulasi yang disediakan shadow DOM. Artinya, peristiwa ditargetkan ulang agar terlihat seolah berasal dari komponen, bukan elemen internal dalam shadow DOM Anda. Beberapa peristiwa bahkan tidak menyebarkan 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 hierarki bayangan terbuka, memanggil event.composedPath()
akan menampilkan array
node yang dilalui peristiwa.
Menggunakan peristiwa kustom
Peristiwa DOM kustom yang dipicu pada node internal di hierarki bayangan tidak
menggelembung keluar dari batas bayangan kecuali jika peristiwa 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 bayangan 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 mengingat kembali 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 root bayangan:
<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 root bayangan
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 tingkatan shadow DOM (misalnya elemen kustom dalam elemen kustom lain), Anda harus secara rekursif masuk lebih dalam ke root bayangan 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 mendapatkan 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,
:focus
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, dimasukkan dalam tab,
focus()
, dll.), "Teks Shadow DOM yang dapat diklik" diklik, atau <input>
internal
difokuskan (termasuk autofocus
).
Jika Anda menyetel delegatesFocus: false
, berikut yang akan Anda lihat sebagai gantinya:
Tips & Trik
Setelah bertahun-tahun saya belajar suatu atau beberapa hal tentang penulisan komponen web. Menurut saya, beberapa tips ini akan berguna untuk penulisan komponen dan proses debug shadow DOM.
Menggunakan pembatasan CSS
Biasanya, tata letak/gaya/cat komponen web benar-benar mandiri. Gunakan
pengekangan CSS di :host
untuk peningkatan
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, mereka 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 bayangan.
<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
Kadang-kadang ada gunanya juga menemukan elemen khusus yang digunakan di laman. Untuk melakukannya, Anda harus berulang-ulang menyusuri shadow DOM 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>
Sebagai ganti mengisi root bayangan menggunakan .innerHTML
, kita dapat menggunakan <template>
deklaratif. Template adalah placeholder ideal untuk mendeklarasikan struktur
komponen web.
Lihat contoh di "Elemen khusus: membangun komponen web yang dapat digunakan kembali".
Dukungan histori & browser
Jika Anda mengikuti komponen web selama beberapa tahun terakhir, Anda akan
mengetahui bahwa Chrome 35+/Opera telah menyertakan versi lama shadow DOM untuk
sekian lama. Blink akan terus mendukung kedua versi secara paralel untuk beberapa
waktu. Spesifikasi v0 menyediakan metode berbeda untuk membuat root bayangan
(element.createShadowRoot
, bukan element.attachShadow
v1). Memanggil
metode lama akan terus membuat root bayangan 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 bagus tentang perbedaan antara shadow DOM v0 dan v1.
Dukungan browser
Shadow DOM v1 dikirimkan di Chrome 53 (status), Opera 40, Safari 10, dan Firefox 63. Edge telah memulai pengembangan.
Untuk mendeteksi shadow DOM, periksa keberadaan attachShadow
:
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
Polyfill
Sebelum dukungan browser tersedia secara luas, polyfill shadydom dan shadycss memberi Anda fitur v1. Shady DOM meniru cakupan DOM dari Shadow DOM dan properti khusus CSS polyfill shadycss 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 petunjuk tentang cara melakukan shim/mencakup gaya Anda.
Kesimpulan
Untuk pertama kalinya, kita memiliki primitif API yang melakukan cakupan CSS dan cakupan DOM dengan benar, serta memiliki komposisi yang sebenarnya. Digabung dengan API komponen web lain
seperti elemen khusus, shadow DOM menyediakan cara untuk menulis komponen
yang benar-benar dienkapsulasi tanpa hack atau menggunakan bagasi lama seperti <iframe>
.
Jangan salah paham. Shadow DOM tentu saja hewan buas yang 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 khusus: membangun komponen web yang dapat digunakan kembali" dari WebFundamentals Google.
- Spesifikasi Shadow DOM v1
- Spesifikasi elemen khusus v1
FAQ
Dapatkah saya menggunakan Shadow DOM v1 saat ini?
Ya, dengan polyfill. Lihat Dukungan browser.
Fitur keamanan apa yang disediakan shadow DOM?
Shadow DOM bukanlah fitur keamanan. Ini adalah alat bantu ringan untuk menentukan cakupan CSS
dan menyembunyikan hierarki DOM di komponen. Jika Anda menginginkan batas keamanan yang sebenarnya,
gunakan <iframe>
.
Apakah komponen web harus menggunakan shadow DOM?
Tidak. Anda tidak harus membuat komponen web yang menggunakan shadow DOM. Namun, dengan menulis elemen khusus yang menggunakan Shadow DOM, Anda dapat memanfaatkan fitur seperti cakupan CSS, enkapsulasi DOM, dan komposisi.
Apa perbedaan antara root bayangan terbuka dan tertutup?
Lihat Akar bayangan tertutup.