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. {i>Widget <i}adalah hal visual yang interaktif. Ini masuk akal untuk memanfaatkan keahlian HTML dan {i>JavaScript<i} Anda saat mengembangkan widget. Standar Komponen Web dirancang untuk membantu Anda yang 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 laman. Kurangnya enkapsulasi berarti stylesheet dokumen Anda mungkin tidak sengaja diterapkan ke bagian di dalam widget; JavaScript 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. DOM Bayangan
  3. Elemen Kustom

Shadow DOM mengatasi masalah enkapsulasi hierarki DOM. Tujuan empat bagian dari Komponen Web dirancang untuk bekerja sama, tetapi Anda juga dapat memilih dan menentukan bagian dari Komponen Web yang akan digunakan. Ini menunjukkan cara menggunakan Shadow DOM.

Halo, Dunia Bayangan

Dengan Shadow DOM, elemen bisa memperoleh jenis simpul baru yang terkait dengan mereka. Jenis node baru ini disebut shadow root. Elemen yang memiliki root bayangan yang terkait dengannya disebut bayangan host. Konten {i>shadow host<i} tidak dirender; isi dari root bayangan 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 {i>JavaScript<i} pada laman menanyakan apa yang textContent, item ini tidak akan “こんたちた影の世界!”, tetapi “Halo, dunia!” karena subhierarki DOM di bawah {i>shadow root<i} dienkapsulasi.

Memisahkan Konten dari Presentasi

Sekarang kita akan melihat penggunaan Shadow DOM untuk memisahkan konten dari presentasi. Misalkan 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 ini markupnya. Inilah yang akan Anda tulis hari ini. Tidak gunakan 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 pohon DOM tidak memiliki enkapsulasi, maka seluruh struktur tag nama diekspos ke dokumen. Jika elemen lain pada laman tidak sengaja menggunakan nama kelas yang sama untuk penataan gaya atau penulisan skrip, kita akan mengalami kesulitan.

Kita dapat menghindari kesulitan.

Langkah 1: Sembunyikan Detail Presentasi

Secara semantik, kita mungkin hanya menganggap:

  • Ini adalah tag nama.
  • Namanya “Bob”.

Pertama, kita tulis markup yang lebih dekat dengan semantik sebenarnya yang diinginkan:

<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 ke dalam elemen <template>, tidak dirender, tetapi dan 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 menyiapkan shadow root, tag nama dirender untuk mencoba lagi perintah. Jika Anda mengklik kanan {i>tag<i} nama dan memeriksa Anda melihat bahwa itu adalah markup semantik yang manis:

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

Ini menunjukkan bahwa, dengan menggunakan Shadow DOM, kita telah menyembunyikan detail penyajian {i>tag<i} nama dari dokumen. Tujuan detail presentasi dienkapsulasi dalam Shadow DOM.

Langkah 2: Pisahkan Konten dari Presentasi

{i>Name tag<i} kita sekarang menyembunyikan detail presentasi dari laman, tetapi sebenarnya tidak memisahkan presentasi dari konten, karena meskipun konten (nama “Bob”) ada di halaman, nama yang dirender adalah yang kita salin ke dalam {i>shadow root<i}. Jika kita ingin mengubah nama pada tag nama, kami harus melakukannya di dua tempat, dan mereka mungkin tidak sinkron.

Elemen HTML bersifat komposisi - Anda dapat meletakkan tombol di dalam tabel, ke titik akhir pelanggan. Komposisi yang kita butuhkan di sini: Tag nama harus berupa latar belakang merah, teks “Hai!” teks, dan konten yang ada di tag nama.

Anda, penulis komponen, menentukan cara komposisi bekerja dengan menggunakan elemen baru yang disebut <content>. Ini membuat titik penyisipan dalam presentasi widget, dan titik penyisipan memilih ceri konten dari {i>host<i} bayangan untuk dipresentasikan pada saat itu.

Jika kita mengubah markup di 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>

Jika tag nama dirender, konten host bayangan akan diproyeksikan ke tempat elemen <content> muncul.

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

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

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

<div id="ex2b">

Sekarang kami telah mencapai pemisahan konten dan presentasi. isi dokumen; presentasi berada di Shadow DOM. Semuanya tetap disinkronkan oleh browser secara otomatis ketika sudah saatnya tiba. untuk merender sesuatu.

Langkah 3: Keuntungan

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

Sekarang jika kita mengubah presentasi, kita tidak perlu mengubah {i>code<i}.

Misalnya, kita ingin melokalkan tag nama kita. Ini masih merupakan nama sehingga konten semantik dalam dokumen tidak berubah:

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

Kode penyiapan root bayangan tetap sama. 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>

Ini adalah peningkatan besar dibandingkan situasi di web saat ini, karena kode pembaruan nama Anda bisa bergantung pada struktur komponen yang sederhana dan konsisten. Nama Anda kode pembaruan tidak perlu mengetahui struktur yang digunakan untuk proses rendering. Jika kita mempertimbangkan apa yang dirender, nama akan muncul kedua dalam bahasa Inggris (setelah “Hai! Nama saya”), tetapi pertama-tama dalam bahasa Jepang (sebelum “と申ます”). Perbedaan itu tidak bermakna secara semantik dari segi pembaruan nama yang sedang ditampilkan, jadi kode pembaruan nama tidak perlu mengetahui tentang detail itu.

Kredit Tambahan: Proyeksi Lanjutan

Dalam contoh di atas, elemen <content> memilih semua konten dari {i>shadow host<i}. Dengan menggunakan select, Anda dapat mengontrol yang diproyeksikan elemen konten. Anda juga dapat menggunakan banyak konten yang kurang penting.

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 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 dengan keduanya elemen <content select="div"> dan <content select=".email">. Berapa kali email Bobi alamat yang muncul, dan dalam warna apa?

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

Alasannya adalah, seperti yang diketahui orang yang meretas Shadow DOM, membangun pohon dari apa yang sebenarnya dirender di layar adalah pesta besar. Elemen konten adalah undangan yang memungkinkan konten dari dokumen ke rendering Shadow DOM di belakang panggung pihak ketiga. Undangan ini dikirim secara berurutan; yang mendapatkan undangan tergantung kepada siapa undangan itu dialamatkan (yaitu, atribut select.) Konten, sekali diundang, selalu menerima undangan (siapa yang tidak akan?!) dan menutupnya berjalan. Jika undangan berikutnya dikirim ke alamat itu lagi, baiklah, tidak ada orang di rumah, dan tidak datang ke pesta Anda.

Dalam contoh di atas, <div class="email"> cocok baik pemilih div maupun .email tetapi karena elemen konten dengan div {i> selector<i} muncul sebelumnya dalam dokumen, <div class="email"> pergi ke pesta kuning, dan tidak ada yang tersedia untuk datang ke pesta biru. (Hal itu mungkin mengapa warnanya begitu biru, meskipun kesengsaraan mencintai perusahaan, sehingga Anda tidak pernah tahu.)

Jika sesuatu diundang ke tidak ada pihak, maka tidak akan dirender. Itulah yang terjadi pada teks “{i>Hello, world<i}” di contoh pertama. Hal ini berguna bila Anda ingin mencapai rendering yang benar-benar berbeda: Tulis model semantik dalam dokumen, yang dapat diakses oleh skrip di laman, tetapi untuk tujuan rendering dan menghubungkannya dengan komputer yang model rendering dalam 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. Tapi bagaimana jika Anda ingin memungkinkan pengguna memilih rentang tanggal untuk makanan penutup mereka liburan pulau (Anda tahu... 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 membuat Shadow DOM yang menggunakan tabel untuk membuat kalender yang apik yang menyoroti rentang tanggal dan sebagainya. Saat pengguna mengklik hari di dalam kalender, komponen memperbarui status di input startDate dan endDate; saat pengguna mengirimkan formulir, nilai dari elemen input itu yang akan dikirimkan.

Mengapa saya menyertakan label dalam dokumen jika tidak dirender? Alasannya adalah jika pengguna melihat formulir dengan browser yang tidak mendukung Shadow DOM, bentuknya masih dapat digunakan, tidak seperti yang 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 Lewati Shadow DOM 101

Itulah dasar-dasar Shadow DOM — Anda meneruskan Shadow DOM 101! Anda dapat lakukan lebih banyak dengan Shadow DOM, misalnya, Anda bisa menggunakan satu {i>host<i} bayangan, atau bayangan bersarang untuk enkapsulasi, atau arsitek halaman Anda menggunakan Tampilan Berbasis Model (MDV) dan DOM Bayangan. Dan Web Komponen lebih dari sekadar Shadow DOM.

Kami akan menjelaskan hal tersebut di postingan selanjutnya.