Template, slot, dan bayangan

Manfaat komponen web adalah penggunaannya kembali: Anda dapat membuat widget UI satu kali, dan menggunakannya kembali beberapa kali. Meskipun memerlukan JavaScript untuk membuat komponen web, Anda tidak memerlukan library JavaScript. HTML dan API terkait menyediakan semua yang Anda butuhkan.

Standar Komponen Web terdiri dari tiga bagian—template HTML, Elemen Khusus, dan Shadow DOM. Jika digabungkan, keduanya memungkinkan pembuatan elemen yang disesuaikan, mandiri (enkapsulasi), dan dapat digunakan kembali yang bisa diintegrasikan dengan lancar ke dalam aplikasi yang sudah ada, seperti semua elemen HTML lain yang telah kita bahas.

Di bagian ini, kita akan membuat elemen <star-rating>, komponen web yang memungkinkan pengguna memberi rating pada pengalaman dalam skala satu sampai lima bintang. Saat menamai elemen khusus, sebaiknya gunakan semua huruf kecil. Sertakan juga tanda hubung, karena ini membantu membedakan antara elemen reguler 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 ditentukan, menyesuaikan bagian teks, seperti yang Anda lakukan pada elemen atau komponen web. Kita juga akan secara singkat membahas 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, instance-nya dibuat 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 akan 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 properti HTMLTemplateElement.content. Agar terlihat, JavaScript harus digunakan untuk mengambil konten dan menambahkan konten tersebut ke DOM.

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

Screenshot codepen sebelumnya seperti yang ditampilkan di DOM.

Mewajibkan JavaScript menerapkan template hanya untuk rating bintang tidak terlalu berguna, tetapi membuat komponen web untuk widget rating bintang yang dapat disesuaikan dan digunakan berulang kali akan bermanfaat.

Elemen <slot>

Kami menyertakan slot untuk menyertakan legenda per kemunculan yang disesuaikan. HTML menyediakan elemen <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 kita cara untuk mengontrol tempat turunan elemen kustom harus disisipkan dalam hierarki bayangannya.

Di 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 tersebut memiliki atribut slot yang nilainya cocok dengan nama slot yang telah diberi nama. Jika elemen kustom tidak memiliki kecocokan untuk slot, konten <slot> akan dirender. Jadi, kami menyertakan <legend> dengan konten generik yang dapat dirender jika siapa saja 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 kustom kita, elemen dengan atribut slot adalah <legend>. Seharusnya tidak demikian. Dalam template kita, <slot name="star-rating-legend"> akan diganti dengan <anyElement slot="star-rating-legend">, dengan <anyElement> dapat berupa elemen apa pun, bahkan elemen kustom lainnya.

Elemen yang tidak ditentukan

Dalam <template>, kita menggunakan elemen <rating>. Ini bukan elemen khusus. Sebaliknya, ini adalah elemen yang tidak diketahui. Browser tidak gagal jika tidak mengenali suatu elemen. Elemen HTML yang tidak dikenal diperlakukan oleh browser sebagai elemen inline anonim yang dapat diberi gaya dengan CSS. Serupa dengan <span>, elemen <rating> dan <star-rating> tidak memiliki gaya atau semantik yang diterapkan oleh agen pengguna.

Perhatikan bahwa <template> dan konten tidak dirender. <template> adalah elemen yang diketahui dan berisi konten yang tidak akan dirender. Elemen <star-rating> belum ditentukan. Hingga kita mendefinisikan elemen, browser akan menampilkannya seperti semua elemen yang tidak dikenali. Untuk saat ini, <star-rating> yang tidak dikenal diperlakukan sebagai elemen inline anonim, sehingga konten yang mencakup legenda dan <p> di <star-rating> ketiga ditampilkan sebagaimana mestinya jika berada di <span>.

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

Elemen khusus

JavaScript diperlukan untuk menetapkan elemen kustom. Saat ditentukan, konten elemen <star-rating> akan diganti dengan root bayangan yang berisi semua konten template 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 ada. 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—tidak disertakan dalam root bayangan sehingga tidak ditampilkan.

Kami menentukan elemen kustom 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 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 mencatat bahwa <form> dari <template> adalah bagian dari shadow root setiap elemen kustom. Clone konten <template> terlihat di setiap elemen kustom di alat developer dan terlihat di browser, tetapi konten elemen kustom itu sendiri tidak dirender ke layar.

Screenshot DevTools menampilkan konten template yang di-clone di setiap elemen kustom.

Dalam contoh <template>, kami menambahkan konten template ke isi dokumen, dengan menambahkan konten ke DOM reguler. Dalam definisi customElements, kami menggunakan appendChild() yang sama, tetapi konten template yang di-clone ditambahkan ke shadow DOM yang dienkapsulasi.

Perhatikan bagaimana bintang-bintang kembali ke tombol pilihan tanpa gaya? Menjadi bagian dari shadow DOM bukan DOM standar, penataan gaya dalam tab CSS Codepen tidak berlaku. Gaya CSS tab tersebut dicakup ke dokumen, bukan ke shadow DOM, sehingga gaya tidak diterapkan. Kita harus membuat gaya terenkapsulasi untuk menata gaya konten Shadow DOM yang dienkapsulasi.

DOM Bayangan

Shadow DOM mencakup gaya CSS untuk setiap pohon bayangan, mengisolasinya dari bagian dokumen lainnya. Artinya, CSS eksternal tidak berlaku untuk komponen Anda, dan gaya komponen tidak berpengaruh pada bagian dokumen lainnya, kecuali jika kami sengaja mengarahkannya.

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

Dengan cakupan elemen kustom, kita tidak perlu khawatir gaya akan merembet ke bagian lain dokumen. Kita dapat mengurangi kekhususan pemilih secara substansial. Misalnya, karena satu-satunya input yang digunakan dalam elemen kustom adalah tombol radio, kita dapat menggunakan input, bukan 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>

Meskipun komponen web dienkapsulasi dengan markup dalam-<template> dan gaya CSS dicakupkan ke shadow DOM serta disembunyikan dari segala hal di luar komponen, konten slot yang dirender, bagian <anyElement slot="star-rating-legend"> dari <star-rating>, tidak dienkapsulasi.

Gaya visual di luar cakupan saat ini

Anda dapat menata gaya dokumen dari dalam shadow DOM dan gaya konten shadow DOM dari gaya global, tetapi tidak mudah. Batas bayangan, tempat shadow DOM berakhir dan DOM reguler dimulai, bisa dilintasi, tetapi hanya dengan sengaja.

Pohon bayangan adalah hierarki DOM di dalam shadow DOM. Akar bayangan adalah simpul akar pohon bayangan.

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

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

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

Jika markup template kita dimulai sedemikian rupa:

<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 bagian yang sama.

Google memiliki checklist yang luar biasa untuk membuat elemen khusus. Anda mungkin juga ingin mempelajari shadow DOM deklaratif.

Menguji pemahaman Anda

Uji pengetahuan Anda tentang template, 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 elemen <template> yang benar?

Elemen umum 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.