DOM Bayangan Deklaratif

Shadow DOM deklaratif adalah fitur platform web standar, yang telah didukung di Chrome mulai versi 90. Perlu diperhatikan bahwa spesifikasi untuk fitur ini berubah pada tahun 2023 (termasuk penggantian nama shadowroot menjadi shadowrootmode), dan versi standar terbaru dari semua bagian fitur yang hadir di Chrome versi 124.

Dukungan Browser

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 123.
  • Safari: 16.4.

Sumber

Shadow DOM adalah salah satu dari tiga standar Komponen Web, yang dilengkapi dengan template HTML dan Elemen Kustom. Shadow DOM menyediakan cara untuk mencakup gaya CSS ke subpohon DOM tertentu dan mengisolasi subpohon tersebut dari bagian lain dokumen. Elemen <slot> memberi kita cara untuk mengontrol tempat turunan Elemen Kustom harus disisipkan dalam Shadow Tree-nya. Kombinasi fitur-fitur ini memungkinkan sebuah sistem untuk membangun komponen mandiri dan dapat digunakan kembali yang terintegrasi secara mulus ke dalam aplikasi yang ada seperti elemen HTML bawaan.

Sampai sekarang, satu-satunya cara untuk menggunakan Shadow DOM adalah dengan membangun akar bayangan menggunakan JavaScript:

const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';

API imperatif seperti ini berfungsi dengan baik untuk rendering sisi klien: modul JavaScript yang sama yang mendefinisikan Elemen Khusus kita juga membuat Akar Bayangan dan mengatur kontennya. Namun, banyak aplikasi web perlu merender konten sisi server atau HTML statis pada waktu build. Hal ini dapat menjadi bagian penting dalam memberikan pengalaman yang wajar kepada pengunjung yang mungkin tidak mampu menjalankan JavaScript.

Justifikasi untuk Rendering Sisi Server (SSR) bervariasi dari satu project ke project lainnya. Beberapa situs harus menyediakan HTML yang dirender server yang berfungsi penuh untuk memenuhi panduan aksesibilitas, situs lain memilih untuk memberikan pengalaman dasar tanpa JavaScript sebagai cara untuk memastikan performa yang baik pada koneksi atau perangkat yang lambat.

Secara historis, sulit untuk menggunakan Shadow DOM yang dikombinasikan dengan Rendering Sisi Server karena tidak ada cara bawaan untuk mengekspresikan Shadow Roots di HTML yang dihasilkan server. Ada juga implikasi performa saat melampirkan Root Bayangan ke elemen DOM yang telah dirender tanpanya. Hal ini dapat menyebabkan pergeseran tata letak setelah halaman dimuat, atau menampilkan flash konten tanpa gaya untuk sementara ("FOUC") saat memuat stylesheet Shadow Root.

Shadow DOM deklaratif (DSD) menghapus batasan ini, menghadirkan Shadow DOM ke server.

Cara membangun Shadow Root Declarative

Root Bayangan Declaratif adalah elemen <template> dengan atribut shadowrootmode:

<host-element>
  <template shadowrootmode="open">
    <slot></slot>
  </template>
  <h2>Light content</h2>
</host-element>

Elemen template dengan atribut shadowrootmode terdeteksi oleh parser HTML dan segera diterapkan sebagai root bayangan dari elemen induknya. Memuat markup HTML murni dari contoh di atas hasil dalam hierarki DOM berikut:

<host-element>
  #shadow-root (open)
  <slot>
    ↳
    <h2>Light content</h2>
  </slot>
</host-element>

Contoh kode ini mengikuti konvensi panel Elemen Chrome DevTools untuk menampilkan konten Shadow DOM. Misalnya, karakter mewakili konten Light DOM dengan slot.

Ini memberi kita manfaat dari enkapsulasi dan proyeksi slot Shadow DOM di HTML statis. JavaScript tidak diperlukan untuk menghasilkan seluruh pohon, termasuk Root Bayangan.

Hidrasi komponen

Shadow DOM deklaratif dapat digunakan sendiri sebagai cara untuk mengenkapsulasi gaya atau menyesuaikan penempatan turunan, tetapi paling efektif ketika digunakan dengan Elemen Kustom. Komponen yang dibuat menggunakan Elemen Kustom akan diupgrade secara otomatis dari HTML statis. Dengan diperkenalkannya Shadow DOM deklaratif, sekarang Elemen Kustom dapat memiliki shadow root sebelum diupgrade.

Elemen Kustom yang diupgrade dari HTML yang menyertakan Root Bayangan Declaratif sudah memiliki shadow root yang terpasang. Artinya, elemen akan memiliki properti shadowRoot yang sudah tersedia saat dibuat instance-nya, tanpa kode Anda secara eksplisit membuatnya. Sebaiknya periksa this.shadowRoot untuk mengetahui root bayangan yang ada di konstruktor elemen Anda. Jika sudah ada nilai, HTML untuk komponen ini akan menyertakan Root Bayangan Declaratif. Jika nilainya null, berarti tidak ada Root Bayangan Declaratif di HTML atau browser tidak mendukung Shadow DOM Declarative.

<menu-toggle>
  <template shadowrootmode="open">
    <button>
      <slot></slot>
    </button>
  </template>
  Open Menu
</menu-toggle>
<script>
  class MenuToggle extends HTMLElement {
    constructor() {
      super();

      // Detect whether we have SSR content already:
      if (this.shadowRoot) {
        // A Declarative Shadow Root exists!
        // wire up event listeners, references, etc.:
        const button = this.shadowRoot.firstElementChild;
        button.addEventListener('click', toggle);
      } else {
        // A Declarative Shadow Root doesn't exist.
        // Create a new shadow root and populate it:
        const shadow = this.attachShadow({mode: 'open'});
        shadow.innerHTML = `<button><slot></slot></button>`;
        shadow.firstChild.addEventListener('click', toggle);
      }
    }
  }

  customElements.define('menu-toggle', MenuToggle);
</script>

Elemen Kustom telah lama digunakan, dan sampai sekarang tidak ada alasan untuk memeriksa root bayangan yang sudah ada sebelum membuatnya menggunakan attachShadow(). Shadow DOM deklaratif menyertakan perubahan kecil yang memungkinkan komponen yang ada berfungsi meskipun demikian: memanggil metode attachShadow() pada elemen dengan Root Bayangan Declarative yang ada tidak akan memunculkan error. Sebagai gantinya, Shadow Root Declarative dikosongkan dan ditampilkan. Hal ini memungkinkan komponen lama yang tidak dibangun untuk Shadow DOM Declaratif untuk terus berfungsi, karena root deklaratif dipertahankan hingga penggantian imperatif dibuat.

Untuk Elemen Kustom yang baru dibuat, properti ElementInternals.shadowRoot baru menyediakan cara eksplisit untuk mendapatkan referensi ke Root Bayangan Declaratif yang ada dari elemen yang ada, baik terbuka maupun tertutup. Ini dapat digunakan untuk memeriksa dan menggunakan Root Bayangan Declaratif apa pun, sambil tetap melakukan fallback ke attachShadow() jika salah satu tidak disediakan.

class MenuToggle extends HTMLElement {
  constructor() {
    super();

    const internals = this.attachInternals();

    // check for a Declarative Shadow Root:
    let shadow = internals.shadowRoot;

    if (!shadow) {
      // there wasn't one. create a new Shadow Root:
      shadow = this.attachShadow({
        mode: 'open'
      });
      shadow.innerHTML = `<button><slot></slot></button>`;
    }

    // in either case, wire up our event listener:
    shadow.firstChild.addEventListener('click', toggle);
  }
}

customElements.define('menu-toggle', MenuToggle);

Satu bayangan per root

Root Bayangan Declaratif hanya terkait dengan elemen induknya. Ini berarti akar bayangan selalu ditempatkan bersama dengan elemen terkait. Keputusan desain ini memastikan shadow root dapat di-streaming seperti dokumen HTML lainnya. Juga nyaman untuk pembuatan dan pembuatan, karena menambahkan root bayangan ke elemen tidak memerlukan pemeliharaan registry dari shadow root yang sudah ada.

untungan dari mengaitkan root bayangan dengan elemen induknya adalah tidak mungkin bagi beberapa elemen untuk diinisialisasi dari <template> Root Bayangan Declaratif yang sama. Akan tetapi, ini tidak penting dalam kebanyakan kasus ketika Shadow DOM deklaratif digunakan, karena isi dari setiap shadow root jarang identik. Meskipun HTML yang dirender oleh server sering berisi struktur elemen berulang, kontennya secara umum berbeda, misalnya, sedikit variasi dalam teks atau atribut. Karena konten Root Bayangan Declaratif serial sepenuhnya statis, mengupgrade beberapa elemen dari satu Root Bayangan Deklaratif hanya akan berfungsi jika elemen tersebut identik. Terakhir, dampak dari akar bayangan serupa yang berulang pada ukuran transfer jaringan relatif kecil karena efek kompresi.

Di masa mendatang, Anda dapat mengunjungi kembali root bayangan bersama. Jika DOM mendapatkan dukungan untuk template bawaan, Root Bayangan Declaratif dapat diperlakukan sebagai template yang dibuat instance-nya guna membuat root bayangan untuk elemen tertentu. Desain Shadow DOM deklaratif saat ini memungkinkan kemungkinan untuk ada di masa mendatang dengan membatasi pengaitan shadow root ke satu elemen.

Streamingnya keren

Mengaitkan Shadow Root Declarative secara langsung dengan elemen induknya akan menyederhanakan proses upgrade dan melampirkannya ke elemen tersebut. Root Bayangan deklaratif terdeteksi selama penguraian HTML, dan langsung dilampirkan saat tag <template> pembuka ditemukan. HTML yang telah diurai dalam <template> diuraikan langsung menjadi root bayangan, sehingga dapat "di-streaming": dirender saat diterima.

<div id="el">
  <script>
    el.shadowRoot; // null
  </script>

  <template shadowrootmode="open">
    <!-- shadow realm -->
  </template>

  <script>
    el.shadowRoot; // ShadowRoot
  </script>
</div>

Khusus parser

Shadow DOM deklaratif adalah fitur parser HTML. Ini berarti bahwa Root Bayangan Declaratif hanya akan diuraikan dan dilampirkan untuk tag <template> dengan atribut shadowrootmode yang ada selama penguraian HTML. Dengan kata lain, Akar Bayangan Deklaratif dapat dibuat selama penguraian HTML awal:

<some-element>
  <template shadowrootmode="open">
    shadow root content for some-element
  </template>
</some-element>

Menetapkan atribut shadowrootmode elemen <template> tidak akan melakukan apa pun, dan template tetap merupakan elemen template biasa:

const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null

Untuk menghindari beberapa pertimbangan keamanan yang penting, Shadow Root deklaratif juga tidak dapat dibuat menggunakan API penguraian fragmen seperti innerHTML atau insertAdjacentHTML(). Satu-satunya cara untuk mengurai HTML dengan menerapkan Root Bayangan Declaratif adalah dengan menggunakan setHTMLUnsafe() atau parseHTMLUnsafe():

<script>
  const html = `
    <div>
      <template shadowrootmode="open"></template>
    </div>
  `;
  const div = document.createElement('div');
  div.innerHTML = html; // No shadow root here
  div.setHTMLUnsafe(html); // Shadow roots included
  const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>

Rendering server dengan gaya

Stylesheet inline dan eksternal didukung sepenuhnya di dalam Root Bayangan deklaratif menggunakan tag <style> dan <link> standar:

<nineties-button>
  <template shadowrootmode="open">
    <style>
      button {
        color: seagreen;
      }
    </style>
    <link rel="stylesheet" href="/comicsans.css" />
    <button>
      <slot></slot>
    </button>
  </template>
  I'm Blue
</nineties-button>

Gaya yang ditentukan dengan cara ini juga sangat dioptimalkan: jika style sheet yang sama ada dalam beberapa Root Bayangan Declaratif, gaya tersebut hanya dimuat dan diuraikan sekali. Browser menggunakan CSSStyleSheet pendukung tunggal yang digunakan bersama oleh semua shadow root, sehingga menghilangkan overhead memori duplikat.

Stylesheet yang Dapat Dibangun tidak didukung dalam Shadow DOM Declarative. Hal ini karena, saat ini, tidak ada cara untuk menserialisasi stylesheet yang dapat dibuat di HTML, dan tidak ada cara untuk merujuknya saat mengisi adoptedStyleSheets.

Cara menghindari flash konten tanpa gaya

Satu potensi masalah di browser yang belum mendukung Shadow DOM Declarative adalah menghindari "flash konten tanpa gaya" (FOUC), yaitu konten mentah yang ditampilkan untuk Elemen Kustom yang belum diupgrade. Sebelum Shadow DOM deklaratif, satu teknik umum untuk menghindari FOUC adalah dengan menerapkan aturan gaya display:none ke Elemen Kustom yang belum dimuat, karena shadow root ini belum dilampirkan dan diisi. Dengan demikian, konten tidak akan ditampilkan hingga statusnya "siap":

<style>
  x-foo:not(:defined) > * {
    display: none;
  }
</style>

Dengan diperkenalkannya Shadow DOM Declarative, Elemen Kustom dapat dirender atau ditulis dalam HTML sedemikian rupa sehingga konten bayangannya ada di tempatnya dan siap sebelum implementasi komponen sisi klien dimuat:

<x-foo>
  <template shadowrootmode="open">
    <style>h2 { color: blue; }</style>
    <h2>shadow content</h2>
  </template>
</x-foo>

Dalam hal ini, display:none "FOUC" akan mencegah konten shadow root deklaratif ditampilkan. Namun, menghapus aturan tersebut akan menyebabkan browser tanpa dukungan Shadow DOM Delaratif menampilkan konten yang salah atau tanpa gaya hingga polyfill Shadow DOM deklaratif dimuat dan mengonversi template shadow root menjadi shadow root sebenarnya.

Untungnya, hal ini dapat diatasi dalam CSS dengan memodifikasi aturan gaya FOUC. Di browser yang mendukung Shadow DOM Declarative, elemen <template shadowrootmode> segera dikonversi menjadi shadow root, tanpa membiarkan elemen <template> di hierarki DOM. Browser yang tidak mendukung Shadow DOM deklaratif mempertahankan elemen <template>, yang dapat kita gunakan untuk mencegah FOUC:

<style>
  x-foo:not(:defined) > template[shadowrootmode] ~ *  {
    display: none;
  }
</style>

"FOUC" yang direvisi, alih-alih menyembunyikan Elemen Khusus aturan menyembunyikan turunannya saat mengikuti elemen <template shadowrootmode>. Setelah Elemen Khusus ditentukan, aturan tidak lagi cocok. Aturan ini diabaikan di browser yang mendukung Shadow DOM deklaratif karena turunan <template shadowrootmode> dihapus selama penguraian HTML.

Deteksi fitur dan dukungan browser

Shadow DOM deklaratif telah tersedia sejak Chrome 90 dan Edge 91, tetapi menggunakan atribut non-standar lama yang disebut shadowroot, bukan atribut shadowrootmode standar. Atribut shadowrootmode yang lebih baru dan perilaku streaming tersedia di Chrome 111 dan Edge 111.

Sebagai API platform web baru, Shadow DOM Declarative belum didukung secara luas di semua browser. Dukungan browser dapat dideteksi dengan memeriksa keberadaan properti shadowRootMode pada prototipe HTMLTemplateElement:

function supportsDeclarativeShadowDOM() {
  return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}

Polyfill

Membangun polyfill yang disederhanakan untuk Shadow DOM deklaratif relatif mudah, karena polyfill tidak perlu mereplikasi secara sempurna semantik pengaturan waktu atau karakteristik khusus parser yang menjadi perhatian implementasi browser. Untuk mem-polyfill Declarative Shadow DOM, kita dapat memindai DOM untuk menemukan semua elemen <template shadowrootmode>, lalu mengonversinya menjadi Shadow Roots yang terlampir pada elemen induknya. Proses ini dapat dilakukan setelah dokumen siap, atau dipicu oleh peristiwa yang lebih spesifik seperti siklus proses Elemen Kustom.

(function attachShadowRoots(root) {
  root.querySelectorAll("template[shadowrootmode]").forEach(template => {
    const mode = template.getAttribute("shadowrootmode");
    const shadowRoot = template.parentNode.attachShadow({ mode });

    shadowRoot.appendChild(template.content);
    template.remove();
    attachShadowRoots(shadowRoot);
  });
})(document);

Bacaan lebih lanjut