101 DOM Bayangan

Dominic Cooney
Dominic Cooney

Pengantar

Komponen Web adalah seperangkat standar canggih yang:

  1. Memungkinkan pembuatan widget
  2. ...yang dapat digunakan kembali dengan andal
  3. ...dan yang 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 hal visual yang interaktif. Sebaiknya manfaatkan keterampilan HTML dan JavaScript saat mengembangkan widget. Standar Komponen Web dirancang untuk membantu Anda melakukan hal tersebut.

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 mungkin secara tidak sengaja diterapkan pada 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, namun Anda juga dapat memilih dan menentukan bagian mana dari Komponen Web 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 root bayangan. Elemen yang memiliki root bayangan yang terkait dengannya disebut host bayangan. Konten host bayangan tidak dirender; konten root bayangan akan 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 daripada

<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>

halaman Anda terlihat seperti

<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, JavaScript tidak akan mendapatkan “こんすち');{/影の世界!”, tetapi “Halo, dunia!” karena subhierarki DOM di bawah root bayangan dienkapsulasi.

Memisahkan Konten dari Presentasi

Sekarang kita akan melihat penggunaan Shadow DOM untuk memisahkan konten dari presentasi. Katakanlah kita memiliki tag 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 markupnya. Itulah yang akan Anda tulis hari ini. Contoh 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 diekspos ke dokumen. Jika elemen lain di halaman tidak sengaja menggunakan nama class yang sama untuk gaya visual atau pembuatan skrip, kita akan mengalami masalah.

Kita bisa menghindari masalah.

Langkah 1: Sembunyikan Detail Presentasi

Secara semantik, kita mungkin hanya memperhatikan bahwa:

  • Ini adalah tag nama.
  • Namanya "Bob".

Pertama, kita menulis markup yang lebih mendekati 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, 'Budi' adalah satu-satunya hal yang dirender. Karena kita memindahkan elemen DOM presentasional di dalam elemen <template>, elemen tersebut tidak dirender, tetapi dapat diakses dari JavaScript. Kita melakukannya sekarang untuk mengisi shadow root:

<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 elemen, Anda akan melihat bahwa itu adalah markup semantik yang manis:

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

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

Langkah 2: Memisahkan 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 root bayangan. Jika ingin mengubah nama pada tag nama, kita harus melakukannya di dua tempat, dan nama tersebut mungkin tidak sinkron.

Elemen HTML bersifat komposisional — Anda dapat meletakkan tombol di dalam tabel, misalnya. Komposisi adalah yang kita butuhkan di sini: Tag nama harus berupa komposisi latar belakang merah, teks “Hi!”, dan konten yang ada pada tag nama.

Anda, sebagai 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 mengambil konten dari shadow host untuk ditampilkan pada titik tersebut.

Jika kita mengubah markup dalam Shadow DOM menjadi 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 diproyeksikan ke tempat elemen <content> muncul.

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

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

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

<div id="ex2b">

Sekarang, kami telah memisahkan konten dan presentasi. Konten ada dalam dokumen; presentasi ada dalam Shadow DOM. Semuanya akan otomatis disinkronkan oleh browser ketika tiba waktunya untuk merender sesuatu.

Langkah 3: Laba

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, tidak perlu mengubah kodenya!

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

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

Kode penyiapan root bayangan tetap sama. Apa yang dimasukkan ke dalam akar bayangan berubah:

<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 terhadap 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 tersebut akan muncul kedua dalam bahasa Inggris (setelah “ Nama saya"), tetapi pertama dalam bahasa Jepang (sebelum “と申すん memikirkan”). Perbedaan itu secara semantik tidak bermakna 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> mengambil semua konten dari host bayangan. Dengan menggunakan atribut select, Anda dapat mengontrol apa saja yang diproyeksikan oleh 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 dengan elemen <content select="div"> dan <content select=".email">. Berapa kali alamat email Bobi muncul, dan dalam warna apa?

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

Alasannya adalah karena orang yang meretas Shadow DOM tahu, membangun hierarki dari apa yang sebenarnya dirender di layar ibarat pihak besar. Elemen konten adalah undangan yang memungkinkan konten dari dokumen ke dalam pihak rendering DOM Shadow di belakang tahap. Undangan ini akan dikirimkan secara berurutan; siapa yang akan menerima undangan bergantung kepada siapa undangan tersebut dialamatkan (yaitu, atribut select). Konten, setelah diundang, selalu menerima undangan (siapa yang tidak mau?!) dan berhenti digunakan. Jika undangan berikutnya dikirim ke alamat itu lagi, memang, tidak ada orang di rumah, dan tidak 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 di dokumen, <div class="email"> akan mengarah ke pihak kuning, dan tidak ada yang bisa datang ke pihak biru. (Itu mungkin mengapa warnanya begitu biru, meskipun kesengsaraan menyukai perusahaan, jadi Anda tidak pernah tahu.)

Jika tidak ada pihak yang diundang, sesuatu tidak akan dirender sama sekali. Itulah yang terjadi pada teks “Halo, dunia” dalam contoh pertama. Hal ini berguna jika Anda ingin menghasilkan rendering yang benar-benar 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 menulis <input type="date">, Anda akan mendapatkan kalender pop-up yang rapi. Namun, bagaimana jika Anda ingin mengizinkan pengguna memilih rentang tanggal untuk liburan ke pulau hidangan penutup (Anda tahu... dengan tempat tidur gantung yang terbuat dari Red Vine). 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 apik yang menyoroti rentang tanggal dan seterusnya. Jika 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 tidak akan dirender? Alasannya adalah jika pengguna melihat formulir dengan browser yang tidak mendukung Shadow DOM, formulir tersebut masih dapat digunakan, hanya saja tidak terlalu menarik. 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 Meneruskan Shadow DOM 101

Itulah dasar-dasar Shadow DOM — Anda teruskan Shadow DOM 101! Anda dapat melakukan lebih banyak hal dengan Shadow DOM. Misalnya, Anda dapat menggunakan beberapa bayangan pada satu host bayangan, atau bayangan bertingkat untuk enkapsulasi, atau merancang halaman menggunakan Tampilan Berbasis Model (MDV) dan Shadow DOM. Dan Komponen Web lebih dari sekadar Shadow DOM.

Kami akan menjelaskannya di postingan selanjutnya.