0% menganggap dokumen ini bermanfaat (0 suara)
31 tayangan

Modul 3 - Web Components - Google Dokumen

Diunggah oleh

Yoga Farma
Hak Cipta
© © All Rights Reserved
Kami menangani hak cipta konten dengan serius. Jika Anda merasa konten ini milik Anda, ajukan klaim di sini.
Format Tersedia
Unduh sebagai PDF, TXT atau baca online di Scribd
0% menganggap dokumen ini bermanfaat (0 suara)
31 tayangan

Modul 3 - Web Components - Google Dokumen

Diunggah oleh

Yoga Farma
Hak Cipta
© © All Rights Reserved
Kami menangani hak cipta konten dengan serius. Jika Anda merasa konten ini milik Anda, ajukan klaim di sini.
Format Tersedia
Unduh sebagai PDF, TXT atau baca online di Scribd
Anda di halaman 1/ 102

Banyak Web Front-End Framework pada saat ini mempromosikan kemampuannya dalam

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.

What is Web Component


Web component merupakan salah satu fitur yang ditetapkan standar World Wide Web
Consortium (W3C). Fitur ini memudahkan developer dalam membuat komponen UI
websitenya menjadi lebih modular.
Dengan semakin pesatnya perkembangan website saat ini kita perlu menetapkan teknik yang
lebih modern dalam mengembangkan website. Salah satunya membuat komponen UI pada
website agar mampu sesuai dengan kebutuhan dan digunakan ulang. Kebanyakan developer
saat ini menggunakan framework untuk membantu pengembangan website menjadi mudah
dalam membuat dan menggunakan komponen UI.
Namun apakah Anda tahu beberapa kelebihan web component dengan komponen yang dibuat
menggunakan framework?

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

Write your first Custom Element


Potongan kode untuk materi ini:
https://repl.it/@dicodingacademy/163-03-first-custom-element?lite=true

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

Berikut contoh penulisan dalam membuat custom element:


class ImageFigure extends HTMLElement {

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

customElements merupakan global variable yang digunakan untuk mendefinisikan custom


element dan memberitahu bahwa terdapat HTML tag baru. Di dalam customElements terdapat
method yang bernama define(). Di sinilah kita meletakan tag name baru kemudian diikuti
dengan JavaScript class yang menerapkan sifat HTMLElement.

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

Berikut kode lengkapnya:

# 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

class ImageFigure extends HTMLElement {

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.

Life Cycle of Custom Element


Potongan kode untuk materi ini:
https://repl.it/@dicodingacademy/163-03-element-life-cycle?lite=true

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:

● connectedCallback() : Akan terpanggil setiap kali elemen berhasil ditambahkan ke


dokumen HTML (DOM). Callback ini merupakan tempat yang tepat untuk menjalankan
konfigurasi awal seperti mendapatkan data, atau mengatur attribute.
● disconnectedCallback() : Akan terpanggil setiap kali elemen dikeluarkan (remove())
dari DOM. Callback ini merupakan tempat yang tepat untuk membersihkan data yang
masih disimpan pada elemen. Bisa itu event, state, ataupun objek.
● attributeChangedCallback() : Akan terpanggil setiap kali nilai atribut yang di-observe
melalui fungsi static get observedAttributes diubah. Callback ini bisa kita manfaatkan
untuk memuat ulang data yang ditampilkan oleh elemen.
● adoptedCallback() : Akan terpanggil setiap kali elemen dipindahkan pada dokumen
baru. Kita relatif jarang menggunakan callback ini, namun jika kita memanfaatkan tag
<iframe> maka callback ini akan terpanggil.

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:

ReferenceError: Must call super constructor in derived class before


accessing 'this' or returning from derived constructor

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.

const imageFigureElement = document.createElement("image-figure");


document.body.appendChild(imageFigureElement);

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

class ImageFigure extends HTMLElement {


constructor() {
super();
console.log("constructed!")
}
connectedCallback() {
console.log("connected!");
}

disconnectedCallback() {
console.log("disconnected!");
}

adoptedCallback() {
console.log("adopted!");
}

attributeChangedCallback(name, oldValue, newValue) {


console.log(`Attribute: ${name} changed!`);
}

// digunakan untuk mengamati perubahan nilai attribute caption


/* kita bisa menetapkan lebih dari satu atribut yang diamati.
dengan memisahkan nama atribut menggunakan koma. Contoh: */
// return ["caption", "title", "src", ...]
static get observedAttributes() {
return ["caption"];
}
}

customElements.define("image-figure", ImageFigure);

# main.js

let imageFigureElement = document.querySelector("image-figure");

// Jika tidak tersedia pada DOM maka dibuat secara sintaksis.


if (!imageFigureElement) {
imageFigureElement = document.createElement("image-figure");
document.body.appendChild(imageFigureElement);
}

// mengubah/manambahkan nilai attribute caption.


setTimeout(() => {
imageFigureElement.setAttribute("caption", "Gambar 1");
}, 1000);
// menghapus imageFigureElement dari DOM
setTimeout(() => {
imageFigureEl

ement.remove();
}, 3000);

# output console

# html structure

Implementasi lifecycle callback pada custom element bersifat opsional. Kita tidak perlu
menuliskannya jika memang tidak diperlukan.

Custom element attribute and method


Potongan kode untuk materi ini:
https://repl.it/@dicodingacademy/163-03-attribute-and-method?lite=true
Selain memiliki siklus hidup, class yang mewarisi sifat HTMLElement juga memiliki properti dan
method yang sama seperti objek DOM. Di mana ia memiliki properti dan method seperti
innerHTML, innerText, appendChild(), remove(), dan sebagainya. Melalui properti dan
method ini kita dapat menetapkan apa yang harus ditampilkan atau mendapatkan nilai atribut
pada custom element. Contohnya seperti ini:

class ImageFigure extends HTMLElement {

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

Jika kita lihat hasilnya, maka akan tampak seperti ini:

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.

Observe Attribute Value


Potongan kode untuk materi ini:
https://repl.it/@dicodingacademy/163-03-attribute-observe?lite=true
Ketika elemen sudah tampak pada DOM, tidak jarang kita mengganti nilai dari atributnya karena
terdapat data yang perlu diperbaharui. Jika kita menggunakan elemen HTML standar,
perubahan yang diterapkan akan langsung kita bisa lihat hasilnya pada browser. Namun
bagaimana dengan custom element? Apakah sama? Mari kita coba bersama.

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

class ImageFigure extends HTMLElement {

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.

const element = document.querySelector("image-figure");


element.setAttribute("caption", "Lorem ipsum dolor sit amet");
Kita bisa lihat pada gif di atas bahwa walaupun kita berhasil mengubah nilai atribut caption,
pada browser nilai yang ditampilkan tersebut tidak berubah. Kok demikian? Pada custom
element kita perlu mengimplementasi dua fungsi dalam kelasnya (ImageFigure) agar kita dapat
mengobservasi nilai atribut yang berubah. Yang pertama fungsi attributeChangedCallback,
dan yang kedua fungsi static get observedAttributes.

Kedua fungsi tersebut saling terhubung. Fungsi attributeChangedCallback akan terpanggil


ketika nilai atribut yang diamati pada fungsi observedAttributes diubah nilainya. Kemudian
pada callback fungsi attributeChangedCallback inilah kita menetapkan logika perubahan.
Pada fungsi ini juga terdapat 3 (tiga) argument fungsi yang bisa dimanfaatkan yaitu:
● name : Nama dari atribut yang berubah
● oldValue : Nilai pada atribut sebelum diubah
● newValue : Nilai baru yang ditetapkan pada atribut.
Urutan dari ketiga argumen tersebut sangatlah penting, jadi jangan sampai tertukar.
Sebenarnya kita dapat memberikan nama apa saja untuk ketiga argumennya namun lebih baik
gunakan name, oldValue, newValue guna memudahkan kita dalam penggunaannya.

Berikut contoh implementasi dari kedua fungsi tersebut:

class ImageFigure extends HTMLElement {

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>
`;
}

attributeChangedCallback(name, oldValue, newValue) {


this[name] = newValue;
this.render();
}

static get observedAttributes() {


return ["caption"];
}
}

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.

return ["caption", "src", "alt"];

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.

class ImageFigure extends HTMLElement {

......

render() {
this.innerHTML = `
<style>
figure {
border: thin #c0c0c0 solid;
display: flex;
flex-flow: column;
padding: 5px;
max-width: 220px;
margin: auto;
}

figure > img {


max-width: 220px;
}

figure > figcaption {


background-color: #222;
color: #fff;
font: italic smaller sans-serif;
padding: 3px;
text-align: center;
}
</style>
<figure>
<img src="${this.src}"
alt="${this.alt}">
<figcaption>${this.caption}</figcaption>
</figure>
`;
}

......
}

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

figure > img {


max-width: 220px;
}

figure > figcaption {


background-color: #222;
color: #fff;
font: italic smaller sans-serif;
padding: 3px;
text-align: center;
}

Maka pada browser akan menampilkan hasil seperti berikut:


Kita tidak menetapkan Shadow DOM pada custom element ini sehingga styling pada custom
element masih dapat terpengaruh oleh keadaan dari luar. Maksudnya, jika kita menetapkan
styling lain dengan menargetkan figure sebagai selector, kemungkinan element <figure> yang
terdapat pada custom element akan ikut terpengaruhi walaupun kita sudah menetapkan styling
secara eksplisit di dalam fungsi render. Lantas bagaimana agar custom element dapat
terenkapsulasi dari gangguan luar? Kita akan membahas ini lebih detail pada pembahasan
Shadow DOM.

Handling complex data


Potongan kode untuk materi ini:
https://repl.it/@dicodingacademy/163-03-handling-complex-data?lite=true

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.

class ArticleItem extends HTMLElement {


set article(article) {
this._article = article;
this.render();
}

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:

const articleItemElement = document.createElement("article-item");


articleItemElement.article = article;

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

article-item > .featured-image {


width: 100%;
max-height: 300px;
object-fit: cover;
object-position: center;
}

article-item > .article-info {


padding: 24px;
}

article-item > .article-info > p {


margin-top: 10px;
}

## script.js

import "./article-item.js";
import article from "./article.js";

const containerElement = document.querySelector(".container");

const articleItemElement = document.createElement("article-item");


articleItemElement.article = article;

containerElement.appendChild(articleItemElement);

## article-item.js

class ArticleItem extends HTMLElement {


set article(article) {
this._article = article;
this.render();
}

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."
}

export default article;

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

const articleListElement = document.createElement("article-list");


articleListElement.articles = articles;

document.body.appendChild(articleListElement);

## article-list.js

import "./article-item.js"

class ArticleList extends HTMLElement {


set articles(articles) {
this._articles = articles;
this.render();
}
render() {
this._articles.forEach(article => {
const articleItemElement = document.createElement("article-item");
// memanggil fungsi setter article() pada article-item.
articleItemElement.article = article;
this.appendChild(articleItemElement);
})
}
}

## article-item.js

class ArticleItem extends HTMLElement {


set article(article) {
this._article = article;
this.render();
}

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

}
]

export default articles;

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

const articleListElement = document.createElement("article-list");


articleListElement.articles = articles;

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

Practice: Menerapkan Custom Element pada proyek Club Finder


Di latihan sebelumnya kita berhasil menerapkan standar ES6 dalam penulisan JavaScript pada
proyek Club Finder. Langkah yang bagus bukan? Nah, saat ini Anda sudah belajar bagaimana
membuat dan menerapkan custom element. Tentu baiknya kita terapkan juga pada proyek Club
Finder agar proyek tersebut lebih baik lagi.
Jika kita telaah dengan seksama, pada proyek Club Finder terdapat 4 (empat) bagian yang
berpotensi untuk dijadikan custom element, yaitu:
1. App Bar : Komponen di posisi atas yang menunjukkan identitas atau nama dari
aplikasi web.
2. Search Bar : Komponen yang terdiri dari elemen <input> dan <button> dan
berfungsi untuk melakukan pencarian club sesuai dengan input pengguna.
3. Club List : Komponen yang berfungsi untuk menampung data dari hasil pencarian,
kemudian menampilkannya dalam bentuk list.
4. Club Item : Komponen yang menampilkan data individual club yang diberikan dari
club list. Komponen ini terdiri dari gambar, nama, dan deskripsi singkat club.

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!

Solution: Membuat app-bar Component


Apakah Anda berhasil menerapkan custom element pada proyek Club Finder? Jika belum, mari
kita lakukan bersama-sama. Kita mulai dari komponen termudah terlebih dahulu yaitu App Bar.

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.

class AppBar extends HTMLElement {

Kemudian di dalam body block classnya, kita implementasi method connectedCallback dan
membuat fungsi render.

class AppBar extends HTMLElement {


connectedCallback(){

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.

class AppBar extends HTMLElement {


connectedCallback(){
this.render();
}

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().

class AppBar extends HTMLElement {


connectedCallback(){
this.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.

class AppBar extends HTMLElement {


connectedCallback(){
this.render();
}

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);
}

Lalu lihat kita lihat hasilnya.

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.

class SearchBar extends HTMLElement {

Kemudian kita implementasi method connectedCallback dan membuat fungsi render.

class SearchBar extends HTMLElement {


connectedCallback(){

render() {

}
}

Lalu panggil fungsi render() di dalam connectedCallback().

class SearchBar extends HTMLElement {


connectedCallback(){
this.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.

class SearchBar extends HTMLElement {


connectedCallback(){
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>
`;
}
}

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.

class SearchBar extends HTMLElement {


connectedCallback(){
this.render();
}

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

Sehingga kode pada fungsi render akan tampak seperti ini:

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.

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);
}
}

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:

<div id="search-container" class="search-container">


<input placeholder="Search football club" id="searchElement" type="search">
<button id="searchButtonElement" type="submit">Search</button>
</div>

Menjadi seperti ini:

<search-bar></search-bar>

Setelah itu, buka berkas src -> script -> view -> main.js dan sesuaikan kode binding elemen
berikut:

const searchElement = document.querySelector("#searchElement");

Dengan mengubah selector-nya menjadi “search-bar”.

const searchElement = document.querySelector("search-bar");


Lalu kita tidak membutuhkan deklarasi variabel buttonSearchElement karena sekarang kita
dapat mengakses button pada komponen pencarian melalui searchElement. Jadi silakan
hapus deklarasi variabel berikut:

const buttonSearchElement = document.querySelector("#searchButtonElement");

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

const main = () => {


const searchElement = document.querySelector("search-bar");
const clubListElement = document.querySelector("#clubList");

const onButtonSearchClicked = async () => {


try {
const result = await DataSource.searchClub(searchElement.value);
renderResult(result);
} catch (message) {
fallbackResult(message)
}
};
const renderResult = results => {
clubListElement.innerHTML = "";
results.forEach(club => {
const { name, fanArt, description } = club;
const clubElement = document.createElement("div");
clubElement.setAttribute("class", "club");

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);
})
};

const fallbackResult = message => {


clubListElement.innerHTML = "";
clubListElement.innerHTML += `<h2 class="placeholder">${message}</h2>`;
};

searchElement.clickEvent = onButtonSearchClicked;
};

export default main;

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!

Solution: Membuat club-list dan club-item Component


Custom element selanjutnya yang perlu kita buat adalah <club-list> dan <club-item>. Masih
ingat mengenai Nested Custom Element? Nah dalam membuat kedua custom element ini kita
akan menggunakan custom element di dalam custom element. Atau biasa disebut dengan
nested custom element.
Mari kita awali dengan membuat dua berkas JavaScript baru dengan nama “club-list.js” dan
“club-item.js” pada src -> script -> component.

Membuat <club-list> element


Langkah pertama kita buat custom element <club-list> terlebih dahulu. Pada berkas
club-list.js, kita buat class ClubList dengan mewarisi sifat HTMLElement.

class ClubList extends HTMLElement {

Kemudian kita buat 2 (dua) fungsi di dalamnya yaitu setter clubs, dan render.

class ClubList extends HTMLElement {


set clubs(clubs) {

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

Lalu paste pada fungsi renderError() dan ubah clubListElement.innerHTML menjadi


this.innerHTML.

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

class ClubList extends HTMLElement {


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

Pembuatan element <club-list> selesai! Sekarang kita lanjut dengan membuat elemen
<club-item>.

Membuat <club-item> element

Pada berkas club-item.js, kita buat class ClubItem dengan mewarisi sifat HTMLElement.

class ClubItem extends HTMLElement {

}
Kemudian kita buat fungsi setter club dan fungsi render.

class ClubItem extends HTMLElement {


set club(club) {

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:

class ClubItem extends HTMLElement {


set club(club) {
this._club = club;
this.render();
}

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

Kemudian paste template html pada this.innerHTML melalui fungsi render().

class ClubItem extends HTMLElement {


set club(club) {
this._club = club;
this.render();
}

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:

class ClubItem extends HTMLElement {


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>`;
}
}

Karena pada this._club inilah properti dari objek club disimpan.

Kemudian pada akhir berkas club-item.js jangan lupa untuk definisikan custom element yang
kita buat agar dapat digunakan pada DOM.

class ClubItem extends HTMLElement {


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._name}</h2>
<p>${this._description}</p>
</div>`;
}
}

customElements.define("club-item", ClubItem);

Dengan begitu elemen <club-item> sudah siap digunakan.

Menggunakan <club-list> element


Setelah membuat kedua custom element yang dibutuhkan, sekarang saatnya kita
menggunakannya!

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:

const clubListElement = document.querySelector("#clubList");

Menjadi:

const clubListElement = document.querySelector("club-list");


Lalu kita sesuaikan juga kode yang terdapat di dalam fungsi renderResult. Hapus seluruh
logika yang ada di dalam fungsi tersebut.

const renderResult = results => {


clubListElement.innerHTML = "";
results.forEach(club => {
const { name, fanArt, description } = club;
const clubElement = document.createElement("div");
clubElement.setAttribute("class", "club");

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);
})
};

Kita cukup menggantinya dengan seperti ini:

const renderResult = results => {


clubListElement.clubs = results;
};

Sesuaikan juga kode yang terdapat pada fungsi fallbackResult, karena kita sudah membuat
fungsi renderError() pada ClubList, maka penggunaanya cukup dilakukan seperti ini:

const fallbackResult = message => {


clubListElement.renderError(message);
};

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

const main = () => {


const searchElement = document.querySelector("search-bar");
const clubListElement = document.querySelector("club-list");

const onButtonSearchClicked = async () => {


try {
const result = await DataSource.searchClub(searchElement.value);
renderResult(result);
} catch (message) {
fallbackResult(message)
}
};

const renderResult = results => {


clubListElement.clubs = results;
};

const fallbackResult = message => {


clubListElement.renderError(message);
};

searchElement.clickEvent = onButtonSearchClicked;
};

export default main;

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

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

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

Dari struktur HTML berikut:

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

What is Shadow DOM?


Saat ini kita mungkin bisa menerapkan konsep enkapsulasi dengan menggunakan <iframe>
agar komponen terpisah dari gangguan luar. Namun teknik ini bukan cara yang baik, berat, dan
dapat menimbulkan masalah. Lantas bagaimana solusinya? Gunakanlah Shadow DOM.

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");

// element yang berada di dalam Shadow DOM


const headingElement = document.createElement("h1");
headingElement.innerText = "Ini merupakan konten di dalam shadow DOM";

// Melampirkan shadow root pada shadow host


// Mengatur mode shadow dengan nilai open
const shadowRoot = divElement.attachShadow({mode: "open"});

// Memasukkan element heading ke dalam shadow root


shadowRoot.appendChild(headingElement);

// Memasukkan elemen shadow host ke regular DOM


document.body.appendChild(divElement);

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

const shadowRoot = divElement.attachShadow({mode: "closed"});


divElement.shadowRoot // null;

Hal ini berarti kita sama sekali tidak dapat mengakses Shadow Tree selain melalui variabel
yang kita definisikan ketika melampirkan Shadow DOM.

const shadowRoot = divElement.attachShadow({mode: "closed"});


divElement.shadowRoot // null;
shadowRoot // # shadow-root (closed)
Karena Shadow DOM terisolasi dari document tree maka element yang terdapat di dalamnya
pun tidak akan terpengaruh oleh styling yang berada diluar dari shadow root-nya.

# 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");

// element yang berada di dalam Shadow DOM


const headingElement = document.createElement("h1");
headingElement.innerText = "Ini merupakan konten di dalam shadow DOM";

// Melampirkan shadow root pada shadow host


// Mengatur mode shadow dengan nilai open
const shadowRoot = divElement.attachShadow({mode: "open"});

// Memasukkan element heading ke dalam shadow root


shadowRoot.appendChild(headingElement);

// Memasukkan elemen shadow host ke regular DOM


document.body.appendChild(divElement);

Jika dilihat pada browser maka hasilnya akan seperti ini:


Berdasarkan hasil di atas, styling hanya akan diterapkan pada elemen <h1> yang berada di
document tree. Sedangkan elemen <h1> yang berada pada shadow dom tidak akan
terpengaruh dengan styling tersebut. Lantas, bagaimana caranya kita melakukan styling pada
Shadow DOM?

Kita dapat melakukannya dengan menambahkan template <style> di dalam


shadowRoot.innerHTML. Contohnya seperti ini:

// menetapkan styling pada Shadow DOM


shadowRoot.innerHTML += `
<style>
h1 {
color: green;
}
</style>
`;
Maka element <style> tersebut akan berada di dalam shadow tree dan akan berdampak pada
elemen yang ada di dalamnya.

Shadow DOM in Web Components


Untuk membantu menerapkan enkapsulasi pada custom element, Shadow DOM berperan
sebagai salah satu API standar yang digunakan dalam membuat Web Component (Hal ini
distandarisasi oleh W3C). Kita sudah belajar bagaimana menerapkan Shadow DOM pada
elemen yang berada pada Document Tree, namun bagaimana caranya bila itu diterapkan pada
custom element?

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>

<!-- Styling di dalam image-figure akan mempengaruhi juga elemen di


luarnya -->
<figure>
<img src="https://i.imgur.com/iJq78XH.jpg"
alt="Dicoding logo"/>
<figcaption>Huruf g dalam logo Dicoding</figcaption>
</figure>
<script src="image-figure.js"></script>
</body>
</html>

## image-figure.js

class ImageFigure extends HTMLElement {

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 {


max-width: 220px;
}

figure > figcaption {


background-color: #222;
color: #fff;
font: italic smaller sans-serif;
padding: 3px;
text-align: center;
}
</style>

<figure>
<img src="${this.src}"
alt="${this.alt}">
<figcaption>${this.caption}</figcaption>
</figure>
`;
}

attributeChangedCallback(name, oldValue, newValue) {


this[name] = newValue;
this.render();
}

static get observedAttributes() {


return ["caption"];
}
}

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:

class ImageFigure extends HTMLElement {

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 {


max-width: 220px;
}

figure > figcaption {


background-color: #222;
color: #fff;
font: italic smaller sans-serif;
padding: 3px;
text-align: center;
}
</style>

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

Solution: Menerapkan Shadow DOM pada Proyek Club Finder


Apakah Anda berhasil menerapkan shadow DOM pada custom element di proyek Club Finder?
Jika belum, mari kita lakukan bersama-sama.

Menerapkan Shadow DOM pada App Bar


Kita mulai dari <app-bar> component yuk. Pertama kita buka dulu proyek club finder dengan
text editor yang kita gunakan.
Kemudian buka berkas script -> component -> app-bar.js, buat constructor dari class tersebut
dan di dalamnya kita tetapkan shadow root seperti ini:

class AppBar extends HTMLElement {

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.

class AppBar extends HTMLElement {

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:

class AppBar extends HTMLElement {

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:

class AppBar extends HTMLElement {

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

Pada perubahan styling tersebut kita menambahkan

* {
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;
}

Menerapkan Shadow DOM pada Search Bar


Setelah berhasil menerapkan Shadow DOM pada App Bar, selanjutnya kita terapkan Shadow
DOM pada search bar. Silakan buka berkas src -> script -> component -> search-bar.js,
kemudian buat constructor dan terapkan Shadow DOM di dalamnya.

class SearchBar extends HTMLElement {

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().

class SearchBar extends HTMLElement {

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

Selain itu juga kita ubah pemanggilan this.querySelector menjadi


this.shadowDOM.querySelector pada fungsi render() dan get value()

class SearchBar extends HTMLElement {

..........

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

.search-container > input {


width: 75%;
padding: 16px;
border: 0;
border-bottom: 1px solid cornflowerblue;
font-weight: bold;
}

.search-container > input:focus {


outline: 0;
border-bottom: 2px solid cornflowerblue;
}

.search-container > input:focus::placeholder {


font-weight: bold;
}

.search-container > input::placeholder {


color: cornflowerblue;
font-weight: normal;
}

.search-container > button {


width: 23%;
cursor: pointer;
margin-left: auto;
padding: 16px;
background-color: cornflowerblue;
color: white;
border: 0;
text-transform: uppercase;
}

@media screen and (max-width: 550px){


.search-container {
flex-direction: column;
position: static;
}

.search-container > input {


width: 100%;
margin-bottom: 12px;
}

.search-container > button {


width: 100%;
}
}

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:

class SearchBar extends HTMLElement {

.........

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

.search-container > input {


width: 75%;
padding: 16px;
border: 0;
border-bottom: 1px solid cornflowerblue;
font-weight: bold;
}

.search-container > input:focus {


outline: 0;
border-bottom: 2px solid cornflowerblue;
}
.search-container > input:focus::placeholder {
font-weight: bold;
}

.search-container > input::placeholder {


color: cornflowerblue;
font-weight: normal;
}

.search-container > button {


width: 23%;
cursor: pointer;
margin-left: auto;
padding: 16px;
background-color: cornflowerblue;
color: white;
border: 0;
text-transform: uppercase;
}

@media screen and (max-width: 550px){


.search-container {
flex-direction: column;
position: static;
}

.search-container > input {


width: 100%;
margin-bottom: 12px;
}

.search-container > button {


width: 100%;
}
}
</style>
<div id="search-container" class="search-container">
<input placeholder="Search football club" id="searchElement"
type="search">
<button id="searchButtonElement" type="submit">Search</button>
</div>
`;
.......
}
}

customElements.define("search-bar", SearchBar);

Simpan perubahan yang dilakukan kemudian lihat hasilnya pada browser.

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

Menerapkan Shadow DOM pada Club List dan Club Item


Terakhir kita terapkan Shadow DOM pada komponen club list dan club item. Silakan buka
berkas src -> script -> component -> club-list.js, kemudian buat constructor dan terapkan
Shadow DOM di dalamnya.

import './club-item.js';

class ClubList extends HTMLElement {

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

Kemudian ubah seluruh kode this.innerHTML menjadi this.shadowDOM.innerHTML dan


this.appendChild menjadi this.shadowDOM.appendChild.

import './club-item.js';

class ClubList extends HTMLElement {

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

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

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

class ClubList extends HTMLElement {

.........

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

class ClubList extends HTMLElement {

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

Seperti biasa jangan lupa untuk mengubah this.innerHTML menjadi


this.shadowDOM.innerHTML ya.

class ClubItem extends HTMLElement {

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:

class ClubItem extends HTMLElement {

.......

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>`;
}
}

......

Sesuaikan kembali selector pada styling tersebut menjadi seperti ini:

class ClubItem extends HTMLElement {

.....

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!

Anda mungkin juga menyukai