Shadow DOM v1 - Komponen Web Mandiri

Shadow DOM memungkinkan developer web membuat DOM dan CSS yang terkotak-kotak untuk komponen web

Ringkasan

Shadow DOM menghilangkan kerapahan membangun aplikasi web. Keringkah tersebut berasal dari sifat global HTML, CSS, dan JS. Selama bertahun-tahun, kami telah menemukan sejumlah tools yang sangat banyak untuk mengatasi masalah tersebut. Misalnya, ketika 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 halus merembet, kekhususan CSS menjadi masalah besar (!important semuanya!), pemilih gaya menjadi tidak terkendali, dan performa dapat menurun. Daftarnya terus bertambah.

Shadow DOM memperbaiki CSS dan DOM. Tutorial ini memperkenalkan gaya cakupan ke platform web. Tanpa alat atau konvensi penamaan, Anda dapat menggabungkan CSS dengan markup, menyembunyikan detail penerapan, dan menulis komponen mandiri dalam JavaScript biasa.

Pengantar

Shadow DOM adalah salah satu dari tiga standar Komponen Web: Template HTML, Shadow DOM, dan Elemen kustom. HTML Imports sebelumnya menjadi bagian dari daftar, tetapi sekarang dianggap tidak digunakan lagi.

Anda tidak perlu menulis komponen web yang menggunakan shadow DOM. Namun, saat Anda melakukannya, Anda akan mendapatkan manfaat dari manfaatnya (cakupan CSS, enkapsulasi DOM, komposisi) dan mem-build elemen kustom yang dapat digunakan kembali, yang tangguh, sangat mudah dikonfigurasi, dan sangat dapat digunakan kembali. Jika elemen kustom menjadi cara untuk membuat HTML baru (dengan JS API), shadow DOM menjadi cara Anda menyediakan HTML dan CSS-nya. Kedua API tersebut bergabung untuk membuat komponen dengan HTML, CSS, dan JavaScript mandiri.

Shadow DOM dirancang sebagai alat untuk membangun aplikasi berbasis komponen. Oleh karena itu, situs ini menghadirkan solusi untuk masalah umum dalam pengembangan web:

  • DOM Terisolasi: DOM komponen bersifat mandiri (misalnya, document.querySelector() tidak akan menampilkan node dalam shadow DOM komponen).
  • CSS cakupan: CSS yang didefinisikan di dalam shadow DOM dicakup ke dalamnya. Aturan gaya tidak bocor dan gaya halaman tetap tertata.
  • Komposisi: Desain API deklaratif berbasis markup untuk komponen Anda.
  • CSS Sederhana - DOM dengan cakupan berarti Anda dapat menggunakan pemilih CSS sederhana, nama ID/class yang lebih umum, dan tidak perlu mengkhawatirkan konflik penamaan.
  • Produktivitas - Pikirkan aplikasi dalam beberapa potongan DOM, bukan satu halaman (global) yang besar.

fancy-tabs demo

Dalam artikel ini, saya akan merujuk ke komponen demo (<fancy-tabs>) dan merujuk cuplikan kode dari komponen tersebut. Jika browser Anda mendukung API, Anda akan melihat demo langsungnya tepat di bawah ini. Jika tidak, lihat sumber lengkapnya di GitHub.

Lihat sumber di GitHub

Apa itu shadow DOM?

Latar belakang pada DOM

HTML menjadi kekuatan web karena mudah digunakan. Dengan mendeklarasikan beberapa tag, Anda dapat menulis halaman dalam hitungan detik yang memiliki presentasi maupun struktur. Namun, HTML itu sendiri tidak terlalu berguna. Orang mudah memahami bahasa berbasis teks, tetapi mesin memerlukan lebih dari itu. Masukkan Document Object Model, atau DOM.

Saat browser memuat laman web, hal-hal menarik terjadi. Salah satu tugasnya adalah mengubah HTML penulis menjadi dokumen live. Pada dasarnya, untuk memahami struktur halaman, browser mengurai HTML (string teks statis) menjadi sebuah model data (objek/node). Browser mempertahankan hierarki HTML dengan membuat hierarki node ini: DOM. Yang menarik tentang DOM adalah karena ia merupakan representasi langsung dari halaman Anda. Tidak seperti HTML statis yang kita tulis, node buatan browser berisi properti, metode, dan yang terpenting... 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 sebenarnya shadow DOM?

DOM... dalam bayangan

Shadow DOM hanyalah DOM biasa dengan dua perbedaan: 1) cara pembuatan/penggunaannya dan 2) perilakunya sehubungan dengan bagian halaman lainnya. Biasanya, Anda membuat node DOM dan menambahkannya sebagai turunan dari elemen lain. Dengan shadow DOM, Anda membuat hierarki DOM cakupan yang dilampirkan ke elemen, tetapi terpisah dari turunannya yang sebenarnya. Subhierarki terbatas ini disebut pohon bayangan. Elemen yang melampirkannya adalah host bayangannya. Apa pun yang Anda tambahkan dalam bayangan akan bersifat lokal untuk elemen hosting, termasuk <style>. Seperti inilah cara {i>shadow DOM <i} mencapai pelingkupan gaya CSS.

Membuat shadow DOM

Root bayangan adalah fragmen dokumen yang dilampirkan ke elemen "host". Tindakan melampirkan akar bayangan adalah cara elemen memperoleh 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 shadow root, tetapi Anda juga dapat menggunakan DOM API lainnya. Ini adalah web. Kita punya pilihan.

Spesifikasi ini menentukan daftar elemen yang tidak dapat menghosting pohon bayangan. Ada beberapa alasan suatu elemen berada dalam daftar:

  • Browser sudah menghosting shadow DOM internalnya sendiri untuk elemen (<textarea>, <input>).
  • Tidak masuk akal bagi elemen untuk menghosting shadow DOM (<img>).

Misalnya, perintah berikut tidak akan berfungsi:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

Membuat shadow DOM untuk elemen khusus

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

Contoh - elemen kustom melampirkan shadow DOM ke dirinya sendiri, sehingga 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. Tindakan tersebut dilakukan di constructor(). Kedua, karena kita membuat root bayangan, aturan CSS di dalam <style> akan mencakup <fancy-tabs>.

Komposisi dan slot

Komposisi adalah salah satu fitur yang paling kurang dipahami pada shadow DOM, tetapi bisa dibilang yang paling penting.

Dalam dunia pengembangan web kita, komposisi adalah cara kita membuat aplikasi dari HTML secara deklaratif. Beberapa elemen penyusun (<div>, <header>, <form>, <input>) digabungkan untuk membentuk aplikasi. Beberapa tag ini bahkan saling bekerja sama. Komposisi adalah alasan elemen native seperti <select>, <details>, <form>, dan <video> sangat fleksibel. Masing-masing tag tersebut menerima HTML tertentu sebagai turunan dan melakukan sesuatu yang istimewa dengannya. Misalnya, <select> mengetahui cara merender <option> dan <optgroup> menjadi widget dropdown dan widget 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 sekumpulan dasar-dasar baru dalam pengembangan web. Sebelum melangkah lebih jauh, mari kita standarkan beberapa terminologi agar kita berbicara dalam istilah yang sama.

Light DOM

Markup yang ditulis pengguna komponen Anda. DOM ini berada di luar shadow DOM komponen. Inilah turunan aktual 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 menentukan struktur internalnya, CSS cakupan, dan mengenkapsulasi detail implementasi Anda. Editor 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 light DOM pengguna ke dalam shadow DOM Anda, yang merender produk akhir. Flattened tree adalah yang pada akhirnya Anda lihat di DevTools dan apa 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 bersama-sama 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 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 bisa terlihat agak aneh. Slot tidak secara fisik memindahkan DOM; mereka merendernya di lokasi lain dalam shadow DOM.

Komponen dapat mendefinisikan nol atau beberapa slot dalam shadow DOM-nya. Slot dapat kosong atau menyediakan konten penggantian. Jika pengguna tidak memberikan 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 spesifik dalam shadow DOM Anda 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 berikut:

<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, flattened tree 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 yang dilakukan <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 menggantikan default.

Gaya yang ditentukan komponen

Berikut adalah fitur shadow DOM yang paling berguna adalah CSS cakupan:

  • Pemilih CSS dari halaman luar tidak berlaku di dalam komponen Anda.
  • Gaya yang ditentukan di dalam tidak akan bocor keluar. Class ini akan dicakup dalam elemen host.

Pemilih CSS yang digunakan dalam shadow DOM diterapkan secara lokal ke komponen Anda. Pada praktiknya, hal ini berarti kita dapat menggunakan kembali nama id/class umum, tanpa khawatir terjadi konflik di halaman lain. 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 dicakupkan ke pohon bayangan:

#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 multi-pilihan (bukan menu 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. Komponen web juga dapat menata gaya dirinya 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 kesalahan pada :host adalah aturan di halaman induk memiliki kekhususan 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 shadow root, 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 tepat bagi komponen Anda untuk mengenkapsulasi perilaku yang bereaksi terhadap interaksi pengguna atau status atau node 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 atau salah satu ancestornya cocok dengan <selector>. Penggunaan umum untuk hal ini adalah penerapan tema berdasarkan sekeliling 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 penerapan 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 <slot>.

Katakanlah kita telah membuat komponen lencana 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 light DOM pengguna. Jika node didistribusikan ke dalam <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 light DOM didistribusikan, light DOM dapat mengambil 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 menebalkan pilihan mereka dan menampilkan panelnya. Hal itu 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 ditentukan 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. Tetapi apa yang terjadi jika Anda ingin menata gaya internal komponen? Untuk itu, kita memerlukan properti khusus CSS.

Membuat sangkutan gaya menggunakan properti khusus CSS

Pengguna dapat mengubah gaya internal jika penulis komponen menyediakan hook penataan gaya menggunakan properti kustom CSS. Secara konseptual, idenya mirip dengan <slot>. Anda membuat "placeholder gaya" untuk diganti oleh pengguna.

Contoh - <fancy-tabs> mengizinkan 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 (harus dihindari)

Ada ragam shadow DOM lain yang disebut mode "tertutup". Saat Anda membuat shadow tree tertutup, JavaScript luar tidak akan dapat mengakses DOM internal komponen Anda. 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 pohon bayangan tertutup:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

API lain juga dipengaruhi oleh mode tertutup:

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

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

  1. Rasa keamanan semu. Tidak ada yang dapat menghentikan penyerang membajak Element.prototype.attachShadow.

  2. Mode tertutup mencegah kode elemen kustom Anda mengakses shadow DOM-nya sendiri. Itu berarti gagal total. Sebagai gantinya, Anda harus menyembunyikan referensi untuk nanti jika ingin menggunakan hal-hal seperti querySelector(). Hal ini sepenuhnya menggagalkan 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');
        }
        ...
    });
    
  3. Mode tertutup membuat komponen Anda kurang fleksibel bagi pengguna akhir. Saat membuat komponen web, akan ada saatnya Anda lupa menambahkan fitur. Opsi konfigurasi. Kasus penggunaan yang diinginkan pengguna. Contoh umumnya adalah lupa menyertakan hook penataan gaya yang memadai untuk node internal. Dengan mode tertutup, tidak ada cara bagi pengguna untuk mengganti default dan mengubah gaya. Dapat mengakses bagian internal komponen akan sangat membantu. Pada akhirnya, pengguna akan melakukan fork komponen Anda, mencari yang lain, atau membuatnya sendiri jika komponen tersebut tidak melakukan apa yang mereka inginkan :(

Bekerja dengan slot di JS

Shadow DOM API menyediakan utilitas untuk menggunakan slot dan node terdistribusi. Ini berguna saat menulis elemen khusus.

peristiwa slotchange

Peristiwa slotchange aktif 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 di konstruktor elemen.

Elemen apa saja yang sedang dirender dalam slot?

Kadang-kadang ada gunanya mengetahui elemen apa yang terkait dengan slot. 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>
PenggunaanPanggilan TeleponHasil
<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>]

Ke slot manakah elemen ditetapkan?

Anda juga bisa menjawab pertanyaan sebaliknya. element.assignedSlot memberi tahu Anda slot komponen mana yang ditetapkan untuk elemen Anda.

Model peristiwa DOM Bayangan

Saat muncul balon dari shadow DOM, targetnya akan disesuaikan untuk mempertahankan enkapsulasi yang disediakan shadow DOM. Artinya, peristiwa ditargetkan ulang agar seolah-olah berasal dari komponen, bukan dari elemen internal dalam shadow DOM Anda. Beberapa kejadian bahkan tidak menyebarkan shadow DOM.

Peristiwa yang benar-benar melewati 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
  • Peristiwa Tarik: dragstart, drag, dragend, drop, dll.

Tips

Jika hierarki bayangan terbuka, pemanggilan event.composedPath() akan menampilkan array node yang dilalui peristiwa.

Menggunakan peristiwa kustom

Peristiwa DOM kustom yang diaktifkan pada node internal dalam pohon bayangan tidak muncul dari batas bayangan 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 shadow root 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 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 dapat mengakses node internal yang mendapatkan fokus:

document.activeElement.shadowRoot.activeElement // only works with open mode.

Jika ada beberapa level shadow DOM (misalnya elemen kustom dalam elemen kustom lain), Anda harus secara rekursif menelusuri 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 fokus adalah opsi delegatesFocus: true, yang memperluas perilaku fokus elemen dalam hierarki bayangan:

  • Jika Anda mengklik node dalam shadow DOM dan node tersebut bukan area yang dapat difokuskan, area pertama yang dapat difokuskan akan menjadi 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

delegasisFocus: perilaku sebenarnya.

Di atas adalah hasil saat <x-focus> difokuskan (klik pengguna, tab ke dalam, 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:

delegasisFocus: false dan input internal difokuskan.
delegatesFocus: false dan <input> internal difokuskan.
delegasisFocus: false dan x-focus
 mendapatkan fokus (misalnya memiliki tabindex=&#39;0&#39;).
delegatesFocus: false dan <x-focus> mendapatkan fokus (misalnya, memiliki tabindex="0").
delegasisFocus: false dan &#39;Clickable Shadow DOM text&#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 shadow DOM elemen diklik).

Tips & Trik

Selama bertahun-tahun saya telah belajar satu atau dua hal tentang penulisan komponen web. Menurut Anda, tips ini berguna untuk penulisan komponen dan melakukan debug shadow DOM.

Menggunakan pembatasan CSS

Biasanya, tata letak/gaya/paint komponen web cukup mandiri. Gunakan pembatasan CSS di :host untuk perf win:

<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.) akan terus diwarisi dalam shadow DOM. Artinya, mereka menembus batas shadow DOM secara default. Jika Anda ingin memulai dari slate baru, gunakan all: initial; untuk mereset gaya yang dapat diwariskan ke nilai awal saat gaya tersebut 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 suatu halaman

Kadang-kadang ada gunanya juga menemukan elemen khusus yang digunakan di laman. Caranya, Anda harus berulang-ulang melintasi 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>

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

Lihat contoh dalam "Elemen kustom: membuat 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 lama mengirimkan versi lama shadow DOM. 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 shadow root dengan semantik v0, sehingga kode v0 yang ada tidak akan rusak.

Jika Anda tertarik dengan spesifikasi v0 lama, lihat artikel html5Rock: 1, 2, 3. Ada juga perbandingan yang bagus tentang perbedaan antara shadow DOM v0 dan v1.

Dukungan browser

Shadow DOM v1 tersedia 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;

Isi Ulang

Sebelum dukungan browser tersedia secara luas, polyfill shadydom dan shadycss akan memberi Anda fitur v1. Shady DOM meniru pelingkupan DOM dari Shadow DOM dan properti khusus CSS polyfill shadycss dan pelingkupan 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 mendapatkan petunjuk cara melakukan shim/mencakup gaya Anda.

Kesimpulan

Untuk pertama kalinya, kita memiliki primitif API yang melakukan pelingkupan CSS dan pelingkupan DOM yang tepat, serta memiliki komposisi sesungguhnya. Dikombinasikan dengan API komponen web lainnya seperti elemen kustom, 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! Tapi Anda layak dipelajari. Luangkan waktu untuk mempelajarinya. Pelajari dan ajukan pertanyaan!

Bacaan lebih lanjut

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 ringan untuk mencakup CSS dan menyembunyikan hierarki DOM di komponen. Jika Anda menginginkan batas keamanan 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 Shadow DOM berarti Anda dapat memanfaatkan fitur seperti cakupan CSS, enkapsulasi DOM, dan komposisi.

Apa perbedaan antara root bayangan terbuka dan tertutup?

Lihat Root bayangan tertutup.