101 DOM Bayangan

Dominic Cooney
Dominic Cooney

Pengantar

Komponen Web adalah serangkaian standar mutakhir yang:

  1. Memungkinkan pembuatan widget
  2. …yang dapat digunakan kembali dengan andal
  3. …dan tidak akan merusak halaman jika versi komponen berikutnya mengubah detail implementasi internal.

Apakah ini berarti Anda harus memutuskan kapan harus menggunakan HTML/JavaScript, dan kapan harus menggunakan Komponen Web? Tidak! HTML dan JavaScript dapat membuat hal-hal visual yang interaktif. Widget adalah elemen visual interaktif. Sebaiknya manfaatkan keterampilan HTML dan JavaScript Anda saat mengembangkan widget. Standar Komponen Web dirancang untuk membantu Anda melakukannya.

Namun, ada masalah mendasar yang membuat widget yang dibuat dari HTML dan JavaScript sulit digunakan: Hierarki DOM di dalam widget tidak dienkapsulasi dari bagian halaman lainnya. Kurangnya enkapsulasi ini berarti stylesheet dokumen Anda mungkin secara tidak sengaja diterapkan ke bagian di dalam widget; JavaScript Anda mungkin secara tidak sengaja mengubah bagian di dalam widget; ID Anda mungkin tumpang-tindih dengan ID di dalam widget; dan seterusnya.

Komponen Web terdiri dari tiga bagian:

  1. Template
  2. Shadow DOM
  3. Elemen Kustom

Shadow DOM mengatasi masalah enkapsulasi hierarki DOM. Keempat bagian Komponen Web dirancang untuk bekerja sama, tetapi Anda juga dapat memilih bagian Komponen Web mana yang akan digunakan. Tutorial ini menunjukkan cara menggunakan Shadow DOM.

Halo, Dunia Bayangan

Dengan Shadow DOM, elemen bisa mendapatkan jenis node baru yang terkait dengannya. Jenis node baru ini disebut shadow root. Elemen yang memiliki root bayangan yang terkait dengannya disebut host bayangan. Konten shadow host tidak dirender; sebagai gantinya, konten shadow root yang dirender.

Misalnya, jika Anda memiliki markup seperti ini:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

maka alih-alih

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

tampilan halaman Anda

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

Tidak hanya itu, jika JavaScript di halaman menanyakan apa textContent tombol tersebut, JavaScript tidak akan mendapatkan “こんにちは、影の世界!”, tetapi “Hello, world!” karena subhierarki DOM di bawah root shadow dienkapsulasi.

Memisahkan konten dari presentasi

Sekarang kita akan melihat penggunaan Shadow DOM untuk memisahkan konten dari presentasi. Misalnya, kita memiliki tanda nama ini:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Berikut markup-nya. Inilah yang akan Anda tulis hari ini. Elemen ini tidak menggunakan Shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Karena hierarki DOM tidak memiliki enkapsulasi, seluruh struktur tag nama akan terekspos ke dokumen. Jika elemen lain di halaman secara tidak sengaja menggunakan nama class yang sama untuk gaya visual atau pembuatan skrip, kita akan mengalami masalah.

Kita dapat menghindari pengalaman buruk.

Langkah 1: Sembunyikan detail presentasi

Secara semantik, kita mungkin hanya peduli bahwa:

  • Ini adalah tanda pengenal.
  • Namanya adalah “Bob”.

Pertama, kita menulis markup yang lebih dekat dengan semantik sebenarnya yang kita inginkan:

<div id="nameTag">Bob</div>

Kemudian, kita menempatkan semua gaya dan div yang digunakan untuk presentasi ke dalam elemen <template>:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

Pada tahap ini, 'Bob' adalah satu-satunya hal yang dirender. Karena kita memindahkan elemen DOM presentasional ke dalam elemen <template>, elemen tersebut tidak dirender, tetapi dapat diakses dari JavaScript. Kita melakukannya sekarang untuk mengisi root bayangan:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

Setelah kita menyiapkan shadow root, tag nama akan dirender lagi. Jika Anda mengklik kanan tag nama dan memeriksa elemennya, Anda akan melihat bahwa elemen tersebut adalah markup semantik yang bagus:

<div id="nameTag">Bob</div>

Hal ini menunjukkan bahwa, dengan menggunakan Shadow DOM, kita telah menyembunyikan detail presentasi tanda nama dari dokumen. Detail presentasi dienkapsulasi dalam Shadow DOM.

Langkah 2: Pisahkan konten dari presentasi

Tag nama kita sekarang menyembunyikan detail presentasi dari halaman, tetapi tidak benar-benar memisahkan presentasi dari konten, karena meskipun konten (nama “Bob”) ada di halaman, nama yang dirender adalah nama yang kita salin ke shadow root. Jika kita ingin mengubah nama pada tanda pengenal, kita harus melakukannya di dua tempat, dan keduanya mungkin tidak sinkron.

Elemen HTML bersifat komposisional — misalnya, Anda dapat menempatkan tombol di dalam tabel. Komposisi adalah yang kita butuhkan di sini: Tanda nama harus berupa komposisi latar belakang merah, teks “Halo!”, dan konten yang ada di tanda nama.

Anda, sebagai pembuat komponen, menentukan cara kerja komposisi dengan widget menggunakan elemen baru yang disebut <content>. Tindakan ini membuat titik penyisipan dalam presentasi widget, dan titik penyisipan memilih konten dari shadow host untuk ditampilkan pada titik tersebut.

Jika kita mengubah markup di Shadow DOM menjadi seperti ini:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

Saat tag nama dirender, konten shadow host diproyeksikan ke tempat elemen <content> muncul.

Sekarang struktur dokumen menjadi lebih sederhana karena nama hanya ada di satu tempat, yaitu dokumen. Jika halaman Anda perlu memperbarui nama pengguna, Anda cukup menulis:

document.querySelector('#nameTag').textContent = 'Shellie';

dan selesai. Rendering name tag otomatis diperbarui oleh browser, karena kita memproyeksikan konten name tag ke tempatnya dengan <content>.

<div id="ex2b">

Sekarang kita telah mencapai pemisahan konten dan presentasi. Konten berada dalam dokumen; presentasi berada dalam Shadow DOM. Keduanya otomatis disinkronkan oleh browser saat tiba waktunya untuk merender sesuatu.

Langkah 3: Keuntungan

Dengan memisahkan konten dan presentasi, kita dapat menyederhanakan kode yang memanipulasi konten — dalam contoh tanda nama, kode tersebut hanya perlu menangani struktur sederhana yang berisi satu <div>, bukan beberapa.

Sekarang, jika kita mengubah presentasi, kita tidak perlu mengubah kode apa pun.

Misalnya, kita ingin melokalkan tag nama. Tag ini masih berupa tag nama, sehingga konten semantik dalam dokumen tidak berubah:

<div id="nameTag">Bob</div>

Kode penyiapan root bayangan tetap sama. Hanya apa yang dimasukkan ke dalam perubahan root bayangan:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

Hal ini merupakan peningkatan besar dibandingkan dengan situasi di web saat ini, karena kode update nama Anda dapat bergantung pada struktur komponen yang sederhana dan konsisten. Kode pembaruan nama Anda tidak perlu mengetahui struktur yang digunakan untuk rendering. Jika kita mempertimbangkan apa yang dirender, nama akan muncul kedua dalam bahasa Inggris (setelah “Hi! Nama saya adalah”), tetapi yang pertama dalam bahasa Jepang (sebelum “と申します”). Perbedaan tersebut secara semantik tidak berarti dari sudut pandang memperbarui nama yang ditampilkan, sehingga kode pembaruan nama tidak perlu mengetahui detail tersebut.

Kredit tambahan: Proyeksi lanjutan

Pada contoh di atas, elemen <content> memilih semua konten dari shadow host. Dengan menggunakan atribut select, Anda dapat mengontrol apa yang diproyeksikan oleh elemen konten. Anda juga dapat menggunakan beberapa elemen konten.

Misalnya, jika Anda memiliki dokumen yang berisi:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

dan shadow root yang menggunakan pemilih CSS untuk memilih konten tertentu:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

Elemen <div class="email"> dicocokkan oleh elemen <content select="div"> dan <content select=".email">. Berapa kali alamat email Bob muncul, dan dalam warna apa?

Jawabannya adalah alamat email Bob muncul sekali, dan berwarna kuning.

Alasannya adalah, seperti yang diketahui oleh orang-orang yang meretas Shadow DOM, membuat hierarki dari apa yang sebenarnya dirender di layar seperti pesta besar. Elemen konten adalah undangan yang memungkinkan konten dari dokumen masuk ke rendering Shadow DOM di balik layar. Undangan ini dikirimkan secara berurutan; siapa yang mendapatkan undangan bergantung pada kepada siapa undangan tersebut ditujukan (yaitu, atribut select.) Konten, setelah diundang, akan selalu menerima undangan (siapa yang tidak mau?!) dan langsung beraksi. Jika undangan berikutnya dikirim ke alamat itu lagi, tidak ada orang di rumah, dan undangan tersebut tidak akan sampai ke pesta Anda.

Pada contoh di atas, <div class="email"> cocok dengan pemilih div dan pemilih .email, tetapi karena elemen konten dengan pemilih div muncul lebih awal dalam dokumen, <div class="email"> akan masuk ke pesta kuning, dan tidak ada yang tersedia untuk datang ke pesta biru. (Mungkin itulah sebabnya dia sangat sedih, meskipun kesedihan suka ditemani, jadi Anda tidak pernah tahu.)

Jika sesuatu tidak diundang ke pesta mana pun, sesuatu tersebut tidak akan dirender sama sekali. Hal itulah yang terjadi pada teks “Halo dunia” dalam contoh pertama. Hal ini berguna saat Anda ingin mencapai rendering yang sangat berbeda: Tulis model semantik dalam dokumen, yang dapat diakses oleh skrip di halaman, tetapi sembunyikan untuk tujuan rendering dan hubungkan ke model rendering yang sangat berbeda di Shadow DOM menggunakan JavaScript.

Misalnya, HTML memiliki pemilih tanggal yang bagus. Jika Anda menulis <input type="date">, Anda akan mendapatkan kalender pop-up yang rapi. Namun, bagaimana jika Anda ingin mengizinkan pengguna memilih rentang tanggal untuk liburan di pulau makanan penutup mereka (Anda tahu… dengan tempat tidur gantung yang terbuat dari Red Vines.) Anda menyiapkan dokumen dengan cara ini:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

tetapi membuat Shadow DOM yang menggunakan tabel untuk membuat kalender yang rapi yang menandai rentang tanggal dan sebagainya. Saat pengguna mengklik hari di kalender, komponen akan memperbarui status di input startDate dan endDate; saat pengguna mengirimkan formulir, nilai dari elemen input tersebut akan dikirimkan.

Mengapa saya menyertakan label dalam dokumen jika label tersebut tidak akan dirender? Alasannya adalah jika pengguna melihat formulir dengan browser yang tidak mendukung Shadow DOM, formulir tersebut masih dapat digunakan, hanya saja tidak terlihat bagus. Pengguna akan melihat sesuatu seperti:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

Anda lulus Shadow DOM 101

Itulah dasar-dasar Shadow DOM — Anda lulus Shadow DOM 101! Anda dapat melakukan lebih banyak hal dengan Shadow DOM, misalnya, Anda dapat menggunakan beberapa shadow pada satu shadow host, atau shadow bertingkat untuk enkapsulasi, atau merancang halaman Anda menggunakan Model-Driven Views (MDV) dan Shadow DOM. Dan Komponen Web lebih dari sekadar Shadow DOM.

Kami akan menjelaskannya dalam postingan berikutnya.