Template, slot, dan bayangan

Manfaat komponen web adalah kegunaannya kembali: Anda dapat membuat widget UI sekali, dan menggunakannya kembali beberapa kali. Sementara Anda memerlukan JavaScript untuk membuat komponen web, Anda tidak memerlukan pustaka JavaScript. HTML dan API terkait menyediakan semua yang Anda butuhkan.

Standar Komponen Web terdiri atas tiga bagian—template HTML, Elemen Kustom, dan Shadow DOM. Jika digabungkan, keduanya memungkinkan pembuatan elemen mandiri (dienkapsulasi) yang disesuaikan, dapat digunakan kembali yang dapat terintegrasi dengan lancar ke dalam aplikasi yang ada, seperti semua elemen HTML lainnya yang telah kita bahas.

Di bagian ini, kita akan membuat elemen <star-rating>, yakni komponen web yang memungkinkan pengguna memberi rating pengalaman pada skala satu hingga lima bintang. Saat memberi nama elemen kustom, sebaiknya gunakan semua huruf kecil. Juga, sertakan tanda hubung, karena ini membantu membedakan antara elemen biasa dan khusus.

Kita akan membahas penggunaan elemen <template> dan <slot>, atribut slot, dan JavaScript untuk membuat template dengan Shadow DOM yang dienkapsulasi. Kemudian kita akan menggunakan kembali elemen yang sudah ditentukan, menyesuaikan bagian teks, seperti halnya elemen atau komponen web. Kita juga akan membahas secara singkat penggunaan CSS dari dalam dan luar elemen kustom.

Elemen <template>

Elemen <template> digunakan untuk mendeklarasikan fragmen HTML yang akan di-clone dan disisipkan ke dalam DOM dengan JavaScript. Konten elemen tidak dirender secara default. Sebaliknya, dibuat instance-nya menggunakan JavaScript.

<template id="star-rating-template">
  <form>
    <fieldset>
      <legend>Rate your experience:</legend>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required />
        <input type="radio" name="rating" value="2" aria-label="2 stars" />
        <input type="radio" name="rating" value="3" aria-label="3 stars" />
        <input type="radio" name="rating" value="4" aria-label="4 stars" />
        <input type="radio" name="rating" value="5" aria-label="5 stars" />
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Karena konten elemen <template> tidak ditulis ke layar, <form> dan kontennya tidak dirender. Ya, Codepen ini kosong, tetapi jika Anda memeriksa tab HTML, Anda akan melihat markup <template>.

Dalam contoh ini, <form> bukan turunan dari <template> di DOM. Sebaliknya, konten elemen <template> adalah turunan dari DocumentFragment yang ditampilkan oleh HTMLTemplateElement.content saat ini. Agar terlihat, JavaScript harus digunakan untuk mengambil konten dan menambahkan konten tersebut ke DOM.

JavaScript singkat ini tidak membuat elemen kustom. Sebaliknya, contoh ini telah menambahkan konten <template> ke dalam <body>. Konten telah menjadi bagian dari DOM yang dapat dilihat dan ditata.

Screenshot codepen sebelumnya seperti yang ditampilkan di DOM.

Mewajibkan JavaScript menerapkan template untuk peringkat bintang satu saja tidak terlalu berguna, namun membuat komponen web untuk berulang kali digunakan, widget rating bintang yang dapat disesuaikan sangatlah berguna.

Elemen <slot>

Kami menyertakan slot untuk menyertakan legenda yang disesuaikan per kejadian. HTML menyediakan <slot> sebagai placeholder di dalam <template> yang, jika diberi nama, akan membuat "slot bernama". Slot bernama dapat digunakan untuk menyesuaikan konten dalam komponen web. Elemen <slot> memberi cara untuk mengontrol tempat turunan dari harus dimasukkan ke dalam pohon bayangannya.

Dalam template, kita mengubah <legend> menjadi <slot>:

<template id="star-rating-template">
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>

Atribut name digunakan untuk menetapkan slot ke elemen lain jika elemen memiliki atribut slot yang nilainya cocok dengan nama slot yang dinamai. Jika elemen kustom tidak memiliki kecocokan untuk slot, konten <slot> akan dirender. Jadi, kami menyertakan <legend> dengan konten generik yang boleh dirender jika seseorang menyertakan <star-rating></star-rating>, tanpa konten, di HTML-nya.

<star-rating>
  <legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Toasty McToastface</legend>
  <p>Is this text visible?</p>
</star-rating>

Atribut slot adalah atribut global yang digunakan untuk mengganti konten <slot> dalam <template>. Dalam elemen khusus, elemen dengan atribut slot adalah <legend>. Tidak harus seperti itu. Dalam template kita, <slot name="star-rating-legend"> akan diganti dengan <anyElement slot="star-rating-legend">, di mana <anyElement> dapat berupa elemen apa pun, bahkan elemen kustom lainnya.

Elemen yang belum ditentukan

Di <template>, kita menggunakan elemen <rating>. Elemen ini bukan elemen kustom. Sebaliknya, itu adalah elemen yang tidak diketahui. Browser tidak gagal ketika mereka tidak mengenali suatu elemen. Elemen HTML yang tidak dikenal diperlakukan oleh browser sebagai sebaris anonim elemen yang dapat diberi gaya dengan CSS. Serupa dengan <span>, elemen <rating> dan <star-rating> tidak menerapkan agen pengguna gaya atau semantik.

Perhatikan bahwa <template> dan kontennya tidak dirender. <template> adalah elemen yang diketahui berisi konten yang tidak akan dirender. Elemen <star-rating> belum ditentukan. Browser akan menampilkannya hingga kita menentukannya seperti semua elemen yang tidak dikenal. Untuk saat ini, <star-rating> yang tidak dikenal diperlakukan sebagai elemen inline anonim, sehingga konten termasuk legenda dan <p> di <star-rating> ketiga akan ditampilkan seperti halnya jika berada di <span>.

Mari kita tentukan elemen untuk mengonversi elemen yang tidak dikenal ini menjadi elemen kustom.

Elemen khusus

JavaScript diperlukan untuk menentukan elemen kustom. Jika ditentukan, konten elemen <star-rating> akan diganti dengan {i>shadow root<i} yang berisi semua konten {i>template<i} yang kita kaitkan dengannya. Elemen <slot> dari template diganti dengan konten elemen dalam <star-rating> yang nilai atribut slot-nya cocok dengan nilai nama <slot>, jika hanya ada satu. Jika tidak, konten slot template akan ditampilkan.

Konten dalam elemen kustom yang tidak terkait dengan slot—<p>Is this text visible?</p> dalam <star-rating> ketiga kami—tidak disertakan dalam akar bayangan dan karenanya tidak ditampilkan.

Kami menentukan elemen khusus yang bernama star-rating dengan memperluas HTMLElement:

customElements.define('star-rating',
  class extends HTMLElement {
    constructor() {
      super(); // Always call super first in constructor
      const starRating = document.getElementById('star-rating-template').content;
      const shadowRoot = this.attachShadow({
        mode: 'open'
      });
      shadowRoot.appendChild(starRating.cloneNode(true));
    }
  });

Setelah elemen ditentukan, setiap kali browser menemukan elemen <star-rating>, elemen tersebut akan dirender seperti yang ditentukan oleh elemen dengan #star-rating-template, yang merupakan template kita. Browser akan melampirkan hierarki shadow DOM ke node, dengan menambahkan clone konten template ke shadow DOM tersebut. Perhatikan bahwa elemen tempat Anda dapat attachShadow() dibatasi.

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(starRating.cloneNode(true));

Jika melihat alat developer, Anda akan mengetahui bahwa <form> dari <template> adalah bagian dari shadow root setiap elemen kustom. Duplikat konten <template> terlihat di setiap elemen kustom di alat developer dan terlihat di browser, tetapi kontennya elemen kustom itu sendiri tidak dirender ke layar.

Screenshot DevTools menampilkan konten template yang digandakan di setiap elemen kustom.

Dalam contoh <template>, kita menambahkan konten template ke isi dokumen, dengan menambahkan konten ke DOM biasa. Dalam definisi customElements, kita menggunakan metode appendChild(), tetapi konten template yang digandakan ditambahkan ke shadow DOM yang dienkapsulasi.

Perhatikan bagaimana bintang kembali menjadi tombol pilihan tanpa gaya? Menjadi bagian dari shadow DOM daripada DOM standar, gaya dalam tab CSS Codepen tidak berlaku. CSS tab tersebut gaya tercakup pada dokumen, bukan pada shadow DOM, sehingga gaya tidak diterapkan. Kita harus membuat paket data untuk menata gaya konten Shadow DOM yang dienkapsulasi.

DOM Bayangan

Shadow DOM mencakup gaya CSS ke setiap shadow tree, mengisolasinya dari bagian lain dokumen. Ini berarti CSS eksternal tidak berlaku untuk komponen Anda, dan gaya komponen tidak berpengaruh pada bagian dokumen lainnya, kecuali jika kita sengaja mengarahkan mereka.

Karena telah menambahkan konten ke shadow DOM, kita dapat menyertakan elemen <style> menyediakan CSS yang dienkapsulasi ke elemen khusus.

Dengan cakupan elemen kustom, kita tidak perlu khawatir gaya akan menyebar ke seluruh dokumen. Kita dapat secara substansial mengurangi kekhususan pemilih. Misalnya, karena satu-satunya input yang digunakan dalam elemen kustom adalah pilihan radio kita dapat menggunakan input alih-alih input[type="radio"] sebagai pemilih.

 <template id="star-rating-template">
  <style>
    rating {
      display: inline-flex;
    }
    input {
      appearance: none;
      margin: 0;
      box-shadow: none;
    }
    input::after {
      content: '\2605'; /* solid star */
      font-size: 32px;
    }
    rating:hover input:invalid::after,
    rating:focus-within input:invalid::after {
      color: #888;
    }
    input:invalid::after,
      rating:hover input:hover ~ input:invalid::after,
      input:focus ~ input:invalid::after  {
      color: #ddd;
    }
    input:valid {
      color: orange;
    }
    input:checked ~ input:not(:checked)::after {
      color: #ccc;
      content: '\2606'; /* hollow star */
    }
  </style>
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required/>
        <input type="radio" name="rating" value="2" aria-label="2 stars"/>
        <input type="radio" name="rating" value="3" aria-label="3 stars"/>
        <input type="radio" name="rating" value="4" aria-label="4 stars"/>
        <input type="radio" name="rating" value="5" aria-label="5 stars"/>
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Sementara komponen web dienkapsulasi dengan markup dalam-<template> dan gaya CSS dicakup ke shadow DOM dan tersembunyi dari segala sesuatu di luar komponen, konten slot yang dirender, <anyElement slot="star-rating-legend"> dari <star-rating>, tidak dienkapsulasi.

Penataan gaya di luar cakupan saat ini

Dimungkinkan saja, namun tidak sederhana, untuk menata gaya dokumen dari dalam shadow DOM dan menata gaya materi shadow DOM dari gaya global. Batas bayangan, tempat shadow DOM berakhir dan DOM reguler dimulai, bisa dilintasi, namun hanya dengan sengaja.

Pohon bayangan adalah hierarki DOM di dalam shadow DOM. Root bayangan adalah node root dari pohon bayangan.

Class semu :host memilih <star-rating>, elemen host bayangan. Shadow host adalah node DOM tempat shadow DOM terpasang. Untuk menargetkan versi host tertentu saja, gunakan :host(). Tindakan ini hanya akan memilih elemen bayangan host yang cocok dengan parameter yang diteruskan, seperti pemilih atribut atau class. Untuk memilih semua elemen kustom, Anda dapat menggunakan star-rating { /* styles */ } di CSS global, atau :host(:not(#nonExistantId)) di gaya template. Dalam hal dengan kekhususan, CSS global yang unggul.

Elemen pseudo ::slotted() melewati batas shadow DOM dari dalam shadow DOM. Elemen ini memilih elemen yang diberi slot jika cocok dengan pemilih. Dalam contoh kita, ::slotted(legend) cocok dengan tiga legenda.

Untuk menargetkan shadow DOM dari CSS dalam cakupan global, template harus diedit. part dapat ditambahkan ke elemen apa pun yang ingin Anda tata gayanya. Lalu, gunakan elemen pseudo ::part() untuk mencocokkan elemen dalam pohon bayangan yang cocok dengan parameter yang diteruskan. Anchor atau elemen asal elemen-elemen semu merupakan host, atau nama elemen kustom, dalam hal ini star-rating. Parameternya adalah nilai atribut part.

Jika markup template kami dimulai seperti berikut:

<template id="star-rating-template">
  <form part="formPart">
    <fieldset part="fieldsetPart">

Kita dapat menargetkan <form> dan <fieldset> dengan:

star-rating::part(formPart) { /* styles */ }
star-rating::part(fieldsetPart) { /* styles */ }

Nama bagian berfungsi mirip dengan class: elemen dapat memiliki beberapa nama bagian yang dipisahkan spasi, dan beberapa elemen dapat memiliki nama suku cadang yang sama.

Google memiliki checklist yang bagus untuk membuat elemen kustom. Anda mungkin juga ingin mempelajari tentang shadow DOM deklaratif.

Menguji pemahaman Anda

Uji pengetahuan Anda tentang {i>template<i}, slot, dan bayangan.

Secara default gaya dari luar shadow DOM akan menata gaya elemen di dalamnya.

Benar.
Coba lagi.
Salah.
Benar.

Jawaban mana yang merupakan deskripsi yang benar dari elemen <template>?

Elemen generik yang digunakan untuk menampilkan konten di halaman Anda.
Coba lagi.
Elemen placeholder.
Coba lagi.
Elemen yang digunakan untuk mendeklarasikan fragmen HTML, yang tidak akan dirender secara default.
Benar.