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 kerapian membangun aplikasi web. Kerapuhan berasal dari sifat global HTML, CSS, dan JS. Selama bertahun-tahun menemukan angka yang sangat besar dari alat untuk menghindari masalah. Misalnya, saat Anda menggunakan ID/class HTML baru, tidak ada yang tahu apakah nama tersebut akan bertentangan dengan nama yang ada yang digunakan oleh halaman. Bug halus merayap, Kekhususan CSS menjadi masalah besar (!important semuanya!), gaya pemilih menjadi tidak terkendali, dan performa dapat menurun. Daftar lanjutannya.

Shadow DOM memperbaiki CSS dan DOM. Tutorial ini memperkenalkan gaya cakupan ke web terkelola sepenuhnya. Tanpa alat atau konvensi penamaan, Anda dapat memaketkan CSS dengan markup, sembunyikan detail implementasi, dan penulis mandiri komponen di JavaScript vanilla.

Pengantar

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

Anda tidak perlu menulis komponen web yang menggunakan shadow DOM. Tapi ketika Anda melakukannya, Anda memanfaatkan manfaatnya (cakupan CSS, enkapsulasi DOM, komposisi) dan build yang dapat digunakan kembali elemen kustom, yang tangguh, sangat mudah dikonfigurasi, dan sangat mudah digunakan kembali. Jika kustom adalah cara untuk membuat HTML baru (dengan JS API), shadow DOM adalah cara Anda menyediakan HTML dan CSS-nya. Kedua API digabungkan untuk membuat komponen dengan HTML, CSS, dan JavaScript mandiri.

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

  • DOM Terisolasi: DOM komponen bersifat mandiri (mis. document.querySelector() tidak akan menampilkan node dalam shadow DOM komponen).
  • CSS Cakupan: CSS yang didefinisikan di dalam shadow DOM dicakupkan ke sana. Aturan gaya tidak bocor dan gaya laman tidak bocor.
  • Komposisi: Mendesain API deklaratif berbasis markup untuk komponen.
  • Menyederhanakan CSS - DOM terbatas berarti Anda dapat menggunakan pemilih CSS sederhana, id/class generik, dan tidak perlu mengkhawatirkan konflik penamaan.
  • Produktivitas - Pikirkan aplikasi dalam beberapa potongan DOM, bukan satu ukuran besar (global).

fancy-tabs demo

Di sepanjang artikel ini, saya akan merujuk pada komponen demo (<fancy-tabs>) dan merujuk kepada cuplikan kode dari {i>code<i}. Jika browser Anda mendukung API, 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 di DOM

HTML menjadi kekuatan web karena mudah digunakan. Dengan mendeklarasikan beberapa {i>tag<i}, Anda dapat menulis laman dalam hitungan detik yang memiliki baik presentasi maupun struktur. Namun, dengan sendirinya, HTML tidak akan terlalu berguna. Sangat mudah bagi manusia untuk memahami teks- berbasis web, tetapi komputer membutuhkan sesuatu yang lebih banyak. Masukkan Objek Dokumen Model, atau DOM.

Saat browser memuat halaman web, browser melakukan banyak hal menarik. Salah satu tindakannya adalah mengubah HTML penulis menjadi dokumen langsung. Pada dasarnya, untuk memahami struktur halaman, browser mengurai HTML (statis string teks) menjadi model data (objek/node). Browser mempertahankan Hierarki HTML dengan membuat pohon dari simpul-simpul ini: DOM. Yang keren mengenai DOM adalah bahwa DOM merupakan representasi langsung dari halaman Anda. Tidak seperti statis HTML yang kita tulis, node yang dihasilkan browser berisi properti, metode, dan tentu saja... bisa dimanipulasi oleh program! Itu sebabnya kita bisa membuat DOM elemen secara langsung menggunakan JavaScript:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

menghasilkan markup HTML berikut:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

Semua itu baik dan baik. Selanjutnya apa itu shadow DOM?

DOM... dalam bayangan

Shadow DOM hanyalah DOM normal dengan dua perbedaan: 1) cara dibuat/digunakan dan 2) bagaimana perilakunya sehubungan dengan bagian halaman lainnya. Biasanya, Anda membuat DOM {i>node <i}dan menambahkannya sebagai turunan elemen lain. Dengan shadow DOM, Anda membuat hierarki DOM terbatas yang dilampirkan ke elemen, namun terpisah dari anak-anak sungguhan. Subpohon cakupan ini disebut pohon bayangan. Elemen tempatnya melekat adalah host bayangannya. Apa pun yang Anda tambahkan dalam bayangan menjadi lokal ke elemen hosting, termasuk <style>. Seperti inilah shadow DOM mencapai cakupan gaya CSS.

Membuat shadow DOM

Akar bayangan adalah fragmen dokumen yang dilampirkan ke elemen "host". Tindakan melampirkan shadow root adalah cara elemen mendapatkan shadow DOM-nya. Kepada membuat shadow DOM untuk 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 lain Google Cloud Platform. Inilah web. Kita punya pilihan.

Spesifikasi menentukan daftar elemen yang tidak bisa menjadi {i> host<i} pohon bayangan. Ada beberapa alasan mengapa suatu elemen dalam daftar:

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

Misalnya, ini tidak 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 membagi-bagi HTML, CSS, dan JS elemen, sehingga yang menghasilkan "komponen web".

Contoh - elemen kustom melampirkan shadow DOM ke elemen itu sendiri, melakukan enkapsulasi 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> akan dibuat. Hal ini dilakukan di constructor(). Kedua, karena kita membuat root bayangan, aturan CSS di dalam <style> akan dicakup ke <fancy-tabs>.

Komposisi dan slot

Komposisi adalah salah satu fitur yang paling kurang dipahami dari {i>shadow DOM<i}, tetapi dapat dikatakan sebagai yang paling penting.

Dalam dunia pengembangan web kita, komposisi adalah cara kita membangun aplikasi, secara deklaratif dari HTML. Elemen penyusun yang berbeda (<div>, <header>, <form>, <input>) berkumpul untuk membentuk aplikasi. Beberapa tag ini bahkan berfungsi satu sama lain. Komposisi adalah alasan elemen native seperti <select>, <details>, <form>, dan <video> sangat fleksibel. Masing-masing tag tersebut menerima HTML tertentu sebagai anak dan melakukan sesuatu yang spesial dengan mereka. Misalnya, <select> tahu cara merender <option> dan <optgroup> ke dalam dropdown dan pilih beberapa widget. Elemen <details> merender <summary> sebagai panah yang dapat diperluas. Bahkan <video> tahu cara menangani anak-anak tertentu: Elemen <source> tidak dirender, tetapi akan memengaruhi perilaku video. Sungguh ajaib!

Terminologi: light DOM vs. shadow DOM

Komposisi Shadow DOM memperkenalkan sekumpulan dasar-dasar baru di web pengembangan produk. Sebelum membahas lebih lanjut, mari kita standarkan beberapa terminologi sehingga kita berbicara dalam bahasa yang sama.

Light DOM

Markup yang ditulis pengguna komponen Anda. DOM ini berada di luar shadow DOM komponen. Ini adalah turunan sebenarnya 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 mendefinisikan struktur internalnya, CSS bercakupan, dan mengenkapsulasi implementasi Anda spesifikasi pendukung. Model ini juga 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 rata

Hasil browser yang mendistribusikan light DOM pengguna ke dalam shadow Anda DOM, merender produk akhir. Pohon yang diratakan adalah apa yang pada akhirnya Anda lihat di DevTools dan apa yang dirender pada 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>

<slot> elemen

Shadow DOM menyusun hierarki DOM yang berbeda menggunakan elemen <slot>. Slot adalah placeholder di dalam komponen Anda yang dapat diisi pengguna dengan markup Anda sendiri. Dengan menentukan satu atau beberapa slot, Anda mengundang markup luar untuk merender pada shadow DOM komponen Anda. Intinya, Anda mengatakan "Render markup di sini".

Elemen diizinkan "silang" batas shadow DOM saat <slot> mengundang mereka masuk. Elemen ini disebut node terdistribusi. Secara konseptual, node terdistribusi bisa terlihat sedikit aneh. Slot tidak memindahkan DOM secara fisik; mereka merendernya di lokasi lain dalam shadow DOM.

Sebuah komponen dapat mendefinisikan nol atau beberapa slot dalam shadow DOM-nya. Slot boleh kosong atau menyediakan konten penggantian. Jika pengguna tidak menyediakan light DOM konten, 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 pada shadow DOM yang dirujuk pengguna berdasarkan nama.

Contoh - slot dalam 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 itu:

<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, pohon yang diratakan itu 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 pohon DOM yang diratakan tetap sama. Kita juga dapat beralih dari <button> ke <h2>. Komponen ini ditulis untuk menangani berbagai jenis turunan... hanya seperti <select>.

Penataan gaya

Ada banyak opsi untuk menata gaya komponen web. Komponen yang menggunakan bayangan DOM bisa ditata oleh halaman utama, mendefinisikan gayanya sendiri, atau menyediakan hook (di bentuk properti kustom CSS) bagi pengguna untuk mengganti setelan default.

Gaya yang ditentukan komponen

Langsung ke bawah, fitur shadow DOM yang paling berguna adalah CSS cakupan:

  • Pemilih CSS dari halaman luar tidak berlaku di dalam komponen Anda.
  • Gaya yang didefinisikan di dalam tidak akan bocor ke luar. Dia tercakup dalam elemen host.

Pemilih CSS yang digunakan dalam shadow DOM berlaku secara lokal untuk komponen Anda. Di beberapa ini berarti kita bisa menggunakan nama id/class umum lagi, tanpa khawatir konflik di tempat lain di halaman tersebut. Pemilih CSS yang lebih sederhana adalah praktik terbaik di dalam Shadow DOM. Bagus juga untuk meningkatkan performa.

Contoh - gaya yang ditentukan dalam root bayangan bersifat lokal

#shadow-root
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        ...
    }
    #tabs {
        display: inline-flex;
        ...
    }
    </style>
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Stylesheet juga tercakup dalam pohon 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 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 pada token tersebut. Komponen web juga dapat menata gayanya sendiri, dengan menggunakan :host pemilih.

Contoh - komponen yang menata gaya visualnya 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 dari :host aturan yang ditentukan dalam elemen. Yaitu, gaya luar menang. Ini memungkinkan pengguna mengganti gaya visual tingkat atas Anda dari luar. Selain itu, :host hanya berfungsi dalam konteks {i>shadow root<i}, jadi Anda tidak bisa menggunakannya di luar shadow DOM ini.

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 berdasarkan node internal pada {i>host<i}.

<style>
:host {
    opacity: 0.4;
    will-change: opacity;
    transition: opacity 300ms ease-in-out;
}
:host(:hover) {
    opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
}
:host(.blue) {
    color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>

Menata gaya berdasarkan konteks

:host-context(<selector>) cocok dengan komponen jika komponen tersebut atau ancestor-nya cocok dengan <selector>. Penggunaan yang umum untuk ini adalah tema berdasarkan lingkungan sekitar. Misalnya, banyak orang membuat 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 khusus CSS.

Menata gaya node terdistribusi

::slotted(<compound-selector>) cocok dengan node yang didistribusikan ke dalam <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 pembahasan sebelumnya, <slot> tidak memindahkan light DOM pengguna. Kapan node didistribusikan ke dalam <slot>, <slot> akan merender DOM-nya, tetapi {i>node <i}yang secara fisik tetap berada di posisi tersebut. Gaya yang diterapkan sebelum distribusi akan terus berlanjut ke berlaku setelah distribusi. Akan tetapi, ketika light DOM didistribusikan, ia bisa mengambil gaya tambahan (gaya yang didefinisikan 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 membuka panelnya. Hal itu dilakukan dengan memilih {i> node<i} terdistribusi yang memiliki Atribut selected. JS elemen kustom (tidak ditampilkan di sini) menambahkan pada waktu yang tepat.

Menata gaya komponen dari luar

Ada beberapa cara untuk menata gaya komponen dari luar. Paling mudah caranya adalah dengan 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 atas gaya yang ditentukan dalam shadow DOM. Misalnya, jika pengguna menulis pemilih fancy-tabs { width: 500px; }, maka akan terjadi trump 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 dari komponen? Untuk itu, kita perlu CSS khusus properti baru.

Membuat hook gaya menggunakan properti khusus CSS

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

Contoh - <fancy-tabs> memungkinkan pengguna mengganti warna latar belakang:

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

Di dalam shadow DOM-nya:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

Dalam hal ini, komponen akan menggunakan black sebagai nilai latar belakang karena yang disediakan pengguna. Jika tidak, nilainya akan ditetapkan secara default ke #9E9E9E.

Topik lanjutan

Membuat root bayangan tertutup (sebaiknya hindari)

Ada ragam shadow DOM lain yang disebut "closed" mode. Saat Anda membuat pohon bayangan tertutup, di luar JavaScript 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 {i>root shadow root<i} 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 lainnya juga terpengaruh oleh mode tertutup:

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

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

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

  2. Mode tertutup mencegah kode elemen khusus mengakses sendiri shadow DOM. Itu berarti gagal sepenuhnya. Sebagai gantinya, Anda harus menyembunyikan referensi untuk nanti jika Anda ingin menggunakan hal-hal seperti querySelector(). Ini sepenuhnya mengalahkan tujuan awal mode tertutup!

        customElements.define('x-element', class extends HTMLElement {
        constructor() {
        super(); // always call super() first in the constructor.
        this._shadowRoot = this.attachShadow({mode: 'closed'});
        this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
        }
        connectedCallback() {
        // When creating closed shadow trees, you'll need to stash the shadow root
        // for later if you want to use it again. Kinda pointless.
        const wrapper = this._shadowRoot.querySelector('.wrapper');
        }
        ...
    });
    
  3. Mode tertutup membuat komponen Anda kurang fleksibel bagi pengguna akhir. Saat Anda membangun komponen web, akan ada saatnya Anda lupa untuk menambahkan aplikasi baru. Opsi konfigurasi. Kasus penggunaan yang diinginkan pengguna. Tanda umum kita lupa menyertakan sangkutan penataan gaya yang memadai untuk {i>node<i} internal. Dengan mode tertutup, tidak ada cara bagi pengguna untuk mengganti default dan mengubah gaya. Mampu mengakses internal komponen sangat membantu. Pada akhirnya, pengguna akan membagi komponen Anda, mencari yang lain, atau membuat komponen sendiri jika tidak melakukan apa yang mereka inginkan :(

Bekerja dengan slot di JS

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

peristiwa slotchange

Peristiwa slotchange diaktifkan saat node terdistribusi slot berubah. Sebagai misalnya, jika pengguna menambah/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 pada konstruktor elemen Anda.

Elemen apa yang sedang dirender dalam slot?

Terkadang berguna untuk mengetahui elemen apa yang terkait dengan slot. Telepon slot.assignedNodes() untuk menemukan elemen mana yang sedang dirender. Tujuan Opsi {flatten: true} juga akan menampilkan konten penggantian slot (jika tidak ada node sedang didistribusikan).

Sebagai contoh, anggaplah shadow DOM Anda terlihat seperti ini:

<slot><b>fallback content</b></slot>
PenggunaanTeleponHasil
<my-component>teks komponen</my-component> slot.assignedNodes(); [component text]
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes(); []
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

Elemen apa ditetapkan ke slot apa?

Anda juga dapat menjawab pertanyaan sebaliknya. element.assignedSlot memberi tahu di slot komponen mana elemen Anda ditetapkan.

Model peristiwa DOM Bayangan

Bila sebuah kejadian menggelembung ke atas dari shadow DOM, targetnya akan disesuaikan untuk mempertahankan enkapsulasi yang disediakan oleh {i>shadow DOM<i}. Yaitu, acara ditargetkan ulang untuk seolah-olah berasal dari komponen dan bukan dari elemen internal dalam shadow DOM ini. Beberapa kejadian bahkan tidak disebarkan dari shadow DOM.

Peristiwa yang memang 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 pohon bayangan terbuka, memanggil event.composedPath() akan menampilkan array {i>node<i} yang dilintasi oleh peristiwa tersebut.

Menggunakan peristiwa kustom

Peristiwa DOM khusus yang diaktifkan pada node internal di shadow tree tidak keluar dari batas bayangan kecuali jika peristiwa dibuat menggunakan Tanda 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 tersebut di luar {i>shadow root<i} 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 memanggil 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 shadow root dibuat dengan mode:'open' (lihat mode tertutup), Anda juga akan dapat mengakses node internal yang mendapatkan fokus:

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

Jika ada beberapa level shadow DOM yang dimainkan (katakanlah elemen khusus di dalam elemen khusus lainnya), Anda perlu menelusuri akar bayangan secara rekursif untuk temukan activeElement:

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

Opsi lain untuk fokus adalah opsi delegatesFocus: true, yang memperluas perilaku fokus elemen dalam shadow tree:

  • Jika Anda mengeklik simpul di dalam shadow DOM dan simpul itu bukan area yang dapat difokuskan, area pertama yang dapat difokuskan menjadi terfokus.
  • Saat node di dalam shadow DOM mendapatkan fokus, :focus berlaku untuk host di 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, diarahkan ke, focus(), dll.), "Teks DOM Bayangan yang Dapat Diklik" diklik, atau dimensi internal <input> difokuskan (termasuk autofocus).

Jika Anda menyetel delegatesFocus: false, berikut adalah yang akan Anda lihat:

delegasisFocus: false dan input internal difokuskan.
delegatesFocus: false dan <input> internal difokuskan.
delegasisFocus: salah dan fokus x
    menambah fokus (misalnya memiliki tabindex=&#39;0&#39;).
delegatesFocus: false dan <x-focus> menambah fokus (misalnya memiliki tabindex="0").
delegasisFocus: false dan &#39;Teks DOM Bayangan yang Dapat Diklik&#39; sesuai dengan
    diklik (atau area kosong lainnya dalam shadow DOM elemen diklik).
delegatesFocus: false dan "Teks DOM Bayangan yang Dapat Diklik" sesuai dengan diklik (atau area kosong lainnya dalam shadow DOM elemen diklik).

Tips & Trik

Bertahun-tahun saya belajar satu atau dua hal tentang penulisan komponen web. diri berpikir bahwa Anda akan menemukan beberapa kiat ini yang berguna untuk penulisan komponen dan melakukan debug shadow DOM.

Gunakan pembatasan CSS

Biasanya, layout/style/Paint komponen web cukup mandiri. Gunakan Pembatasan CSS di :host untuk performa menang:

<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.) dilanjutkan untuk mewarisi dalam shadow DOM. Artinya, mereka menembus batas shadow DOM dengan secara default. Jika Anda ingin memulai dari slate yang baru, gunakan all: initial; untuk mereset gaya yang dapat diwarisi ke nilai awal ketika 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 menemukan elemen khusus yang digunakan pada laman. Untuk melakukannya, Anda perlu perlu menelusuri shadow DOM secara rekursif dari semua elemen yang digunakan pada laman.

const allCustomElements = [];

function isCustomElement(el) {
    const isAttr = el.getAttribute('is');
    // Check for <super-button> and <button is="super-button">.
    return el.localName.includes('-') || isAttr && isAttr.includes('-');
}

function findAllCustomElements(nodes) {
    for (let i = 0, el; el = nodes[i]; ++i) {
    if (isCustomElement(el)) {
        allCustomElements.push(el);
    }
    // If the element has shadow DOM, dig deeper.
    if (el.shadowRoot) {
        findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
    }
    }
}

findAllCustomElements(document.querySelectorAll('*'));

Membuat elemen dari <template>

Daripada mengisi shadow root menggunakan .innerHTML, kita dapat menggunakan <template>. {i>Template<i} adalah {i>placeholder<i} yang ideal untuk mendeklarasikan struktur komponen web.

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

Sejarah & dukungan browser

Jika Anda telah mengikuti komponen web selama beberapa tahun terakhir, Anda akan perlu diketahui bahwa Chrome 35+/Opera telah menyertakan versi lama shadow DOM untuk beberapa saat. Blink akan terus mendukung kedua versi secara paralel untuk beberapa baik. Spesifikasi v0 menyediakan metode berbeda untuk membuat shadow root (element.createShadowRoot, bukan element.attachShadow v1). Memanggil metode lama terus membuat shadow root dengan semantik v0, jadi v0 kode tidak akan rusak.

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

Dukungan browser

Shadow DOM v1 diluncurkan pada Chrome 53 (status), Opera 40, Safari 10, dan Firefox 63. Tepi telah memulai pengembangan.

Untuk mendeteksi shadow DOM, periksa keberadaan attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyfill

Sampai dukungan browser tersedia secara luas, shadydom dan polyfill shadycss memberi Anda v1 aplikasi baru. Shady DOM meniru cakupan DOM Shadow DOM dan polyfill shadycss Properti khusus 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 mendapatkan petunjuk cara melakukan shim/cakupan gaya Anda.

Kesimpulan

Untuk pertama kalinya, kita memiliki primitif API yang melakukan pelingkupan CSS dengan benar, Cakupan DOM, dan memiliki komposisi sesungguhnya. Dikombinasikan dengan API komponen web lain seperti elemen khusus, shadow DOM menyediakan cara untuk menulis secara benar-benar dienkapsulasi komponen tanpa peretasan atau menggunakan bagasi lama seperti <iframe>.

Jangan salah paham. Shadow DOM jelas merupakan binatang buas yang rumit! Tapi itu binatang buas layak untuk dipelajari. Luangkan waktu untuk mempelajarinya. Pelajari dan ajukan pertanyaan!

Bacaan lebih lanjut

FAQ

Bisakah saya menggunakan Shadow DOM v1 sekarang?

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 pohon DOM di komponen. Jika Anda menginginkan batasan keamanan yang sesungguhnya, gunakan <iframe>.

Apakah komponen web harus menggunakan shadow DOM?

Tidak. Anda tidak perlu membuat komponen web yang menggunakan shadow DOM. Namun, dengan menulis elemen khusus yang menggunakan Shadow DOM, Anda dapat mengambil memanfaatkan fitur seperti pelingkupan CSS, enkapsulasi DOM, dan komposisi.

Apa perbedaan antara akar bayangan terbuka dan tertutup?

Lihat Akar bayangan tertutup.