Pengantar
Komponen Web adalah serangkaian standar mutakhir yang:
- Memungkinkan pembuatan widget
- …yang dapat digunakan kembali dengan andal
- …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:
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.