DOM Bayangan Deklaratif

Cara baru untuk mengimplementasikan dan menggunakan Shadow DOM secara langsung di HTML.

Shadow DOM Declarative adalah fitur platform web, saat ini dalam proses standardisasi. Fitur ini diaktifkan secara default di Chrome versi 111.

Shadow DOM adalah salah satu dari tiga standar Komponen Web, yang dibulatkan dengan template HTML dan Elemen Kustom. Shadow DOM menyediakan cara untuk menentukan cakupan gaya CSS ke subhierarki DOM tertentu dan mengisolasi subhierarki tersebut dari bagian dokumen lainnya. Elemen <slot> memberi kita cara untuk mengontrol tempat turunan Elemen Kustom harus disisipkan dalam Pohon Bayangannya. Gabungan fitur-fitur ini memungkinkan sistem untuk membangun komponen mandiri yang dapat digunakan kembali dan terintegrasi secara lancar dengan aplikasi yang ada seperti elemen HTML bawaan.

Sampai saat ini, satu-satunya cara untuk menggunakan Shadow DOM adalah membuat shadow root 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 Khusus kami juga membuat Root Bayangan dan menyetel 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 untuk setiap project. Beberapa situs harus menyediakan HTML yang dirender server yang berfungsi sepenuhnya untuk memenuhi pedoman aksesibilitas, yang lainnya memilih untuk memberikan pengalaman tanpa JavaScript dasar sebagai cara untuk menjamin performa yang baik pada koneksi atau perangkat yang lambat.

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

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

Membangun Akar Bayangan Deklaratif

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 elemen induknya. Memuat markup HTML murni dari contoh di atas 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 DOM Bayangan. Misalnya, karakter ↳ merepresentasikan konten Light DOM yang ditempatkan.

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

Hidrasi komponen

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

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

<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 ada, dan sampai saat ini, tidak ada alasan untuk memeriksa shadow root yang ada sebelum membuatnya menggunakan attachShadow(). Shadow DOM deklaratif menyertakan perubahan kecil yang memungkinkan komponen yang ada berfungsi, meskipun ini: memanggil metode attachShadow() pada elemen dengan Shadow Root Declarative yang sudah ada tidak akan memunculkan error. Sebagai gantinya, Declarative Shadow Root dikosongkan dan dikembalikan. Hal ini memungkinkan komponen lama yang tidak dibuat untuk Declarative Shadow DOM dapat terus berfungsi, karena root deklaratif dipertahankan hingga penggantian penting dibuat.

Untuk Elemen Kustom yang baru dibuat, properti ElementInternals.shadowRoot baru memberikan cara eksplisit untuk mendapatkan referensi ke Declarative Shadow Root yang ada dari suatu elemen, baik terbuka maupun tertutup. Ini dapat digunakan untuk memeriksa dan menggunakan Declarative Shadow Root, sambil tetap kembali ke attachShadow() jika tidak menyediakannya.

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 terkait dengan elemen induknya. Ini berarti root bayangan selalu ditempatkan bersama elemen yang terkait. Keputusan desain ini memastikan root bayangan dapat di-streaming seperti bagian dokumen HTML lainnya. Cara ini juga praktis untuk penulisan dan pembuatan, karena menambahkan root bayangan ke elemen tidak memerlukan pemeliharaan registry root bayangan yang sudah ada.

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

Pada masa mendatang, Anda dapat membuka kembali root bayangan yang dibagikan. Jika DOM mendapatkan dukungan untuk template bawaan, Root Bayangan Declaratif dapat diperlakukan sebagai template yang dibuat instance-nya untuk membangun root bayangan bagi elemen tertentu. Desain Declarative Shadow DOM saat ini memungkinkan adanya kemungkinan ini di masa mendatang dengan membatasi pengaitan root bayangan ke satu elemen.

Streaming itu keren

Mengaitkan Declarative Shadow Roots secara langsung dengan elemen induknya akan menyederhanakan proses upgrade dan melampirkannya ke elemen tersebut. Root Bayangan Declaratif terdeteksi selama penguraian HTML, dan langsung dilampirkan saat tag <template> pembukaan ditemukan. HTML yang diuraikan dalam <template> akan 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. Ini berarti Root Bayangan Declaratif hanya akan diurai dan dilampirkan untuk tag <template> dengan atribut shadowrootmode yang ada selama penguraian HTML. Dengan kata lain, Declarative Shadow Roots 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 mengakibatkan 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, Declarative Shadow Roots juga tidak dapat dibuat menggunakan API penguraian fragmen seperti innerHTML atau insertAdjacentHTML(). Satu-satunya cara untuk mengurai HTML dengan Declarative Shadow Roots yang diterapkan adalah dengan meneruskan opsi includeShadowRoots baru ke DOMParser:

<script>
  const html = `
    <div>
      <template shadowrootmode="open"></template>
    </div>
  `;
  const div = document.createElement('div');
  div.innerHTML = html; // No shadow root here
  const fragment = new DOMParser().parseFromString(html, 'text/html', {
    includeShadowRoots: true
  }); // Shadow root here
</script>

Rendering server dengan gaya

Stylesheet inline dan eksternal sepenuhnya didukung 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 stylesheet yang sama ada di beberapa Declarative Shadow Roots, gaya tersebut hanya akan dimuat dan diurai sekali. Browser menggunakan CSSStyleSheet pendukung tunggal yang digunakan bersama oleh semua root bayangan, menghilangkan overhead memori duplikat.

Stylesheet Constructable tidak didukung di DOM Bayangan Deklaratif. Hal ini karena saat ini tidak ada cara untuk membuat serialisasi stylesheet yang dapat dibuat di HTML, dan tidak ada cara untuk merujuknya saat mengisi adoptedStyleSheets.

Menghindari flash konten yang tidak bergaya

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

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

Dengan diperkenalkannya Declarative Shadow DOM, Elemen Kustom dapat dirender atau ditulis dalam HTML sedemikian rupa sehingga konten bayangannya sudah berada 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, 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 Declarative Shadow DOM polyfill dimuat dan mengonversi template shadow root menjadi shadow root yang sesungguhnya.

Untungnya, hal ini dapat diatasi di CSS dengan memodifikasi aturan gaya FOUC. Di browser yang mendukung Declarative Shadow DOM, elemen <template shadowrootmode> langsung dikonversi menjadi shadow root, sehingga tidak meninggalkan 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>

Aturan "FOUC" yang direvisi menyembunyikan turunan ketika elemen tersebut mengikuti elemen <template shadowrootmode>, bukan menyembunyikan Elemen Kustom yang belum ditentukan. Setelah Elemen Kustom ditentukan, aturan tidak lagi cocok. Aturan ini 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');
}

Isi Ulang

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