Modul 3 - Web Components - Google Dokumen
Modul 3 - Web Components - Google Dokumen
meminimalisir kode yang berulang karena menerapkan teknik components dan modules.
Component jadi hal yang sangat populer karena dengannya, kita dapat mudah memasang dan
mempreteli kumpulan element (component) pada website.
Component bersifat reusable, sehingga kita bisa menggunakanya pada banyak project tanpa
harus membuat ulang. Bahkan kita dapat menggunakan component yang dibuat dan dibagikan
oleh orang lain. Inilah mengapa Front-End Framework seperti React, Angular, ataupun Vue
sangat populer karena terdapat penerapan component di dalamnya.
Sejak dulu setiap framework atau library pasti memiliki caranya sendiri dalam
penggunaan/pembuatannya. Termasuk dalam penggunaan/pembuatan component-nya.
Sehingga component akan bersifat reusable, dengan catatan selama masih dalam framework
yang sama. Apakah itu menjadi masalah? Tentu!
Terlalu nyaman dalam salah satu framework yang digunakan akan menjadi masalah. Karena
jika kita berada di framework yang berbeda, komponen yang kita biasa gunakan belum tentu
dapat digunakan pada framework tersebut. Contohnya, Jika kita menuliskan library Angular
dan kita ingin ia berfungsi pada framework Vue? Apakah bisa? Maka dari itu kita perlu
menuliskan berdasarkan standar umum dalam membuat komponen sehingga dapat digunakan
oleh framework dan browser manapun.
Web component merupakan salah satu fitur standar yang terdapat pada Browser API. Dengan
ini kita jadi mudah membuat component UI yang bersifat reusable. Pada materi kali ini, kita
akan membahas seputar web component mulai dari bagaimana membuatnya hingga
menerapkannya pada pada proyek Club Finder.
● Standard : Web Component merupakan standar yang ditetapkan oleh WC3 dalam
membuat komponen yang reusable.
● Compatibility : Karena web component merupakan standard maka dapat digunakan
pada framework seperti Angular, React, ataupun Vue.
● Simple : Menggunakan web component tidak memerlukan konfigurasi khusus layaknya
framework yang ada. Karena web component dibangun tak lain hanya menggunakan
JS/CSS/HTML murni.
Web component bersifat reusable. Bahkan dapat digunakan walaupun kita menggunakan
framework sekalipun. Apa pasal? Web component dibangun tak lain menggunakan
JS/HTML/CSS murni. Terdapat dua API penting dalam menerapkan web component, yakni:
● Custom Elements: Digunakan untuk membuat elemen baru (custom element). Kita juga
bisa menentukan perilaku element tersebut sesuai kebutuhan.
● Shadow DOM: Digunakan untuk membuat HTML element terenkapsulasi dari gangguan
luar. Biasanya digunakan pada custom element, agar elemen tersebut tidak terpengaruh
oleh styling yang ditetapkan di luar dari custom elemen-nya.
Custom Element
HTML memberikan kemudahan dalam mengatur struktur website. Untungnya seluruh browser
sepakat untuk menggunakannya. Pada kelas Belajar Dasar Pemrograman Web kita sudah
belajar penulisan dan penggunaan tag pada HTML dalam membuat struktur website.
Ada banyak sekali tags HTML yang dapat kita manfaatkan secara langsung. Apakah Anda tahu
pada HTML5 hampir terdapat 100 tag standar yang bisa kita gunakan? Karena banyaknya tag
HTML yang tersedia, seharusnya kita bisa membuat struktur website yang memiliki arti
(semantic meaning).
Untuk membuat struktur website memiliki arti, kita harus tahu HTML tag mana yang tepat untuk
digunakan. Sebelum HTML5, hampir seluruh bagian pembentuk layout pada website dibuat
menggunakan tag <div>. Baik itu untuk header, footer, artikel, ataupun konten samping.
Setelah hadirnya HTML5, kita dikenalkan pada beberapa elemen yang dapat digunakan dalam
mengelompokkan sebuah elemen dengan lebih jelas dan memiliki arti (semantic meaning).
Elemen-elemen ini memiliki nama sesuai dengan fungsi atau peran dari elemen tersebut.
Element div memang digunakan untuk mencakup elemen yang belum atau tidak tersedia.
Biasanya kita menyiasati penggunaan tag div dengan menambahkan attribute id ataupun class
untuk menunjukkan fungsinya. Namun dalam penulisan nilai atributnya, terkadang kita tidak
memiliki standar khusus sehingga kode tersebut akhirnya hanya kita sendirilah yang
mengetahuinya.
Namun sekarang, penggunaan elemen <div> pada website seharusnya dapat lebih
diminimalisir lagi. Dengan membuat dan menggunakan custom element, struktur HTML kita
dapat dibaca lebih mudah.
(Kiri) Struktur HTML dengan menggunakan tag <div>. (Kanan) Struktur HTML dengan Custom
Element.
Dengan Custom Element kita dapat membuat struktur elemen HTML yang lebih rapi lagi.
Karena dengannya, kita dapat membuat DOM element kita sendiri sesuai kebutuhan. Anda
melihat contoh penerapannya pada gambar di atas.
Dalam membuat custom element, kita menuliskannya dengan menggunakan JavaScript class.
Class tersebut mewarisi sifat dari HTMLElement. HTMLElement merupakan interface yang
merepresentasikan element HTML. Interface ini biasanya diterapkan pada class JavaScript
sehingga terbentuklah element HTML baru melalui class tersebut (custom element).
Yeay! ImageFigure sekarang merupakan sebuah HTML element baru. Namun tunggu dulu.
Untuk menggunakannya pada berkas HTML, kita perlu menetapkan nama tag yang nantinya
digunakan pada HTML. Caranya dengan menggunakan variabel customElements seperti ini:
customElements.define("image-figure", ImageFigure);
“Dalam penamaan tag untuk custom element, nama tag harus terdiri dari dua kata yang
dipisahkan oleh dash (-). Jika tidak, pembuatan custom element tidak akan berhasil. Hal Ini
diperlukan untuk memberi tahu browser perbedaan antara elemen asli HTML dan custom
element.”
Setelah mendefinisikan custom element, barulah ia siap digunakan pada berkas HTML. Kita
cukup menuliskan tagnya layaknya elemen HTML biasa.
<image-figure></image-figure>
Jangan lupa lampirkan script pada berkas yang digunakan untuk menuliskan class
ImageFigure.
<script src="image-figure.js"></script>
# Multi tab
## index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>My First Custom Element</title>
</head>
<body>
<image-figure></image-figure>
<script src="image-figure.js"></script>
</body>
</html>
## image-figure.js
customElements.define("image-figure", ImageFigure);
Coba jalankan kode di atas pada browser, kita tidak akan mendapatkan apapun. Sampai saat
ini, element <image-figure> berperan layaknya element <div> ataupun <span> yang tidak
memiliki fungsi khusus. Karena kita belum menetapkan seperti apa jadinya element baru ini.
Untuk menetapkan seperti apa fungsi dari elemen baru, kita lakukan semuanya dengan
menggunakan kode JavaScript yang dituliskan di dalam class ImageFigure. Tapi sebelum itu,
kita pelajari dulu siklus hidup (life cycle) dari elemen HTML.
Ketika sebuah JavaScript class mewarisi sifat dari HTMLElement maka class tersebut akan
memiliki siklus hidup layaknya sebuah elemen HTML. Kita dapat menerapkan logika pada
setiap siklus hidup yang ada dengan memanfaatkan lifecycle callbacks yang ada. Berikut ini
lifecycle callbacks yang ada pada HTMLElement:
Untuk mempermudah memahami urutan siklus hidup element pada HTML kita bisa lihat pada
ilustrasi berikut:
Walaupun sebenarnya constructor() bukan termasuk siklus hidup HTML Element, namun
fungsi tersebut sering digunakan untuk melakukan konfigurasi awal ketika pertama kali element
dibuat. Seperti menentukan event listener, atau menetapkan Shadow DOM.
Ketika kita mengimplementasikan constructor pada custom element, kita wajib memanggil
method super(). Jika tidak, maka akan menghasilkan error:
Class yang merupakan custom element lebih ketat dibandingkan class lain. Kita tidak dapat
membuat argument pada constructor class-nya. Karena instance dibuat tidak menggunakan
keyword new seperti class JavaScript umumnya.
Terdapat dua cara membuat instance dari custom element. Yang pertama adalah menggunakan
nama tagnya langsung yang dituliskan pada kode HTML. Contohnya:
<body>
<image-figure></image-figure>
</body>
Lalu cara kedua adalah dengan menggunakan sintaks JavaScript. Sama seperti membuat
element HTML biasa, kita gunakan method document.createElement dalam membuat elemen
baru.
Kita bisa mencobanya sendiri dengan menuliskan kode-kode berikut dan menjalankannya pada
browser. Kemudian lihat output yang dihasilkan pada browser. Output tersebut akan
menunjukan urutan siklus hidup yang terpanggil.
#Multi tab
# index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Element life cycle</title>
</head>
<body>
<!-- silakan hapus tag <image-figure> untuk membuatnya secara sintaks
JavaScript -->
<image-figure></image-figure>
<script src="my-custom-element.js"></script>
<script src="main.js"></script>
</body>
</html>
# image-figure.js
disconnectedCallback() {
console.log("disconnected!");
}
adoptedCallback() {
console.log("adopted!");
}
customElements.define("image-figure", ImageFigure);
# main.js
ement.remove();
}, 3000);
# output console
# html structure
Implementasi lifecycle callback pada custom element bersifat opsional. Kita tidak perlu
menuliskannya jika memang tidak diperlukan.
connectedCallback() {
this.src = this.getAttribute("src") || null;
this.alt = this.getAttribute("alt") || null;
this.caption = this.getAttribute("caption") || null;
this.innerHTML = `
<figure>
<img src="${this.src}"
alt="${this.alt}">
<figcaption>${this.caption}</figcaption>
</figure>
`;
}
}
customElements.define("image-figure", ImageFigure);
Dari kode di atas ketika element <image-figure> tampak pada DOM, maka ia akan
mendapatkan nilai yang ditetapkan pada atribut src, alt, dan caption. Kemudian nilai atribut
tersebut akan ditampilkan dalam format elemen <figure> dengan memanfaatkan innerHTML.
Untuk memberikan atribut dan nilainya pada custom element, tidak ada bedanya dengan
element HTML biasa. Kita bisa melakukannya langsung pada elemennya, atau melalui
JavaScript.
#multi tab
## tag-html
<image-figure
src="https://i.imgur.com/iJq78XH.jpg"
alt="Dicoding Logo"
caption="Huruf g dalam logo Dicoding">
</image-figure>
## JavaScript
const imageFigureElement = document.createElement("image-figure");
imageFigureElement.setAttribute("src", "https://i.imgur.com/iJq78XH.jpg");
imageFigureElement.setAttribute("alt", "Dicoding Logo");
imageFigureElement.setAttribute("caption", "Huruf g dalam logo Dicoding")
Dengan custom elemen ini kita bisa membuat elemen <figure> tanpa harus menuliskan lagi
element <img> dan <figcaption> di dalamnya. Cukup gunakan custom elemen ini dengan
menetapkan nilai atribut yang dibutuhkan. Sudah bisa melihat kerennya custom elemen?
Fungsi custom elemen bukan hanya membuat fungsi elemen baru, namun bisa juga dibuat
untuk mempermudah penggunaan HTML yang ada.
Pada contoh sebelumnya, kita telah membuat element <image-figure> yang berfungsi
layaknya element <figure> dengan penggunaan yang lebih sederhana. Contohnya kita memiliki
kode seperti ini:
## Multi tab
## index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Attribute Observe</title>
</head>
<body>
<image-figure
src="https://i.imgur.com/iJq78XH.jpg"
alt="Dicoding Logo"
caption="Huruf g dalam logo Dicoding">
</image-figure>
<script src="image-figure.js"></script>
</body>
</html>
# image-figure.js
connectedCallback() {
this.src = this.getAttribute("src") || null;
this.alt = this.getAttribute("alt") || null;
this.caption = this.getAttribute("caption") || null;
this.innerHTML = `
<figure>
<img src="${this.src}"
alt="${this.alt}">
<figcaption>${this.caption}</figcaption>
</figure>
`;
}
}
customElements.define("image-figure", ImageFigure);
Lalu kita buka pada browser, maka akan tampak seperti ini:
Lalu mari kita coba mengubah nilai atribut caption dengan nilai baru menggunakan JavaScript
melalui console browser.
connectedCallback() {
this.src = this.getAttribute("src") || null;
this.alt = this.getAttribute("alt") || null;
this.caption = this.getAttribute("caption") || null;
this.render();
}
render() {
this.innerHTML = `
<figure>
<img src="${this.src}"
alt="${this.alt}">
<figcaption>${this.caption}</figcaption>
</figure>
`;
}
customElements.define("image-figure", ImageFigure);
Mari kita telaah kodenya satu persatu. Di dalam fungsi attributeChangedCallback(), kita
tuliskan kode untuk mengubah nilai properti this[name] dengan nilai baru yang ditetapkan.
this[name] ini merupakan cara dinamis untuk mengubah nilai properti sesuai dengan nama
atribut yang diubah. Misalkan jika kita mengubah atribut “caption” maka akan mengubah nilai
this[“caption”], jika kita mengubah atribut “alt” maka akan mengubah nilai this[“alt”].
Setelah mengubah nilainya lalu kita panggil fungsi render(). Perhatikan juga bahwa kita perlu
memisahkan kode rendering HTML di browser pada fungsi yang terpisah (tidak dilakukan di
connectedCallback). Tujuannya agar kita dapat memanggil fungsi tersebut tidak hanya sekali
tapi setiap kali terdapat perubahan data.
Lalu terdapat juga static get observedAttributes(). Apa fungsinya? Fungsi getter statis ini
berperan sebagai “seseorang” yang mengamati perubahan nilai pada atribut yang ditentukan.
Jika terjadi perubahan, ia akan memanggil attributeChangedCallback dengan memberitahu
nama atribut apa yang berubah, nilai sebelum perubahan, serta nilai baru yang akan ditetapkan.
observedAttributes tidak akan mengamati seluruh atribut yang diterapkan pada custom
element, hanya atribut yang dituliskan pada nilai kembaliannya yang akan diamati.
return ["caption"];
Nilai kembalian dari observedAttributes merupakan array. Jika kita ingin mengamati lebih dari
satu atribut, kita dapat menuliskannya layaknya array literals.
Setelah mengimplementasi kedua fungsi tadi seharusnya custom element sudah dapat bereaksi
ketika terjadi perubahan nilai atribut.
Styling Custom Element without Shadow DOM
Potongan kode untuk materi ini:
https://repl.it/@dicodingacademy/163-03-styling-without-shadow-dom?lite=true
Tidak ada cara khusus dalam menetapkan styling pada custom elemen jika tidak menerapkan
Shadow DOM. Kita dapat menetapkan styling dengan cara yang sama seperti standar element
HTML. Dalam arti kita bisa menggunakan nama tag sebagai selector, atribut id ataupun class
sebagai selector-nya.
Pada custom element biasanya kita menuliskan styling dengan menggunakan tag <style> pada
saat merender template HTML menggunakan innerHTML.
......
render() {
this.innerHTML = `
<style>
figure {
border: thin #c0c0c0 solid;
display: flex;
flex-flow: column;
padding: 5px;
max-width: 220px;
margin: auto;
}
......
}
customElements.define("image-figure", ImageFigure);
Ataupun dengan menuliskan styling pada berkas css yang ditautkan pada html.
/* Berkas style.css */
figure {
border: thin #c0c0c0 solid;
display: flex;
flex-flow: column;
padding: 5px;
max-width: 220px;
margin: auto;
}
Sebelumnya kita sudah belajar bagaimana custom element menampilkan data melalui atribut.
Seperti yang kita ketahui, nilai dari atribut pada elemen lazimnya hanya data primitif. Namun
bagaimana jika custom elemen membutuhkan data yang kompleks atau memiliki nilai yang
banyak seperti ini?
const article = {
id: 1,
title: "Lorem Ipsum Dolor",
featuredImage: "https://i.picsum.photos/id/204/536/354.jpg?grayscale",
description: "Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard dummy text ever since
the 1500s, when an unknown printer took a galley of type and scrambled it to
make a type specimen book. It has survived not only five centuries, but also
the leap into electronic typesetting, remaining essentially unchanged. It was
popularised in the 1960s with the release of Letraset sheets containing Lorem
Ipsum passages, and more recently with desktop publishing software like Aldus
PageMaker including versions of Lorem Ipsum."
}
Tentu jika kita simpan nilai tersebut pada atribut HTML akan terlihat berantakan pada penulisan
elemennya.
<article-item
id="1"
title="Lorem Ipsum Dolor"
featured-image="https://i.picsum.photos/id/204/536/354.jpg?grayscale"
description="Lorem Ipsum is simply dummy text of the printing and
typesetting industry. Lorem Ipsum has been the industry's standard dummy
text ever since the 1500s, when an unknown printer took a galley of type
and scrambled it to make a type specimen book. It has survived not only
five centuries, but also the leap into electronic typesetting, remaining
essentially unchanged. It was popularised in the 1960s with the release of
Letraset sheets containing Lorem Ipsum passages, and more recently with
desktop publishing software like Aldus PageMaker including versions of
Lorem Ipsum."></article-item>
Cukup merepotkan bukan? Hal ini malah menghilangkan tujuan dari custom elemen yakni
semakin mudah kita baca. Lantas bagaimana cara menangani data kompleks yang dibutuhkan
oleh custom element?
Karena pembuatan custom element ini memanfaatkan sebuah JavaScript class, kita bisa
memanfaatkan dengan menyimpan data tersebut pada properti class. Masih ingat pembahasan
properti accessor atau getter/setter? Nah, dengan teknik ini kita dapat menetapkan data yang
kompleks pada custom element.
render() {
this.innerHTML = `
<img class="featured-image" src="${this._article.featuredImage}">
<div class="article-info">
<h2><a href="${this._article.id}">${this._article.title}</a></h2>
<p>${this._article.description}</p>
</div>
`;
}
}
customElements.define("article-item", ArticleItem);
Dengan begitu tentu kita hanya bisa menetapkan data pada custom element melalui sintaks
JavaScript dengan mengakses properti .article seperti ini:
Cukup mudah bukan? Karena kita memanggil fungsi render() di dalam set article(), maka
custom element tidak akan menampilkan apapun pada browser sebelum nilai article-nya
ditetapkan. Penasaran? Berikut kode lengkapnya jika Anda ingin mencobanya sendiri:
# Multi tab
## index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Handling Complex Data</title>
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="container">
</div>
<script src="script.js" type="module"></script>
</body>
</html>
## style.css
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
padding: 16px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
article-item {
display: block;
margin-bottom: 18px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
border-radius: 10px;
overflow: hidden;
}
## script.js
import "./article-item.js";
import article from "./article.js";
containerElement.appendChild(articleItemElement);
## article-item.js
render() {
this.innerHTML = `
<img class="featured-image" src="${this._article.featuredImage}">
<div class="article-info">
<h2><a href="${this._article.id}">${this._article.title}</a></h2>
<p>${this._article.description}</p>
</div>
`;
}
}
customElements.define("article-item", ArticleItem);
## article.js
const article = {
id: 1,
title: "Lorem Ipsum Dolor",
featuredImage: "https://i.picsum.photos/id/204/536/354.jpg?grayscale",
description: "Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard dummy text ever since
the 1500s, when an unknown printer took a galley of type and scrambled it to
make a type specimen book. It has survived not only five centuries, but also
the leap into electronic typesetting, remaining essentially unchanged. It was
popularised in the 1960s with the release of Letraset sheets containing Lorem
Ipsum passages, and more recently with desktop publishing software like Aldus
PageMaker including versions of Lorem Ipsum."
}
Jika kita jalankan maka browser akan menampilkan element <article-item> dengan data yang
didapat dari article.js.
Nested Custom Element
Potongan kode untuk materi ini:
https://repl.it/@dicodingacademy/163-03-nested-custom-element?lite=true
Ketika menggunakan custom element, mungkin terdapat keadaan di mana kita membutuhkan
custom element berada di dalam custom element lain. Contohnya, banyak website saat ini yang
menampilkan data berupa list, entah itu daftar artikel ataupun item belanja.
Biasanya setiap daftar yang ditampilkan ditampung dalam sebuah container <div>. Kemudian
item yang sama ditampilkan secara berulang dengan data yang berbeda pada container
tersebut.
Web component dapat memudahkan dalam mengorganisir daftar item yang ditampilkan dalam
bentuk list menggunakan container. Caranya kita membuat dua custom element yatu container,
dan itemnya. Container digunakan untuk menampung elemen item di dalamnya. Selain itu pada
container juga data (array) diberikan. Nantinya container-lah yang akan membuat elemen item
di dalamnya berdasarkan data yang diberikan.
Belum terbayang seperti apa? Berikut contohnya:
# Multi tab
## index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>repl.it</title>
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<script src="script.js" type="module"></script>
</body>
</html>
## script.js
import "./article-list.js";
import articles from "./articles.js";
document.body.appendChild(articleListElement);
## article-list.js
import "./article-item.js"
## article-item.js
render() {
this.innerHTML = `
<img class="featured-image" src="${this._article.featuredImage}">
<div class="article-info">
<h2><a href="${this._article.id}">${this._article.title}</a></h2>
<p>${this._article.description}</p>
</div>
`;
}
}
customElements.define("article-item", ArticleItem);
## articles.js
const articles = [
{
id: 1,
title: "Lorem Ipsum Dolor",
featuredImage: "https://i.picsum.photos/id/204/536/354.jpg",
description: "Lorem Ipsum is simply dummy text of the printing and
typesetting industry. Lorem Ipsum has been the industry's standard dummy text
ever since the 1500s, when an unknown printer took a galley of type and
scrambled it to make a type specimen book. It has survived not only five
centuries, but also the leap into electronic typesetting, remaining essentially
unchanged. It was popularised in the 1960s with the release of Letraset sheets
containing Lorem Ipsum passages, and more recently with desktop publishing
software like Aldus PageMaker including versions of Lorem Ipsum."
},
{
id: 2,
title: "Lorem Ipsum Dolor",
featuredImage: "https://i.picsum.photos/id/209/536/354.jpg",
description: "Lorem Ipsum is simply dummy text of the printing and
typesetting industry. Lorem Ipsum has been the industry's standard dummy text
ever since the 1500s, when an unknown printer took a galley of type and
scrambled it to make a type specimen book. It has survived not only five
centuries, but also the leap into electronic typesetting, remaining essentially
unchanged. It was popularised in the 1960s with the release of Letraset sheets
containing Lorem Ipsum passages, and more recently with desktop publishing
software like Aldus PageMaker including versions of Lorem Ipsum."
},
{
id: 3,
title: "Lorem Ipsum Dolor",
featuredImage: "https://i.picsum.photos/id/206/536/354.jpg",
description: "Lorem Ipsum is simply dummy text of the printing and
typesetting industry. Lorem Ipsum has been the industry's standard dummy text
ever since the 1500s, when an unknown printer took a galley of type and
scrambled it to make a type specimen book. It has survived not only five
centuries, but also the leap into electronic typesetting, remaining essentially
unchanged. It was popularised in the 1960s with the release of Letraset sheets
containing Lorem Ipsum passages, and more recently with desktop publishing
software like Aldus PageMaker including versions of Lorem Ipsum."
},
{
id: 4,
title: "Lorem Ipsum Dolor",
featuredImage: "https://i.picsum.photos/id/212/536/354.jpg",
description: "Lorem Ipsum is simply dummy text of the printing and
typesetting industry. Lorem Ipsum has been the industry's standard dummy text
ever since the 1500s, when an unknown printer took a galley of type and
scrambled it to make a type specimen book. It has survived not only five
centuries, but also the leap into electronic typesetting, remaining essentially
unchanged. It was popularised in the 1960s with the release of Letraset sheets
containing Lorem Ipsum passages, and more recently with desktop publishing
software like Aldus PageMaker including versions of Lorem Ipsum."
}
]
Pada kode di atas kita bisa melihat bahwa terdapat dua buah custom component yaitu
<article-list> dan <article-item>. Pada article-list.js terdapat fungsi setter articles yang
berfungsi untuk menyimpan nilai articles pada properti this._articles.
set articles(articles) {
this._articles = articles;
this.render();
}
Kemudian properti tersebut digunakan pada fungsi render() untuk ditampilkan satu persatu
melalui <article-item>.
render() {
this._articles.forEach(article => {
const articleItemElement = document.createElement("article-item");
// memanggil fungsi setter article() pada article-item.
articleItemElement.article = article;
this.appendChild(articleItemElement);
})
}
Dengan begitu,menampilkan data pada main.js akan lebih mudah. Kita tidak perlu melakukan
proses perulangan lagi di sana karena proses tersebut langsung dilakukan ketika menggunakan
element <article-list>. Kita cukup memberikan nilai array yang akan ditampilkan.
import "./article-list.js";
import articles from "./articles.js";
document.body.appendChild(articleListElement);
Semakin mudah kita menggunakan sebuah element maka akan semakin baik bukan?
Walaupun terlihat agak sedikit merepotkan dalam membuatnya, perlu Anda ingat bahwa web
component ini bersifat reusable. Artinya, jika kita ingin membuat komponen serupa, kita tidak
perlu membuatnya dari awal.
Dengan menjalankan kode di atas, maka hasilnya akan tampak seperti ini:
Potongan kode untuk seluruh contoh custom element yang digunakan pada materi ini:
https://repl.it/@dicodingacademy/163-03-custom-element?lite=true
Tugas Anda sekarang adalah membuat proyek Club Finder menerapkan custom element yang
modular dan reusable sesuai dengan poin-poin yang disebutkan di atas.
Jika Anda mengalami stuck sebelum melanjutkan pada pembahasan solusi, sebaiknya Anda
tanyakan dulu masalah yang Anda hadapi pada diskusi kelas ya.
Good Luck!
Agar mengelola berkas pada proyek jadi lebih mudah, kita perlu membuat folder baru dengan
nama “component” di dalam folder src -> script.
Folder ini akan menampung berkas JavaScript yang digunakan dalam membuat custom
element.
Lalu di dalam folder component, buat berkas JavaScript baru dengan nama “app-bar.js”.
Kemudian kita buat class dengan nama AppBar yang mewarisi sifat HTMLElement.
Kemudian di dalam body block classnya, kita implementasi method connectedCallback dan
membuat fungsi render.
render() {
}
}
Seperti yang sudah kita ketahui, connectedCallback() akan terpanggil ketika element telah
diterapkan pada DOM. Jika kita ingin element ini ketika diterapkan langsung melakukan
rendering maka kita dapat memanggil fungsi this.render() di dalam connectedCallback.
render() {
}
}
Lalu pada fungsi render, kita tuliskan kode yang berfungsi untuk menampilkan elemen yang
dibutuhkan pada melalui properti this.innerHTML. Apa saja yang dibutuhkan? Kita bisa
melihatnya pada berkas index.html.
<header>
<div id="appBar" class="app-bar">
<h2>Club Finder</h2>
</div>
</header>
Di dalam elemen <header> terdapat elemen <div> yang menerapkan class “app-bar”. Nah kita
copy element di dalam app-bar, dan paste untuk dijadikan nilai pada this.innerHTML di fungsi
render().
render() {
this.innerHTML = `<h2>Club Finder</h2>`;
}
}
Lalu di akhir berkas app-bar.js, jangan lupa untuk definisikan custom element yang kita buat
agar dapat digunakan pada DOM.
render() {
this.innerHTML = `<h2>Club Finder</h2>`;
}
}
customElements.define("app-bar", AppBar);
Dengan begitu kita dapat mengubah penerapan app-bar pada index.html dengan
menggunakan tag <app-bar>.
<header>
<app-bar></app-bar>
</header>
Terakhir, agar kode pada berkas app-bar.js tereksekusi, impor berkas app-bar.js pada berkas
app.js, seperti ini:
import "./src/script/component/app-bar.js";
Tuliskan kode tersebut pada awal berkas app.js, sehingga keseluruhan kode pada berkasnya
akan tampak seperti ini:
import "./src/script/component/app-bar.js";
import main from "./src/script/view/main.js";
document.addEventListener("DOMContentLoaded", main);
Kemudian coba kita buka proyeknya menggunakan local server. Inilah tampilan hasilnya:
Oops, tampilan App Bar tampak berantakan. Kita perlu memperbaiki css yang digunakan pada
elemen App Bar sebelumnya. Buka berkas appbar.css lalu ubah selector-nya dari .app-bar
menjadi app-bar.
app-bar {
padding: 16px;
width: 100%;
background-color: cornflowerblue;
color: white;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
}
Yah, kini teks “Club Finder” tidak tampak karena background element tidak bekerja dengan
baik. Kenapa begini yah? Pasalnya, custom element standarnya merupakan inline element,
sehingga tidak akan mengisi panjang lebar parent element-nya. Solusinya adalah dengan
mengubah sifat inline pada custom element menjadi block dengan cara menambahkan properti
display dengan nilai block pada selector app-bar.
app-bar {
display: block;
padding: 16px;
width: 100%;
background-color: cornflowerblue;
color: white;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
}
Dengan begitu tampilan kita berhasil membuat custom element <app-bar> dengan baik!
Solution: Membuat search-bar Component
Pembuatan elemen <search-bar> lebih sedikit rumit dari pembuatan komponen sebelumnya,
karena di dalam komponen search bar terdapat element <input> dan <button>. Kombinasi
kedua element tersebut digunakan dalam mencari data club. Sebisa mungkin kita membuat
custom element <search-bar> sehingga mempermudah kala menggunakan komponen
tersebut.
Mari kita mulai dengan membuat berkas JavaScript baru dengan nama search-bar.js.
Kemudian di dalamnya kita membuat class SearchBar dengan mewarisi sifat HTMLElement.
render() {
}
}
render() {
}
}
Di dalam fungsi render kita ambil elemen yang dibutuhkan untuk ditampilkan dari berkas
index.html.
<div id="search-container" class="search-container">
<input placeholder="Search football club" id="searchElement" type="search">
<button id="searchButtonElement" type="submit">Search</button>
</div>
Agar mudah, copy seluruh kode tersebut dan paste untuk dijadikan nilai this.innerHTML di
dalam fungsi render.
render() {
this.innerHTML = `
<div id="search-container" class="search-container">
<input placeholder="Search football club" id="searchElement"
type="search">
<button id="searchButtonElement" type="submit">Search</button>
</div>
`;
}
}
Karena di dalam elemen ini terdapat <button> yang harus memiliki sebuah event ketika ia
ditekan, maka kita harus menyediakan setter . Gunanya untuk menetapkan fungsi event agar
dapat mudah diterapkan dari luar class SearchBar.
set clickEvent(event) {
this._clickEvent = event;
this.render();
}
render() {
this.innerHTML = `
<div id="search-container" class="search-container">
<input placeholder="Search football club" id="searchElement"
type="search">
<button id="searchButtonElement" type="submit">Search</button>
</div>
`;
}
}
Lalu kita terapkan this._clickEvent sebagai event pada element <button> dengan cara
menuliskan kode berikut pada akhir fungsi render():
this.querySelector("#searchButtonElement").addEventListener("click",
this._clickEvent);
render() {
this.innerHTML = `
<div id="search-container" class="search-container">
<input placeholder="Search football club" id="searchElement"
type="search">
<button id="searchButtonElement" type="submit">Search</button>
</div>`;
this.querySelector("#searchButtonElement").addEventListener("click",
this._clickEvent);
}
Dengan begitu nantinya kita dapat mudah dalam clickEvent pada SearchBar yang digunakan
di berkas main.js.
Pada berkas main.js juga kita memanfaatkan value dari element <input> untuk mendapatkan
kata kunci pencarian club. Agar mudah mendapatkan nilai value dari elemen <input> yang
terdapat pada search bar, kita buat fungsi getter yang mengembalikan nilai value dari elemen
<input> tersebut.
get value() {
return this.querySelector("#searchElement").value;
}
Sehingga keseluruhan kode yang terdapat berkas search-bar.js akan terlihat seperti ini:
class SearchBar extends HTMLElement {
connectedCallback(){
this.render();
}
set clickEvent(event) {
this._clickEvent = event;
this.render();
}
get value() {
return this.querySelector("#searchElement").value;
}
render() {
this.innerHTML = `
<div id="search-container" class="search-container">
<input placeholder="Search football club" id="searchElement"
type="search">
<button id="searchButtonElement" type="submit">Search</button>
</div>
`;
this.querySelector("#searchButtonElement").addEventListener("click",
this._clickEvent);
}
}
Lalu di akhir berkasnya, jangan lupa untuk definisikan custom element yang kita buat agar
dapat digunakan pada DOM.
set clickEvent(event) {
this._clickEvent = event;
this.render();
}
get value() {
return this.querySelector("#searchElement").value;
}
render() {
this.innerHTML = `
<div id="search-container" class="search-container">
<input placeholder="Search football club" id="searchElement"
type="search">
<button id="searchButtonElement" type="submit">Search</button>
</div>
`;
this.querySelector("#searchButtonElement").addEventListener("click",
this._clickEvent);
}
}
customElements.define("search-bar", SearchBar);
Yeay, pembuatan custom element sudah selesai. Sekarang saatnya kita menggunakannya!
Pertama ubahlah struktur html yang membentuk komponen pencarian dengan menggunakan
tag <search-bar>. Silakan buka berkas index.html kemudian ubah kode berikut:
<search-bar></search-bar>
Setelah itu, buka berkas src -> script -> view -> main.js dan sesuaikan kode binding elemen
berikut:
Kemudian kita sesuaikan kembali penerapan event click pada komponen pencarian dengan
mengubah kode berikut:
buttonSearchElement.addEventListener("click", onButtonSearchClicked);
Menjadi:
searchElement.clickEvent = onButtonSearchClicked;
Terakhir, karena berkas main.js perlu kode pada berkas search-bar.js tereksekusi, kita lakukan
impor berkas search-bar.js pada berkas main.js, seperti ini:
import '../component/search-bar.js';
Tuliskan kode tersebut pada awal berkas app.js, sehingga keseluruhan kode pada berkasnya
akan tampak seperti ini:
import '../component/search-bar.js';
import DataSource from '../data/data-source.js';
clubElement.innerHTML = `
<img class="fan-art-club" src="${fanArt}" alt="Fan Art">
<div class="club-info">
<h2>${name}</h2>
<p>${description}</p>
</div>`;
clubListElement.appendChild(clubElement);
})
};
searchElement.clickEvent = onButtonSearchClicked;
};
Kemudian coba kita buka proyeknya menggunakan local server kemudian lakukan pencarian
dengan menggunakan kata kunci “Arsenal”. Hasilnya adalah tampilan berikut:
Selamat! Anda sudah menerapkan komponen Search Bar dengan baik!
Kemudian kita buat 2 (dua) fungsi di dalamnya yaitu setter clubs, dan render.
render() {
}
}
Fungsi set clubs digunakan untuk menetapkan properti this._clubs pada class ini. Nantinya
properti tersebut akan digunakan pada fungsi render dalam membuat custom element
<club-item>.
set clubs(clubs) {
this._clubs = clubs;
this.render();
}
Kemudian di dalam fungsi render, kita lakukan proses perulangan dengan menggunakan
forEach pada this._clubs. Pada setiap iterasinya kita akan mendapatkan individual club dan
pada saat itu juga kita buat custom element <club-item>. Pada tiap elemen <club-item> dibuat
sebagai child dari element <club-list> ini. Hasilnya. fungsi render akan tampak seperti ini:
render() {
this.innerHTML = "";
this._clubs.forEach(club => {
const clubItemElement = document.createElement("club-item");
clubItemElement.club = club
this.appendChild(clubItemElement);
})
}
Perlu satu fungsi lagi pada custom element ini, yaitu fungsi untuk menangani ketika hasil
pencarian mengalami kegagalan atau tidak ditemukkan. Maka dari itu mari kita buat fungsi
dengan nama renderError() dengan satu buah parameter yang merupakan pesan eror/alasan
yang perlu ditampilkan.
renderError(message) {
Untuk template html yang akan ditampilkan, kita dapat copy dari fungsi fallbackResult pada
berkas src -> script -> view -> main.js.
clubListElement.innerHTML = "";
clubListElement.innerHTML += `<h2 class="placeholder">${message}</h2>`;
renderError(message) {
this.innerHTML = "";
this.innerHTML += `<h2 class="placeholder">${message}</h2>`;
}
Pada akhir berkas club-list.js jangan lupa untuk definisikan custom element yang kita buat
agar dapat digunakan pada DOM.
customElements.define("club-list", ClubList);
Oh ya! Karena pada berkas ini kita menggunakan elemen <club-item> yang nanti akan
dituliskan pada berkas club-item.js, maka kita perlu melakukan impor berkas club-item.js di
berkas ini.
import './club-item.js';
Sehingga sekarang keseluruhan kode yang terdapat pada berkas ini akan tampak seperti ini:
import './club-item.js';
renderError(message) {
this.innerHTML = "";
this.innerHTML += `<h2 class="placeholder">${message}</h2>`;
}
render() {
this.innerHTML = "";
this._clubs.forEach(club => {
const clubItemElement = document.createElement("club-item");
clubItemElement.club = club
this.appendChild(clubItemElement);
})
}
}
customElements.define("club-list", ClubList);
Pembuatan element <club-list> selesai! Sekarang kita lanjut dengan membuat elemen
<club-item>.
Pada berkas club-item.js, kita buat class ClubItem dengan mewarisi sifat HTMLElement.
}
Kemudian kita buat fungsi setter club dan fungsi render.
render() {
}
}
Fungsi setter club berfungsi untuk menetapkan nilai club ke properti this._club yang nantinya
akan digunakan pada fungsi render untuk menampilkan data individual club hasil pencarian.
Sehingga kita sesuaikan kode di dalam fungsi setter club menjadi seperti ini:
render() {
}
}
Lalu kita copy template html yang berada pada fungsi renderResult di berkas src -> script ->
view -> main.js.
clubElement.innerHTML = `
<img class="fan-art-club" src="${fanArt}" alt="Fan Art">
<div class="club-info">
<h2>${name}</h2>
<p>${description}</p>
</div>`;
render() {
this.innerHTML = `
<img class="fan-art-club" src="${fanArt}" alt="Fan Art">
<div class="club-info">pada
<h2>${name}</h2>
<p>${description}</p>
</div>`;
}
}
Lalu kita sesuaikan kembali properti-properti yang digunakan pada html template, menjadi
seperti ini:
render() {
this.innerHTML = `
<img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">
<div class="club-info">
<h2>${this._club.name}</h2>
<p>${this._club.description}</p>
</div>`;
}
}
Kemudian pada akhir berkas club-item.js jangan lupa untuk definisikan custom element yang
kita buat agar dapat digunakan pada DOM.
render() {
this.innerHTML = `
<img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">
<div class="club-info">
<h2>${this._name}</h2>
<p>${this._description}</p>
</div>`;
}
}
customElements.define("club-item", ClubItem);
Silakan buka berkas index.html, kemudian ubah penerapan club list menggunakan elemen
<div> berikut:
<div id="clubList"></div>
Menjadi:
<club-list></club-list>
Selanjutnya buka berkas src -> script -> view -> main.js. Kita sesuaikan kembali selector pada
saat melakukan binding clubListElement. Ubah kode berikut:
Menjadi:
clubElement.innerHTML = `
<img class="fan-art-club" src="${fanArt}" alt="Fan Art">
<div class="club-info">
<h2>${name}</h2>
<p>${description}</p>
</div>`;
clubListElement.appendChild(clubElement);
})
};
Sesuaikan juga kode yang terdapat pada fungsi fallbackResult, karena kita sudah membuat
fungsi renderError() pada ClubList, maka penggunaanya cukup dilakukan seperti ini:
Karena kita menggunakan elemen <club-list> pada berkas main.js, maka kita perlu melakukan
impor berkas club-list.js pada berkas main.js.
import '../component/club-list.js';
Dengan begitu keseluruhan kode pada berkas main.js akan tampak seperti berikut:
import '../component/club-list.js';
import '../component/search-bar.js';
import DataSource from '../data/data-source.js';
searchElement.clickEvent = onButtonSearchClicked;
};
Sekarang kita coba buka proyeknya menggunakan local server lalu tekan tombol pencarian.
Voila, inilah tampilan hasilnya:
Ops, tampilan daftar club tampak berantakan. Kita perlu menyesuaikan styling-nya juga. Jadi
silakan buka berkas src -> style -> clublist.css. Kemudian ubah seluruh selector #clubList
menjadi club-list dan selector .club menjadi club-item.
club-list {
margin-top: 32px;
width: 100%;
padding: 16px;
}
club-item {
margin-bottom: 18px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
border-radius: 10px;
overflow: hidden;
}
club-item .fan-art-club {
width: 100%;
max-height: 300px;
object-fit: cover;
object-position: center;
}
.club-info {
padding: 24px;
}
.club-info > h2 {
font-weight: lighter;
}
.club-info > p {
margin-top: 10px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 10; /* number of lines to show */
}
Kemudian tambahkan juga properti display dengan nilai block pada selector club-list dan
club-item.
club-list {
display: block;
….
}
….
club-item {
display: block;
….
}
….
Sehingga keseluruhan kode pada berkas clublist.css akan tampak seperti ini:
club-list {
display: block;
margin-top: 32px;
width: 100%;
padding: 16px;
}
club-item {
display: block;
margin-bottom: 18px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
border-radius: 10px;
overflow: hidden;
}
club-item .fan-art-club {
width: 100%;
max-height: 300px;
object-fit: cover;
object-position: center;
}
.club-info {
padding: 24px;
}
.club-info > h2 {
font-weight: lighter;
}
.club-info > p {
margin-top: 10px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 10; /* number of lines to show */
}
Sekarang kita coba buka kembali proyek club finder dengan menggunakan local server.
Seharusnya kini semuanya sudah berjalan dengan baik.
Shadow DOM
Halaman website yang ditampilkan terbentuk dari HTML. HTML sangat membantu kita karena
ia cukup mudah dipelajari dan digunakan. HTML mudah dipahami oleh kita namun tidak untuk
mesin, sehingga terciptalah DOM (Document Object Model) sebagai penghubung antara HTML
dengan bahasa pemrograman. Di dalam DOM seluruh struktur HTML dapat digambarkan dalam
bentuk objek yang dapat dimanipulasi melalui bahasa pemrograman, salah satunya JavaScript.
Ketika browser memuat halaman, HTML akan secara otomatis dimodelkan menjadi sebuah
object dan nodes hingga membentuk “DOM Tree”. Berikut contoh DOM Tree yang terbuat
<html>
<head>
<title>Web Components</title>
</head>
<body>
<h1>Mari belajar Shadow DOM</h1>
</body>
</html>
Object dan nodes yang dihasilkan dari DOM akan memiliki properti dan method yang dapat kita
manfaatkan untuk memanipulasi konten di dalamnya. Seluruh elemen dan style pada HTML
(apapun yang berada di dalam DOM) akan terekspos secara global dan nilainya dapat kita
peroleh dari mana saja. Biasanya untuk mendapatkan element kita gunakan
document.querySelector, setelah itu kita dapat leluasa mengontrol elemen, mengubah konten
di dalamnya ataupun mengubah styling yang diterapkan.
Encapsulation
Saat ini banyak web yang dibangun melalui arsitektur berbasis komponen sehingga diharapkan
komponen tersebut dapat digunakan kembali. Namun bukankah tidak baik jika komponen
tersebut dapat diganggu dan diubah dari luar? Sebaiknya komponen dapat bertahan dari
gangguan luar agar secara visual atau fungsinya agar tetap dalam keadaan aslinya. Maka dari
itu, kita perlu menerapkan konsep enkapsulasi pada komponen tersebut.
Shadow DOM dapat mengisolasi sebagian struktur DOM di dalam komponen sehingga tidak
dapat disentuh dari luar komponen atau nodenya. Singkatnya kita bisa sebut Shadow DOM
sebagai “DOM dalam DOM”. Bagaimana ia bekerja? Perhatikan ilustrasi berikut:
Sumber:
https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM
Shadow DOM dapat membuat DOM Tree lain terbentuk secara terisolasi melalui host yang
merupakan komponen dari regular DOM Tree (Document Tree). Shadow DOM Tree ini dimulai
dari root bayangan (Shadow root), yang dibawahnya dapat memiliki banyak element lagi
layaknya Document Tree.
Terdapat beberapa terminologi yang perlu kita ketahui dari ilustrasi di atas:
● Shadow host : Merupakan komponen/node yang terdapat pada regular DOM di mana
shadow DOM terlampir pada komponen/node ini.
● Shadow tree : DOM Tree di dalam shadow DOM.
● Shadow boundary : Batas dari shadow DOM dengan regular DOM.
● Shadow root : Root node dari shadow tree.
Kita dapat memanipulasi elemen yang terdapat di dalam shadow tree layaknya pada document
tree, namun cakupannya selama kita berada di dalam shadow boundary. Dengan kata lain, jika
kita berada di document tree kita tidak dapat memanipulasi elemen bahkan menerapkan styling
pada elemen yang terdapat di dalam shadow tree. Itulah mengapa shadow DOM dapat
membuat komponen terenkapsulasi.
Basic Usage
Potongan kode untuk materi ini:
● https://repl.it/@dicodingacademy/163-03-shadow-dom-basic-usage?lite=true
● https://repl.it/@dicodingacademy/163-03-shadow-dom-styling?lite=true
Untuk melampirkan Shadow DOM pada elemen penggunaan sangat mudah, yaitu dengan
menggunakan properti attachShadow pada elemen-nya seperti ini:
# Multi tab
# main.js
// Shadow Host
const divElement = document.createElement("div");
# index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Shadow DOM Basic Usage</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
Jika kita lihat pada browser, maka struktur HTML yang akan dihasilkan adalah seperti ini:
Dan struktur DOM tree yang terbentuk akan tampak seperti ini:
Dalam penggunaan attachShadow() kita melampirkan objek dengan properti mode yang
memiliki nilai ‘open’. Sebenarnya terdapat dua opsi nilai yang dapat digunakan dalam properti
mode, yaitu “open” dan “closed”.
Menggunakan nilai open berarti kita memperbolehkan untuk mengakses properti shadowRoot
melalui elemen yang melampirkan Shadow DOM.
divElement.attachShadow;
properti shadowRoot mengembalikan struktur DOM yang berada pada shadow tree.
Namun jika kita menggunakan nilai closed maka properti shadowRoot akan mengembalikan
nilai null.
Hal ini berarti kita sama sekali tidak dapat mengakses Shadow Tree selain melalui variabel
yang kita definisikan ketika melampirkan Shadow DOM.
# Multi tab
## index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Shadow DOM Basic Usage</title>
<style>
h1 {
color: red;
}
</style>
</head>
<body>
<h1>Ini merupakan konten yang berada di Document tree</h1>
<script src="main.js"></script>
</body>
</html>
## main.js
// Shadow Host
const divElement = document.createElement("div");
Jawabannya cukup mudah! Mari kita lihat kembali contoh custom element yang pernah kita
buat pada materi Styling Custom Element without Shadow DOM.
# Multi tab
## index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Styling without Shadow DOM</title>
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<image-figure
src="https://i.imgur.com/iJq78XH.jpg"
alt="Dicoding Logo"
caption="Huruf g dalam logo Dicoding">
</image-figure>
## image-figure.js
connectedCallback() {
this.src = this.getAttribute("src") || null;
this.alt = this.getAttribute("alt") || null;
this.caption = this.getAttribute("caption") || null;
this.render();
}
render() {
this.innerHTML = `
<style>
figure {
border: thin #c0c0c0 solid;
display: flex;
flex-flow: column;
padding: 5px;
max-width: 220px;
margin: auto;
}
<figure>
<img src="${this.src}"
alt="${this.alt}">
<figcaption>${this.caption}</figcaption>
</figure>
`;
}
customElements.define("image-figure", ImageFigure);
Ketika kode tersebut dijalankan pada browser, kita bisa melihat terdapat dua komponen
<figure> yang ditampilkan, salah satunya adalah custom element.
Kita bisa melihat juga bahwa keduanya memiliki styling yang sama, padahal kita hanya
menetapkan styling di dalam komponen ImageFigure saja. Yup, hal tersebut wajar terjadi
karena pada custom element kita tidak menetapkan Shadow DOM sehingga styling pada
custom element akan berdampak juga terhadap komponen di luarnya.
Dalam melampirkan Shadow DOM pada custom element sama seperti pada elemen biasanya,
yaitu menggunakan attachShadow. Namun dalam custom element, kita lakukan pada
constructor class-nya seperti ini:
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: "open"});
}
.....
}
Agar nilai shadowRoot dapat diakses pada fungsi mana saja di class, maka kita perlu
memasukkan nilai shadowRoot pada properti class menggunakan this. Kita bebas
menentukan nama properti sesuai keinginan, namun untuk memudahkan kita gunakan nama
_shadowRoot. Lalu mengapa penamaannya menggunakan tanda underscore (_) di depannya?
Jawabannya, this pada konteks class ini merupakan HTMLElement dan ia sudah memiliki
properti dengan nama shadowRoot. Untuk membedakan properti _shadowRoot asli dengan
properti baru yang kita buat, kita bisa tambahkan underscore di awal penamaannya. Hal ini
dibutuhkan karena jika kita menerapkan mode closed pada Shadow DOM, nilai properti
shadowRoot akan mengembalikan null, sehingga tidak ada cara lain untuk kita mengakses
Shadow Tree.
Setelah menerapkan Shadow DOM pada constructor, ketika ingin mengakses apapun yang
merupakan properti dari DOM kita harus melalui _shadowRoot. Contohnya ketika ingin
menerapkan template HTML, kita tidak bisa menggunakan langsung this.innerHTML, namun
perlu melalui this._shadowRoot.innerHTML.
Sehingga kita perlu menyesuaikan kembali beberapa kode yang terdapat pada fungsi render
menjadi seperti ini:
render() {
this._shadowRoot.innerHTML = `
<style>
figure {
border: thin #c0c0c0 solid;
display: flex;
flex-flow: column;
padding: 5px;
max-width: 220px;
margin: auto;
}
<figure>
<img src="${this.src}"
alt="${this.alt}">
<figcaption>${this.caption}</figcaption>
</figure>
`;
}
Dengan begitu sekarang styling pada komponen hanya berlaku pada komponen itu sendiri.
Begitu juga sebaliknya, styling yang dituliskan di luar dari komponen tidak akan berdampak
pada elemen di dalam komponen.
Practice: Menerapkan Shadow DOM pada Proyek Club Finder
Di latihan sebelumnya kita berhasil web component pada proyek Club Finder. Namun dalam
penerapanya, kita belum menggunakan Shadow DOM. Sehingga komponen tersebut belum
terenkapsulasi atau masih dapat terganggu oleh styling diluar komponennya.
Tugas Anda sekarang adalah menerapkan Shadow DOM pada setiap custom element yang
digunakan pada proyek Club Finder.
Jika Anda mengalami stuck sebelum melanjutkan pada pembahasan solusi, sebaiknya Anda
tanyakan dulu masalah yang Anda hadapi pada diskusi kelas ya.
Good Luck!
constructor() {
super();
this.shadowDOM = this.attachShadow({mode: "open"});
}
connectedCallback(){
this.render();
}
render() {
this.innerHTML = `<h2>Club Finder</h2>`;
}
}
customElements.define("app-bar", AppBar);
Karena kita sudah menerapkan Shadow DOM pada AppBar, jangan lupa pada fungsi render(),
kita ubah this.innerHTML menjadi this.shadowDOM.innerHTML.
constructor() {
super();
this.shadowDOM = this.attachShadow({mode: "open"});
}
connectedCallback(){
this.render();
}
render() {
this.shadowDOM.innerHTML = `<h2>Club Finder</h2>`;
}
}
customElements.define("app-bar", AppBar);
Kemudian buka berkas style -> appbar.css dan pindahkan (cut) seluruh kode yang ada pada
berkas tersebut.
app-bar {
display: block;
padding: 16px;
width: 100%;
background-color: cornflowerblue;
color: white;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
}
Lalu tempel (paste) pada nilai this.shadowDOM.innerHTML dengan dibungkus oleh element
<style> tepat sebelum element <h2> pada fungsi render() di berkas app-bar.js seperti ini:
constructor() {
super();
this.shadowDOM = this.attachShadow({mode: "open"});
}
connectedCallback(){
this.render();
}
render() {
this.shadowDOM.innerHTML = `
<style>
app-bar {
display: block;
padding: 16px;
width: 100%;
background-color: cornflowerblue;
color: white;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
}
</style>
<h2>Club Finder</h2>`;
}
}
customElements.define("app-bar", AppBar);
Coba kita simpan perubahan yang diterapkan kemudian lihat perubahannya pada browser.
Ups, pada browser kita dapat melihat title yang ditampilkan pada <app-bar> tampak
berantakan. Untuk menanganinya, kita perlu menyesuaikan kembali style yang diterapkan pada
custom element menjadi seperti ini:
constructor() {
super();
this.shadowDOM = this.attachShadow({mode: "open"});
}
connectedCallback(){
this.render();
}
render() {
this.shadowDOM.innerHTML = `
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:host {
display: block;
width: 100%;
background-color: cornflowerblue;
color: white;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
}
h2 {
padding: 16px;
}
</style>
<h2>Club Finder</h2>`;
}
}
customElements.define("app-bar", AppBar);
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
Yang digunakan untuk menghilangkan seluruh margin dan padding standar yang diterapkan
pada element html. Dan kita juga mengubah pengaturan box-sizing menjadi border-box.
Lalu kode pada kode styling lainnya juga kita melihat bahwa selector app-bar digantikan dengan
:host. Apa itu :host? Selector :host merupakan selector yang digunakan untuk menunjuk
element :host yang menerapkan Shadow DOM. Pada host kita tidak dapat mengatur padding
sehingga kita perlu memindahkannya pada elemen <h2>.
Setelah melakukan perubahan tersebut simpan (save) kembali perubahannya dan lihat hasilnya
pada browser, seharusnya <app-bar> sudah ditampilkan dengan baik.
Karena kita sudah tidak membutuhkan lagi berkas src -> styles -> appbar.css, kita dapat
menghapus berkas tersebut.
Jangan lupa untuk menghapus import css tersebut pada src -> styles -> style.css.
@import "clublist.css";
@import "searchbar.css";
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
}
main {
width: 90%;
max-width: 800px;
margin: 32px auto;
}
constructor() {
super();
this.shadowDOM = this.attachShadow({mode: "open"});
}
connectedCallback(){
this.render();
}
set clickEvent(event) {
this._clickEvent = event;
this.render();
}
get value() {
return this.querySelector("#searchElement").value;
}
render() {
this.innerHTML = `
<div id="search-container" class="search-container">
<input placeholder="Search football club" id="searchElement"
type="search">
<button id="searchButtonElement" type="submit">Search</button>
</div>
`;
this.querySelector("#searchButtonElement").addEventListener("click",
this._clickEvent);
}
}
customElements.define("search-bar", SearchBar);
Sama seperti yang kita lakukan pada component App Bar, kita ubah this.innerHTML menjadi
this.shadowDOM.InnerHTML pada fungsi render().
constructor() {
super();
this.shadowDOM = this.attachShadow({mode: "open"});
}
connectedCallback(){
this.render();
}
set clickEvent(event) {
this._clickEvent = event;
this.render();
}
get value() {
return this.querySelector("#searchElement").value;
}
render() {
this.shadowDOM.innerHTML = `
<div id="search-container" class="search-container">
<input placeholder="Search football club" id="searchElement"
type="search">
<button id="searchButtonElement" type="submit">Search</button>
</div>
`;
this.querySelector("#searchButtonElement").addEventListener("click",
this._clickEvent);
}
}
customElements.define("search-bar", SearchBar);
..........
get value() {
return this.shadowDOM.querySelector("#searchElement").value;
}
render() {
.........
this.shadowDOM.querySelector("#searchButtonElement").addEventListener("click",
this._clickEvent);
}
}
...........
Kemudian buka berkas src -> styles -> searchbar.css, pindahkan (cut) seluruh kode yang
terdapat pada berkas tersebut
.search-container {
max-width: 800px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
padding: 16px;
border-radius: 5px;
display: flex;
position: sticky;
top: 10px;
background-color: white;
}
Lalu tempel (paste) pada nilai this.shadowDOM.innerHTML dengan dibungkus oleh element
<style> tepat sebelum element <div> pada fungsi render() di berkas search-bar.js seperti ini:
.........
render() {
this.shadowDOM.innerHTML = `
<style>
.search-container {
max-width: 800px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
padding: 16px;
border-radius: 5px;
display: flex;
position: sticky;
top: 10px;
background-color: white;
}
customElements.define("search-bar", SearchBar);
Komponen Search Bar tampak normal dan berfungsi dengan baik sehingga kita tidak perlu
menyesuaikan lagi styling-nya.
Karena kita sudah tidak membutuhkan lagi berkas src -> styles -> searchbar.css, kita dapat
menghapus berkas tersebut.
Jangan lupa untuk menghapus import css tersebut pada src -> styles -> style.css.
@import "clublist.css";
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
}
main {
width: 90%;
max-width: 800px;
margin: 32px auto;
}
import './club-item.js';
constructor() {
super();
this.shadowDOM = this.attachShadow({mode: "open"});
}
set clubs(clubs) {
this._clubs = clubs;
this.render();
}
renderError(message) {
this.innerHTML = "";
this.innerHTML += `<h2 class="placeholder">${message}</h2>`;
}
render() {
this.innerHTML = "";
this._clubs.forEach(club => {
const clubItemElement = document.createElement("club-item");
clubItemElement.club = club
this.appendChild(clubItemElement);
})
}
}
customElements.define("club-list", ClubList);
import './club-item.js';
constructor() {
super();
this.shadowDOM = this.attachShadow({mode: "open"});
}
set clubs(clubs) {
this._clubs = clubs;
this.render();
}
renderError(message) {
this.shadowDOM.innerHTML = "";
this.shadowDOM.innerHTML += `<h2 class="placeholder">${message}</h2>`;
}
render() {
this.shadowDOM.innerHTML = "";
this._clubs.forEach(club => {
const clubItemElement = document.createElement("club-item");
clubItemElement.club = club
this.shadowDOM.appendChild(clubItemElement);
})
}
}
customElements.define("club-list", ClubList);
Kemudian buka berkas src -> styles -> clublist.css dan pindahkan (cut) kode styling dengan
selector club-list > .placeholder
Lalu tempel (paste) pada nilai this.shadowDOM.innerHTML dengan dibungkus oleh element
<style> tepat sebelum element <h2> fungsi renderError() di berkas club-list.js seperti ini:
import './club-item.js';
.........
renderError(message) {
this.shadowDOM.innerHTML = `
<style>
club-list > .placeholder {
font-weight: lighter;
color: rgba(0,0,0,0.5);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>
`;
this.shadowDOM.innerHTML += `<h2 class="placeholder">${message}</h2>`;
}
.......
}
customElements.define("club-list", ClubList);
Hapus child selector (>) beserta kombinatornya, sisakan .placeholder sebagai selector dari
styling tersebut. Sehingga kode pada berkas ini seluruhnya tampak seperti:
import './club-item.js';
constructor() {
super();
this.shadowDOM = this.attachShadow({mode: "open"});
}
set clubs(clubs) {
this._clubs = clubs;
this.render();
}
renderError(message) {
this.shadowDOM.innerHTML = `
<style>
.placeholder {
font-weight: lighter;
color: rgba(0,0,0,0.5);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>
`;
this.shadowDOM.innerHTML += `<h2 class="placeholder">${message}</h2>`;
}
render() {
this.shadowDOM.innerHTML = "" ;
this._clubs.forEach(club => {
const clubItemElement = document.createElement("club-item");
clubItemElement.club = club
this.shadowDOM.appendChild(clubItemElement);
})
}
}
customElements.define("club-list", ClubList);
Simpan perubahan tersebut dan lihat hasilnya pada browser, tampilan dari daftar club akan
sangat berantakan.
Tenang kita akan memperbaikinya dengan beranjak ke berkas src -> script -> component ->
club-list.js.
Pada berkas tersebut buat sebuah constructor dan terapkan Shadow DOM di dalamnya.
class ClubItem extends HTMLElement {
constructor() {
super();
this.shadowDOM = this.attachShadow({mode: "open"});
}
set club(club) {
this._club = club;
this.render();
}
render() {
this.innerHTML = `
<img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">
<div class="club-info">
<h2>${this._club.name}</h2>
<p>${this._club.description}</p>
</div>`;
}
}
customElements.define("club-item", ClubItem);
constructor() {
super();
this.shadowDOM = this.attachShadow({mode: "open"});
}
set club(club) {
this._club = club;
this.render();
}
render() {
this.shadowDOM.innerHTML = `
<img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">
<div class="club-info">
<h2>${this._club.name}</h2>
<p>${this._club.description}</p>
</div>`;
}
}
customElements.define("club-item", ClubItem);
Selanjutnya buka kembali berkas src -> styles -> clublist.css dan pindahkan styling berikut:
club-item {
display: block;
margin-bottom: 18px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
border-radius: 10px;
overflow: hidden;
}
club-item .fan-art-club {
width: 100%;
max-height: 300px;
object-fit: cover;
object-position: center;
}
.club-info {
padding: 24px;
}
.club-info > h2 {
font-weight: lighter;
}
.club-info > p {
margin-top: 10px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 10; /* number of lines to show */
}
Tempel pada nilai this.shadowDOM.innerHTML dengan dibungkus oleh element <style> tepat
sebelum element <img> pada fungsi render() di berkas club-item.js seperti ini:
.......
render() {
this.shadowDOM.innerHTML = `
<style>
club-item {
display: block;
margin-bottom: 18px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
border-radius: 10px;
overflow: hidden;
}
club-item .fan-art-club {
width: 100%;
max-height: 300px;
object-fit: cover;
object-position: center;
}
.club-info {
padding: 24px;
}
.club-info > h2 {
font-weight: lighter;
}
.club-info > p {
margin-top: 10px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 10; /* number of lines to show */
}
</style>
<img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">
<div class="club-info">
<h2>${this._club.name}</h2>
<p>${this._club.description}</p>
</div>`;
}
}
......
.....
render() {
this.shadowDOM.innerHTML = `
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:host {
display: block;
margin-bottom: 18px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
border-radius: 10px;
overflow: hidden;
}
.fan-art-club {
width: 100%;
max-height: 300px;
object-fit: cover;
object-position: center;
}
.club-info {
padding: 24px;
}
.club-info > h2 {
font-weight: lighter;
}
.club-info > p {
margin-top: 10px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 10; /* number of lines to show */
}
</style>
<img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">
<div class="club-info">
<h2>${this._club.name}</h2>
<p>${this._club.description}</p>
</div>`;
}
}
........
Simpan perubahan tersebut dan lihat pada browser, seharusnya tampilan daftar tim sudah
kembali normal.
Oh ya, sebelum beranjak kita buka kembali berkas src -> styles -> clublist.css. Di sana masih
terdapat satu rule styling berikut:
club-list {
display: block;
margin-top: 32px;
width: 100%;
padding: 16px;
}
Jangan hapus rule styling tersebut karena kita masih menggunakannya untuk mengatur jarak
daftar liga yang ditampilkan. Namun sebaiknya kita pindahkan rule styling tersebut pada berkas
src -> styles -> style.css.
@import "clublist.css";
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
}
main {
width: 90%;
max-width: 800px;
margin: 32px auto;
}
club-list {
display: block;
margin-top: 32px;
width: 100%;
padding: 16px;
}
Dengan begitu kita dapat leluasa menghapus berkas clublist.css dan menghapus @import
pada berkas style.css.
Selamat! Kita sudah berhasil menerapkan Shadow DOM pada seluruh custom element yang
digunakan di proyek Club Finder. Sampai ketemu di materi selanjutnya ya!