Shadow DOM v1 - Komponen Web Mandiri

Shadow DOM memungkinkan developer web membuat DOM dan CSS yang dikompartementasikan untuk komponen web

Ringkasan

Shadow DOM menghilangkan kerentanan dalam mem-build aplikasi web. Kerentanan ini berasal dari sifat global HTML, CSS, dan JS. Selama bertahun-tahun, kami telah membuat jumlah alat yang sangat banyak 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 ada dan digunakan oleh halaman. Bug halus muncul, spesifikasi CSS menjadi masalah besar (!important semua hal!), pemilih gaya menjadi tidak terkontrol, dan performa dapat terpengaruh. Daftarnya terus bertambah.

Shadow DOM memperbaiki CSS dan DOM. Versi ini memperkenalkan gaya cakupan ke platform web. Tanpa alat atau konvensi penamaan, Anda dapat memaketkan CSS dengan markup, menyembunyikan detail implementasi, dan menulis komponen mandiri di JavaScript vanilla.

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, saat melakukannya, Anda akan 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), DOM bayangan adalah cara Anda menyediakan HTML dan CSS-nya. Kedua API tersebut digabungkan untuk membuat komponen dengan HTML, CSS, dan JavaScript mandiri.

Shadow DOM dirancang sebagai alat untuk mem-build aplikasi berbasis komponen. Oleh karena itu, teknologi ini menghadirkan solusi untuk masalah umum dalam pengembangan web:

  • DOM Terisolasi: DOM komponen bersifat mandiri (misalnya, document.querySelector() tidak akan menampilkan node di DOM bayangan komponen).
  • CSS terbatas: CSS yang ditentukan di dalam shadow DOM dibatasi untuknya. Aturan gaya tidak bocor dan gaya halaman tidak akan tercampur.
  • Komposisi: Mendesain API deklaratif berbasis markup untuk komponen Anda.
  • Menyederhanakan CSS - DOM cakupan berarti Anda dapat menggunakan pemilih CSS sederhana, nama ID/class yang lebih umum, dan tidak perlu khawatir dengan konflik penamaan.
  • Produktivitas - Anggap aplikasi dalam potongan DOM, bukan satu halaman (global) besar.

Demo fancy-tabs

Di seluruh artikel ini, saya akan merujuk ke komponen demo (<fancy-tabs>) dan mereferensikan cuplikan kode darinya. Jika browser Anda mendukung API, Anda akan melihat demo langsung di bawah. Jika tidak, lihat sumber lengkap di GitHub.

Lihat sumber di GitHub

Apa yang dimaksud dengan shadow DOM?

Latar belakang tentang 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 itu sendiri tidak terlalu berguna. Manusia mudah memahami bahasa berbasis teks, tetapi mesin memerlukan 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 live. Pada dasarnya, untuk memahami struktur halaman, browser mengurai HTML (string statis teks) menjadi model data (objek/node). Browser mempertahankan hierarki HTML dengan membuat hierarki node ini: DOM. Hal yang menarik tentang DOM adalah DOM adalah representasi live 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 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>

Semuanya baik dan bagus. Lalu, apa itu shadow DOM?

DOM… dalam bayangan

Shadow DOM hanyalah DOM normal dengan dua perbedaan: 1) cara pembuatan/penggunaannya dan 2) perilakunya sehubungan dengan bagian lain halaman. Biasanya, Anda membuat node DOM dan menambahkannya sebagai turunan elemen lain. Dengan shadow DOM, Anda membuat hierarki DOM tercakup yang dilampirkan ke elemen, tetapi terpisah dari turunan sebenarnya. Subtree cakupan ini disebut shadow tree. Elemen yang dilampirkan adalah host bayangan-nya. Apa pun yang Anda tambahkan di bayangan akan menjadi lokal untuk elemen hosting, termasuk <style>. Inilah cara DOM bayangan mencapai 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 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 bayangan, tetapi Anda juga dapat menggunakan DOM API lainnya. Ini adalah web. Kita memiliki pilihan.

Spesifikasi menentukan daftar elemen yang tidak dapat menghosting hierarki bayangan. Ada beberapa alasan elemen mungkin ada dalam daftar:

  • Browser sudah menghosting shadow DOM internalnya sendiri untuk elemen (<textarea>, <input>).
  • Elemen tidak dapat menghosting DOM bayangan (<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

DOM Bayangan sangat berguna saat membuat elemen kustom. Gunakan shadow DOM untuk memisahkan HTML, CSS, dan JS elemen, sehingga menghasilkan "komponen web".

Contoh - elemen kustom meletakkan 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 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 shadow DOM yang paling tidak dipahami, tetapi mungkin yang paling penting.

Dalam dunia pengembangan web, komposisi adalah cara kita membuat aplikasi, secara deklaratif dari HTML. Elemen penyusun yang berbeda (<div>, <header>, <form>, <input>) digabungkan untuk membentuk aplikasi. Beberapa tag ini bahkan berfungsi satu sama lain. 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 dengan tag tersebut. Misalnya, <select> mengetahui cara merender <option> dan <optgroup> menjadi widget dropdown dan multi-pilih. Elemen <details> merender <summary> sebagai panah yang dapat diluaskan. Bahkan <video> tahu cara menangani turunan tertentu: elemen <source> tidak dirender, tetapi memengaruhi perilaku video. Keren!

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 DOM bayangan komponen. Ini adalah 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>

DOM Bayangan

DOM yang ditulis oleh penulis komponen. Shadow DOM bersifat lokal untuk komponen dan menentukan struktur internalnya, CSS tercakup, dan mengenkapsulasi detail implementasi Anda. Komponen 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 browser yang mendistribusikan light DOM pengguna ke dalam shadow DOM Anda, yang merender produk akhir. Hierarki yang diratakan adalah yang 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>

DOM bayangan menyusun hierarki DOM yang berbeda-beda menggunakan elemen <slot>. Slot adalah placeholder di dalam komponen Anda yang dapat diisi pengguna dengan markup mereka sendiri. Dengan menentukan satu atau beberapa slot, Anda mengundang markup luar untuk dirender di DOM bayangan komponen. Pada dasarnya, Anda mengatakan "Render markup pengguna di sini".

Elemen diizinkan untuk "menyeberang" batas shadow DOM saat <slot> mengundangnya masuk. Elemen ini disebut node terdistribusi. Secara konseptual, node terdistribusi dapat tampak sedikit aneh. Slot tidak memindahkan DOM secara fisik; slot merendernya di lokasi lain di dalam shadow DOM.

Komponen dapat menentukan nol atau beberapa slot di shadow DOM-nya. Slot dapat kosong atau menyediakan konten penggantian. Jika pengguna tidak menyediakan konten light DOM, slot akan merender konten penggantiannya.

<!-- 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 namanya.

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 penasaran, hierarki yang diratakan 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 konfigurasi yang berbeda, tetapi hierarki DOM yang diratakan tetap sama. Kita juga dapat beralih dari <button> ke <h2>. Komponen ini ditulis untuk menangani berbagai jenis turunan… seperti <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

Fitur paling berguna dari shadow DOM adalah CSS tercakup:

  • Pemilih CSS dari halaman luar tidak berlaku di dalam komponen Anda.
  • Gaya yang ditentukan di dalam tidak akan terpotong. Elemen ini dicakup ke elemen host.

Pemilih CSS yang digunakan di dalam DOM bayangan berlaku secara lokal untuk komponen Anda. Dalam prakteknya, ini berarti kita dapat menggunakan nama ID/class umum lagi, tanpa khawatir akan konflik di tempat lain di halaman. Pemilih CSS yang lebih sederhana adalah praktik terbaik di dalam Shadow DOM. Hal ini juga baik untuk performa.

Contoh - gaya yang ditentukan di 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 dicakup ke hierarki bayangan:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Pernah bertanya-tanya bagaimana elemen <select> merender widget multi-pilihan (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 sendiri secara berbeda berdasarkan atribut yang Anda deklarasikan di dalamnya. Komponen web juga dapat menata gayanya sendiri, dengan menggunakan pemilih :host.

Contoh - gaya komponen itu sendiri

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

Satu masalah dengan :host adalah aturan di halaman induk memiliki spesifitas yang lebih tinggi daripada aturan :host yang ditentukan dalam elemen. Artinya, gaya luar menang. Hal ini memungkinkan pengguna mengganti gaya visual tingkat atas Anda dari luar. Selain itu, :host hanya berfungsi dalam konteks root bayangan, sehingga Anda tidak dapat menggunakannya di luar DOM bayangan.

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>

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

DOM bayangan 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 secara fisik tetap berada di tempatnya. Gaya yang diterapkan sebelum distribusi akan terus diterapkan setelah distribusi. Namun, saat didistribusikan, light DOM dapat menggunakan 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 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 mengalahkan gaya yang ditentukan di shadow DOM. Misalnya, jika pengguna menulis pemilih fancy-tabs { width: 500px; }, pemilih tersebut akan menggantikan aturan komponen: :host { width: 650px;}.

Menyesuaikan gaya komponen itu sendiri hanya akan membantu 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 kustom CSS

Pengguna dapat menyesuaikan gaya internal jika penulis komponen menyediakan hook gaya visual menggunakan properti khusus CSS. Secara konsep, ide ini 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 bayangan tertutup (sebaiknya hindari)

Ada ragam lain dari DOM bayangan yang disebut mode "tertutup". Saat Anda membuat hierarki bayangan tertutup, JavaScript di luar tidak akan dapat mengakses DOM internal komponen Anda. Cara kerjanya mirip dengan cara kerja elemen native seperti <video>. JavaScript tidak dapat mengakses DOM bayangan <video> karena browser menerapkan DOM bayangan 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 terpengaruh oleh mode tertutup:

  • Element.assignedSlot / TextNode.assignedSlot menampilkan null
  • Event.composedPath() untuk peristiwa yang terkait dengan elemen di dalam DOM bayangan, menampilkan []

Berikut adalah ringkasan saya tentang alasan Anda tidak boleh membuat komponen web dengan {mode: 'closed'}:

  1. Rasa aman yang artifisial. Tidak ada yang dapat menghentikan penyerang untuk membajak Element.prototype.attachShadow.

  2. Mode tertutup mencegah kode elemen kustom Anda mengakses DOM bayangan sendiri. Itu adalah kegagalan total. Sebagai gantinya, Anda harus menyimpan referensi untuk nanti jika ingin menggunakan hal-hal seperti querySelector(). Hal ini benar-benar 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');
        }
        ...
    });
    
  3. Mode tertutup membuat komponen Anda kurang fleksibel bagi pengguna akhir. Saat mem-build 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 menyesuaikan gaya. Kemampuan untuk mengakses internal komponen sangat membantu. Pada akhirnya, pengguna akan melakukan fork pada komponen Anda, menemukan komponen lain, atau membuat komponen mereka sendiri jika komponen Anda tidak melakukan hal yang mereka inginkan :(

Menggunakan slot di JS

shadow DOM API menyediakan utilitas untuk menggunakan slot dan node terdistribusi. Hal ini berguna saat menulis elemen kustom.

peristiwa slotchange

Peristiwa slotchange diaktifkan saat node yang didistribusikan slot 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, mengetahui elemen yang terkait dengan slot akan berguna. Panggil slot.assignedNodes() untuk menemukan elemen yang dirender slot. Opsi {flatten: true} juga akan menampilkan konten penggantian slot (jika tidak ada node yang didistribusikan).

Sebagai contoh, misalkan shadow DOM Anda terlihat seperti ini:

<slot><b>fallback content</b></slot>
PenggunaanTeleponHasil
<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>]

Slot apa yang ditetapkan untuk elemen?

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 seperti berasal dari komponen, bukan elemen internal dalam DOM bayangan Anda. Beberapa peristiwa bahkan tidak menyebar dari shadow DOM.

Peristiwa yang memang 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 diaktifkan pada node internal dalam hierarki bayangan tidak akan 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 ingat dari model peristiwa DOM bayangan, peristiwa yang diaktifkan di dalam DOM bayangan 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 tingkat shadow DOM yang digunakan (misalnya elemen kustom dalam elemen kustom lain), Anda perlu menyelidiki root bayangan 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 hierarki bayangan:

  • Jika Anda mengklik node di dalam shadow DOM dan node tersebut bukan area yang dapat difokuskan, area pertama yang dapat difokuskan akan difokuskan.
  • Saat node di dalam shadow DOM mendapatkan fokus, :focus berlaku untuk 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

delegatesFocus: perilaku true.

Di atas adalah hasil saat <x-focus> difokuskan (klik pengguna, tab, focus(), dll.), "Teks Shadow DOM 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 <input> internal difokuskan.
delegatesFocus: false dan x-focus
    mendapatkan fokus (misalnya, memiliki tabindex=&#39;0&#39;).
delegatesFocus: false dan <x-focus> mendapatkan fokus (misalnya, memiliki tabindex="0").
delegatesFocus: false dan &#39;Teks Shadow DOM yang Dapat Diklik&#39; diklik (atau area kosong lainnya dalam shadow DOM elemen diklik).
delegatesFocus: false dan "Teks Shadow DOM yang Dapat Diklik" diklik (atau area kosong lainnya dalam DOM bayangan elemen diklik).

Tips & Trik

Selama bertahun-tahun, saya telah mempelajari beberapa hal tentang penulisan komponen web. Saya pikir Anda akan menemukan beberapa tips ini berguna untuk menulis komponen dan men-debug shadow DOM.

Menggunakan pembatasan CSS

Biasanya, tata letak/gaya/cat komponen web cukup mandiri. Gunakan pembatasan 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 diwarisi di shadow DOM. Artinya, elemen tersebut menembus batas shadow DOM secara default. Jika Anda ingin memulai dengan yang baru, gunakan all: initial; untuk mereset gaya yang dapat diwarisi ke nilai awalnya saat melintasi 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

Terkadang ada gunanya menemukan elemen kustom yang digunakan di halaman. Untuk melakukannya, Anda harus menelusuri DOM bayangan secara rekursif 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>

Alih-alih mengisi root bayangan menggunakan .innerHTML, kita dapat menggunakan <template> deklaratif. Template adalah placeholder yang ideal untuk mendeklarasikan struktur komponen web.

Lihat contohnya di "Elemen kustom: membuat komponen web yang dapat digunakan kembali".

Dukungan histori & browser

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

Hingga dukungan browser tersedia secara luas, polyfill shadydom dan shadycss memberi Anda fitur v1. Shady DOM meniru cakupan DOM Shadow DOM dan polyfill shadycss 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/menentukan cakupan gaya Anda.

Kesimpulan

Untuk pertama kalinya, kita memiliki primitif API yang melakukan cakupan CSS yang tepat, cakupan DOM, dan memiliki komposisi yang sebenarnya. Dikombinasikan dengan API komponen web lainnya seperti elemen kustom, DOM bayangan menyediakan cara untuk menulis komponen yang benar-benar dienkapsulasi tanpa hack atau menggunakan bagasi lama seperti <iframe>.

Jangan salah paham. DOM Bayangan tentu saja merupakan hal yang kompleks. Namun, ini adalah teknologi yang layak dipelajari. Luangkan waktu untuk mempelajarinya. Pelajari dan ajukan pertanyaan.

Bacaan lebih lanjut

FAQ

Dapatkah saya menggunakan Shadow DOM v1 sekarang?

Dengan polyfill, ya. 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, menulis elemen kustom yang menggunakan DOM Bayangan berarti Anda dapat memanfaatkan fitur seperti cakupan CSS, enkapsulasi DOM, dan komposisi.

Apa perbedaan antara root bayangan terbuka dan tertutup?

Lihat Akar bayangan tertutup.