101 DOM Bayangan

Dominic Cooney
Dominic Cooney

Pengantar

Komponen Web adalah serangkaian standar canggih yang:

  1. Memungkinkan pembuatan widget
  2. …yang dapat digunakan kembali dengan andal
  3. …dan yang tidak akan merusak halaman jika versi berikutnya dari komponen 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 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 lain halaman tersebut. Kurangnya enkapsulasi ini berarti stylesheet dokumen Anda mungkin tidak sengaja diterapkan ke bagian di dalam widget; JavaScript Anda mungkin 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 Web Components dirancang untuk bekerja sama, tetapi Anda juga dapat memilih bagian Web Components mana yang akan digunakan. Tutorial ini menunjukkan cara menggunakan Shadow DOM.

Halo, Dunia Bayangan

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

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 textContent tombol, JavaScript tidak akan mendapatkan “こんにちは、影の世界!”, tetapi “Hello, world!” karena sub-pohon DOM di bawah root bayangan dienkapsulasi.

Memisahkan Konten dari Presentasi

Sekarang kita akan melihat penggunaan Shadow DOM untuk memisahkan konten dari presentasi. Misalkan kita memiliki badge 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. Ini adalah yang akan Anda tulis hari 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 ditampilkan ke dokumen. Jika elemen lain di halaman tidak sengaja menggunakan nama class yang sama untuk gaya atau skrip, kita akan mengalami masalah.

Kita dapat menghindari pengalaman yang buruk.

Langkah 1: Sembunyikan Detail Presentasi

Secara semantik, kita mungkin hanya peduli bahwa:

  • Ini adalah tag nama.
  • Namanya “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 presentasi di 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 menyiapkan root bayangan, tag nama akan dirender lagi. Jika Anda mengklik kanan tag nama dan memeriksa elemen, Anda akan melihat bahwa markup semantiknya bagus:

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

Hal ini menunjukkan bahwa, dengan menggunakan Shadow DOM, kita telah menyembunyikan detail presentasi tag 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 sebenarnya tidak memisahkan presentasi dari konten, karena meskipun konten (nama "Bob") ada di halaman, nama yang dirender adalah nama yang kita salin ke shadow root. Jika ingin mengubah nama pada tag nama, 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 hal yang kita butuhkan di sini: Tag nama harus berupa komposisi latar belakang merah, teks “Halo!”, dan konten yang ada di tag nama.

Anda, penulis komponen, menentukan cara kerja komposisi dengan widget menggunakan elemen baru yang disebut <content>. Tindakan ini akan membuat titik penyisipan dalam presentasi widget, dan titik penyisipan akan memilih konten dari host bayangan 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 host bayangan akan diproyeksikan ke tempat elemen <content> muncul.

Sekarang struktur dokumen lebih sederhana karena namanya hanya ada di satu tempat — dokumen. Jika halaman Anda perlu memperbarui nama pengguna, cukup tulis:

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

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

<div id="ex2b">

Sekarang kita telah mencapai pemisahan konten dan presentasi. Konten ada dalam dokumen; presentasi ada di Shadow DOM. Keduanya akan 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 tag 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 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>

Ini adalah peningkatan besar dibandingkan situasi di web saat ini, karena kode pembaruan 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 di urutan kedua dalam bahasa Inggris (setelah “Halo! Nama saya adalah”), tetapi pertama-tama dalam bahasa Jepang (sebelum “と申します”). Perbedaan tersebut tidak bermakna secara semantik 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 host bayangan. Dengan menggunakan atribut select, Anda dapat mengontrol apa yang diproyeksikan elemen konten. Anda juga dapat menggunakan beberapa elemen konten.

Misalnya, jika Anda memiliki dokumen yang berisi hal berikut:

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

dan root bayangan 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 Budi muncul, dan dalam warna apa?

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

Alasannya adalah, seperti yang diketahui oleh orang-orang yang melakukan peretasan pada 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 pihak rendering Shadow DOM di belakang panggung. Undangan ini dikirimkan secara berurutan; siapa yang mendapatkan undangan bergantung pada kepada siapa undangan tersebut ditujukan (yaitu, atribut select). Konten, setelah diundang, selalu menerima undangan (siapa yang tidak?!) dan langsung dikirim. Jika undangan berikutnya dikirim ke alamat tersebut lagi, ternyata tidak ada orang di rumah, dan undangan tersebut tidak akan datang 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 grup kuning, dan tidak ada yang tersedia untuk masuk ke grup biru. (Mungkin itulah alasannya warnanya sangat biru, meskipun penderitaan itu lebih baik bersama, jadi Anda tidak pernah tahu.)

Jika sesuatu diundang ke tidak ada pihak, hal tersebut tidak akan dirender sama sekali. Itulah yang terjadi pada teks “Hello, world” dalam contoh pertama. Cara 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 menghubungkannya 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. Tetapi bagaimana jika Anda ingin pengguna memilih rentang tanggal untuk liburan makanan penutup di pulau mereka (dengan tempat tidur gantung yang terbuat dari Pohon Anggur Merah). 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 buat 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 dalam input startDate dan endDate; saat pengguna mengirimkan formulir, nilai dari elemen input tersebut akan dikirimkan.

Mengapa saya menyertakan label dalam dokumen jika tidak akan dirender? Alasannya adalah jika pengguna melihat formulir dengan browser yang tidak mendukung Shadow DOM, formulir tersebut masih dapat digunakan, tetapi tidak begitu bagus. Pengguna 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 Lewati Shadow DOM 101

Itulah dasar-dasar Shadow DOM — Anda telah lulus Shadow DOM 101! Anda dapat melakukan lebih banyak hal dengan Shadow DOM, misalnya, Anda dapat menggunakan beberapa bayangan di satu host bayangan, atau bayangan bertingkat untuk enkapsulasi, atau merancang halaman menggunakan Tampilan yang Didorong Model (MDV) dan Shadow DOM. Selain itu, Komponen Web lebih dari sekadar Shadow DOM.

Kami akan menjelaskan hal tersebut di postingan selanjutnya.