DOM Bayangan Deklaratif

Shadow DOM Deklaratif adalah fitur platform web standar, yang telah didukung di Chrome mulai versi 90. Perhatikan bahwa spesifikasi untuk fitur ini berubah pada tahun 2023 (termasuk penggantian nama shadowroot menjadi shadowrootmode), dan versi standar terbaru dari semua bagian fitur ini tersedia 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 Khusus. Shadow DOM menyediakan cara untuk menentukan cakupan gaya CSS ke subpohon DOM tertentu dan mengisolasi subpohon tersebut dari dokumen lainnya. Elemen <slot> memberi kita cara untuk mengontrol tempat turunan Elemen Kustom harus disisipkan dalam Hierarki Bayangannya. Kombinasi fitur ini memungkinkan sistem untuk mem-build komponen mandiri yang dapat digunakan kembali dan terintegrasi dengan lancar ke dalam aplikasi yang ada, seperti elemen HTML bawaan.

Hingga saat ini, satu-satunya cara untuk menggunakan Shadow DOM adalah dengan membuat root 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 menentukan Elemen Kustom juga membuat Akar Bayangan dan menetapkan kontennya. Namun, banyak aplikasi web perlu merender konten sisi server atau ke HTML statis pada waktu build. Hal ini dapat menjadi bagian penting dalam memberikan pengalaman yang wajar kepada pengunjung yang mungkin tidak dapat 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 pedoman aksesibilitas, sementara 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 bersama dengan Rendering Sisi Server karena tidak ada cara bawaan untuk mengekspresikan Root Bayangan dalam HTML yang dibuat server. Ada juga implikasi performa saat melampirkan Akar Bayangan ke elemen DOM yang telah dirender tanpanya. Hal ini dapat menyebabkan pergeseran tata letak setelah halaman dimuat, atau menampilkan flash konten tanpa gaya ("FOUC") untuk sementara saat memuat stylesheet Shadow Root.

Declarative Shadow DOM (DSD) menghilangkan batasan ini, sehingga Shadow DOM dapat diakses di server.

Cara mem-build Declarative Shadow Root

Root Bayangan Deklaratif 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 langsung diterapkan sebagai root bayangan elemen induknya. Memuat markup HTML murni dari contoh di atas akan menghasilkan 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 yang di-slot.

Hal ini memberi kita manfaat enkapsulasi dan proyeksi slot Shadow DOM dalam HTML statis. JavaScript tidak diperlukan untuk menghasilkan seluruh hierarki, termasuk Root Bayangan.

Hidrasi komponen

Shadow DOM Deklaratif dapat digunakan sendiri sebagai cara untuk mengenkapsulasi gaya atau menyesuaikan penempatan turunan, tetapi paling efektif jika digunakan dengan Elemen Khusus. Komponen yang dibuat menggunakan Elemen Kustom akan otomatis diupgrade dari HTML statis. Dengan diperkenalkannya Shadow DOM Deklaratif, kini Elemen Kustom dapat memiliki root bayangan sebelum diupgrade.

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

<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 sudah ada sejak lama, dan hingga saat ini tidak ada alasan untuk memeriksa root bayangan yang 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 Akar Bayangan Deklaratif yang ada tidak akan menampilkan error. Sebagai gantinya, Root Bayangan Deklaratif akan dikosongkan dan ditampilkan. Hal ini memungkinkan komponen lama yang tidak di-build untuk Declarative Shadow DOM terus berfungsi, karena root deklaratif dipertahankan hingga penggantian imperatif dibuat.

Untuk Elemen Kustom yang baru dibuat, properti ElementInternals.shadowRoot baru memberikan cara eksplisit untuk mendapatkan referensi ke Root Bayangan Deklaratif yang ada dari elemen, baik terbuka maupun tertutup. Ini dapat digunakan untuk memeriksa dan menggunakan Declarative Shadow Root, sambil tetap kembali ke attachShadow() jika tidak ada yang 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

Declarative Shadow Root hanya dikaitkan dengan elemen induknya. Artinya, root bayangan selalu ditempatkan bersama dengan elemen terkaitnya. Keputusan desain ini memastikan root bayangan dapat di-streaming seperti dokumen HTML lainnya. Hal ini juga praktis untuk penulisan dan pembuatan, karena menambahkan root bayangan ke elemen tidak memerlukan pemeliharaan registry root bayangan yang ada.

Kompromi dari mengaitkan root bayangan dengan elemen induknya adalah beberapa elemen tidak dapat diinisialisasi dari <template> Root Bayangan Deklaratif yang sama. Namun, hal ini tidak akan menjadi masalah dalam sebagian besar kasus penggunaan Declarative Shadow DOM, karena konten setiap root bayangan jarang identik. Meskipun HTML yang dirender server sering kali berisi struktur elemen berulang, kontennya umumnya berbeda–misalnya, sedikit variasi dalam teks, atau atribut. Karena konten Declarative Shadow Root yang diserialisasi sepenuhnya statis, mengupgrade beberapa elemen dari satu Declarative Shadow Root hanya akan berfungsi jika elemen tersebut kebetulan identik. Terakhir, dampak root bayangan serupa yang berulang pada ukuran transfer jaringan relatif kecil karena efek kompresi.

Di masa mendatang, Anda mungkin dapat meninjau kembali root bayangan bersama. Jika DOM mendapatkan dukungan untuk template bawaan, Akar Bayangan Deklaratif dapat diperlakukan sebagai template yang dibuat instance-nya untuk membuat root bayangan bagi elemen tertentu. Desain Shadow DOM Deklaratif saat ini memungkinkan kemungkinan ini ada di masa mendatang dengan membatasi pengaitan root bayangan ke satu elemen.

Streaming itu keren

Mengaitkan Akar Bayangan Deklaratif secara langsung dengan elemen induknya akan menyederhanakan proses upgrade dan melampirkan akar bayangan ke elemen tersebut. Root Bayangan Deklaratif terdeteksi selama penguraian HTML, dan segera dilampirkan saat tag <template> pembuka-nya ditemukan. HTML yang diuraikan dalam <template> diuraikan langsung ke 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. Artinya, Root Bayangan Deklaratif 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 dari elemen <template> tidak akan melakukan apa pun, dan template tetap menjadi 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, Akar Bayangan Deklaratif juga tidak dapat dibuat menggunakan API penguraian fragmen seperti innerHTML atau insertAdjacentHTML(). Satu-satunya cara untuk mengurai HTML dengan Akar Bayangan Deklaratif yang diterapkan adalah 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 Declarative Shadow Roots 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 sheet gaya yang sama ada di beberapa Declarative Shadow Roots, sheet gaya tersebut hanya dimuat dan diuraikan satu kali. Browser menggunakan satu CSSStyleSheet pendukung yang dibagikan oleh semua root bayangan, sehingga menghilangkan overhead memori duplikat.

Stylesheet yang Dapat Dibuat tidak didukung di Shadow DOM Deklaratif. Hal ini karena, saat ini, tidak ada cara untuk melakukan serialisasi stylesheet yang dapat dibuat di HTML, dan tidak ada cara untuk mereferensikannya saat mengisi adoptedStyleSheets.

Cara menghindari flash konten tanpa gaya

Salah satu potensi masalah di browser yang belum mendukung Declarative Shadow DOM adalah menghindari "flash of unstyled content" (FOUC), saat konten mentah ditampilkan untuk Elemen Kustom yang belum diupgrade. Sebelum Shadow DOM Deklaratif, salah satu teknik umum untuk menghindari FOUC adalah menerapkan aturan gaya display:none ke Elemen Kustom yang belum dimuat, karena akar bayangannya belum dilampirkan dan diisi. Dengan cara ini, konten tidak akan ditampilkan hingga "siap":

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

Dengan diperkenalkannya Shadow DOM Deklaratif, Elemen Khusus dapat dirender atau ditulis dalam HTML sehingga konten bayangannya sudah ada dan siap sebelum penerapan komponen sisi klien dimuat:

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

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

Untungnya, masalah ini dapat diatasi di CSS dengan mengubah aturan gaya FOUC. Di browser yang mendukung Shadow DOM Deklaratif, elemen <template shadowrootmode> langsung dikonversi menjadi root bayangan, sehingga tidak ada elemen <template> di hierarki DOM. Browser yang tidak mendukung Declarative Shadow DOM mempertahankan elemen <template>, yang dapat kita gunakan untuk mencegah FOUC:

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

Alih-alih menyembunyikan Elemen Kustom yang belum ditentukan, aturan "FOUC" yang direvisi akan menyembunyikan turunan saat mengikuti elemen <template shadowrootmode>. Setelah Elemen Kustom ditentukan, aturan tidak lagi cocok. Aturan diabaikan di browser yang mendukung Declarative Shadow DOM 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 dan perilaku streaming yang lebih baru tersedia di Chrome 111 dan Edge 111.

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

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

Polyfill

Membuat polyfill yang disederhanakan untuk Shadow DOM Deklaratif relatif mudah, karena polyfill tidak perlu mereplikasi semantik pengaturan waktu atau karakteristik khusus parser yang menjadi perhatian implementasi browser dengan sempurna. Untuk melakukan polyfill pada Shadow DOM Deklaratif, kita dapat memindai DOM untuk menemukan semua elemen <template shadowrootmode>, lalu mengonversinya menjadi Akar Bayangan yang terpasang 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