news

Rabu, 10 Juni 2020

Belajar Web Component Dasar


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 ReactAngular, 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.
20200310180135577d410ea4b01a4ec8f6c0f13ce55190.gif
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 dibandingkan 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. 
20200310180507ede3cf1615fb43775e53c5893786c7bc.png
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.
2020031018053892a731d399ccaf1a172064c4888440e3.png
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.
20200310180628b4595d1b6bdfcd820d3871f6e2ce21ea.png
(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

Dalam membuat custom element, kita menuliskannya dengan menggunakan JavaScript class. Class tersebut mewarisi sifat dari HTMLElementHTMLElement 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:

  1. class ImageFigure extends HTMLElement {

  2.  

  3. }


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:

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

  1. <image-figure></image-figure>


Jangan lupa lampirkan script pada berkas yang digunakan untuk menuliskan class ImageFigure.

  1. <script src="image-figure.js"></script>


Berikut kode lengkapnya:

  1. <!DOCTYPE html>

  2. <html>

  3.  <head>

  4.    <meta charset="utf-8">

  5.    <meta name="viewport" content="width=device-width">

  6.    <title>My First Custom Element</title>

  7.  </head>

  8.  <body>

  9.    <image-figure></image-figure>

  10.    <script src="image-figure.js"></script>

  11.  </body>

  12. </html>




  1. class ImageFigure extends HTMLElement {

  2.  

  3. }

  4.  

  5. 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. 
20200310181513ebac1cf89024807a90e2cd2729534e21.png
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

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:
20200310181931e74da220c3a1a4a3ffd73cce428f7cc7.png
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:

  1. <body>

  2.     <image-figure></image-figure>

  3. </body>



Lalu cara kedua adalah dengan menggunakan sintaks JavaScript. Sama seperti membuat element HTML biasa, kita gunakan method document.createElement dalam membuat elemen baru.

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

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

  1. <!DOCTYPE html>

  2. <html>

  3.  <head>

  4.    <meta charset="utf-8">

  5.    <meta name="viewport" content="width=device-width">

  6.    <title>Element life cycle</title>

  7.  </head>

  8. <body>

  9.   <!-- silakan hapus tag <image-figure> untuk membuatnya secara sintaks JavaScript -->

  10.    <image-figure></image-figure>

  11.    <script src="image-figure.js"></script>

  12.    <script src="main.js"></script>

  13.  </body>

  14. </html>



  1. class ImageFigure extends HTMLElement {

  2.  constructor() {

  3.    super();

  4.    console.log("constructed!")

  5.  }

  6.  

  7.  

  8.  connectedCallback() {

  9.    console.log("connected!");

  10.  }

  11.  

  12.  

  13.  disconnectedCallback() {

  14.    console.log("disconnected!");

  15.  }

  16.  

  17.  

  18.  adoptedCallback() {

  19.    console.log("adopted!");

  20.  }

  21.  

  22.  

  23.  attributeChangedCallback(name, oldValue, newValue) {

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

  25.  }

  26.  

  27.  

  28.  // digunakan untuk mengamati perubahan nilai attribute caption

  29.  /* kita bisa menetapkan lebih dari satu atribut yang diamati.

  30.     dengan memisahkan nama atribut menggunakan koma. Contoh: */

  31.  // return ["caption", "title", "src", ...]

  32.  static get observedAttributes() {

  33.    return ["caption"];

  34.  }

  35. }

  36.  

  37.  

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








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

  2.  

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

  4. if (!imageFigureElement) {

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

  6.  document.body.appendChild(imageFigureElement);

  7. }

  8.  

  9. // mengubah/manambahkan nilai attribute caption.

  10. setTimeout(() => {

  11.  imageFigureElement.setAttribute("caption", "Gambar 1");

  12. }, 1000);

  13.  

  14.  

  15. // menghapus imageFigureElement dari DOM

  16. setTimeout(() => {

  17.  imageFigureElement.remove();

  18. }, 3000);


20200310182652be0450ebbab22838a5eee1b22bd53873.gif
2020031018272778b0288d1d32c9983ccc17ea1b7fa7d3.gif

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

Custom element attribute and method

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 innerHTMLinnerTextappendChild()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:
  1. class ImageFigure extends HTMLElement {
  2.  
  3.  connectedCallback() {
  4.    this.src = this.getAttribute("src") || null;
  5.    this.alt = this.getAttribute("alt") || null;
  6.    this.caption = this.getAttribute("caption") || null;
  7.  
  8.    this.innerHTML = `
  9.      <figure>
  10.        <img src="${this.src}"
  11.            alt="${this.alt}">
  12.        <figcaption>${this.caption}</figcaption>
  13.      </figure>
  14.    `;
  15.  }
  16. }
  17.  
  18. 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 srcalt, 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.

  1. <image-figure

  2.      src="https://i.imgur.com/iJq78XH.jpg"

  3.      alt="Dicoding Logo"

  4.      caption="Huruf g dalam logo Dicoding">

  5. </image-figure>



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

  2.  

  3. imageFigureElement.setAttribute("src", "https://i.imgur.com/iJq78XH.jpg");

  4. imageFigureElement.setAttribute("alt", "Dicoding Logo");

  5. imageFigureElement.setAttribute("caption", "Huruf g dalam logo Dicoding");

  6.  

  7. document.body.appendChild(imageFigureElement);


Jika kita lihat hasilnya, maka akan tampak seperti ini:
202003101842396ebe6d528e732b9586f5fe3562899d10.png
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

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:

  1. <!DOCTYPE html>

  2. <html>

  3.  <head>

  4.    <meta charset="utf-8">

  5.    <meta name="viewport" content="width=device-width">

  6.    <title>Attribute Observe</title>

  7.  </head>

  8. <body>

  9.    <image-figure

  10.      src="https://i.imgur.com/iJq78XH.jpg"

  11.      alt="Dicoding Logo"

  12.      caption="Huruf g dalam logo Dicoding">

  13.      </image-figure>

  14.    <script src="image-figure.js"></script>

  15.  </body>

  16. </html>




  1. class ImageFigure extends HTMLElement {

  2.  

  3.  connectedCallback() {

  4.    this.src = this.getAttribute("src") || null;

  5.    this.alt = this.getAttribute("alt") || null;

  6.    this.caption = this.getAttribute("caption") || null;

  7.  

  8.  

  9.    this.innerHTML = `

  10.      <figure>

  11.        <img src="${this.src}"

  12.            alt="${this.alt}">

  13.        <figcaption>${this.caption}</figcaption>

  14.      </figure>

  15.    `;

  16.  }

  17. }

  18.  

  19.  

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


Lalu kita buka pada browser, maka akan tampak seperti ini:
202003101846224b05a9460bd020f4cfbd9732778240e0.png
Lalu mari kita coba mengubah nilai atribut caption dengan nilai baru menggunakan JavaScript melalui console browser.

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

  2. element.setAttribute("caption", "Lorem ipsum dolor sit amet");


2020031018472504f1bce82240562e413552d465c4949c.gif

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 nameoldValuenewValue guna memudahkan kita dalam penggunaannya.
Berikut contoh implementasi dari kedua fungsi tersebut:
  1. class ImageFigure extends HTMLElement {
  2.  
  3.  connectedCallback() {
  4.    this.src = this.getAttribute("src") || null;
  5.    this.alt = this.getAttribute("alt") || null;
  6.    this.caption = this.getAttribute("caption") || null;
  7.    this.render();
  8.  }
  9.  
  10.  render() {
  11.    this.innerHTML = `
  12.      <figure>
  13.        <img src="${this.src}"
  14.            alt="${this.alt}">
  15.        <figcaption>${this.caption}</figcaption>
  16.      </figure>
  17.    `;
  18.  }
  19.  
  20.  attributeChangedCallback(name, oldValue, newValue) {
  21.    this[name] = newValue;
  22.    this.render();
  23.  }
  24.  
  25.  static get observedAttributes() {
  26.    return ["caption"];
  27.  }
  28. }
  29.  
  30. 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.

  1. return ["caption"];



Nilai kembalian dari observedAttributes merupakan array. Jika kita ingin mengamati lebih dari satu atribut, kita dapat menuliskannya layaknya array literals.

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



Setelah mengimplementasi kedua fungsi tadi seharusnya custom element sudah dapat bereaksi ketika terjadi perubahan nilai atribut.
20200310185339fbfc985571e3eec644d9202d7a27103b.gif

Styling Custom Element without Shadow DOM

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.
  1. class ImageFigure extends HTMLElement {
  2.  
  3. ......
  4.  
  5.  render() {
  6.     this.innerHTML = `
  7.       <style>
  8.         figure {
  9.           border: thin #c0c0c0 solid;
  10.           display: flex;
  11.           flex-flow: column;
  12.           padding: 5px;
  13.           max-width: 220px;
  14.           margin: auto;
  15.         }
  16.  
  17.         figure > img {
  18.           max-width: 220px;
  19.         }
  20.  
  21.         figure > figcaption {
  22.           background-color: #222;
  23.           color: #fff;
  24.           font: italic smaller sans-serif;
  25.           padding: 3px;
  26.           text-align: center;
  27.         }
  28.       </style>
  29.  
  30.       <figure>
  31.         <img src="${this.src}"
  32.             alt="${this.alt}">
  33.         <figcaption>${this.caption}</figcaption>
  34.       </figure>
  35.     `;
  36.  }
  37.  
  38. ......
  39. }
  40.  
  41. customElements.define("image-figure", ImageFigure);

Ataupun dengan menuliskan styling pada berkas css yang ditautkan pada html.
  1. /* Berkas style.css */
  2. figure {
  3.    border: thin #c0c0c0 solid;
  4.    display: flex;
  5.    flex-flow: column;
  6.    padding: 5px;
  7.    max-width: 220px;
  8.    margin: auto;
  9. }
  10.  
  11. figure > img {
  12.    max-width: 220px;
  13. }
  14.  
  15. figure > figcaption {
  16.    background-color: #222;
  17.    color: #fff;
  18.    font: italic smaller sans-serif;
  19.    padding: 3px;
  20.    text-align: center;
  21. }

Maka pada browser akan menampilkan hasil seperti berikut:

2020031018584997dd30daf430969f36872543cfbea701.png
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

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?

  1. const article = {

  2.  id: 1,

  3.  title: "Lorem Ipsum Dolor",

  4.  featuredImage: "https://i.picsum.photos/id/204/536/354.jpg?grayscale",

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

  6. }



Tentu jika kita simpan nilai tersebut pada atribut HTML akan terlihat berantakan pada penulisan elemennya.


  1. <article-item

  2.        id="1"

  3.        title="Lorem Ipsum Dolor"

  4. featured-image="https://i.picsum.photos/id/204/536/354.jpg?grayscale"

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

  1. class ArticleItem extends HTMLElement {

  2.  set article(article) {

  3.    this._article = article;

  4.    this.render();

  5.  }

  6.  

  7.  render() {

  8.    this.innerHTML = `

  9.      <img class="featured-image" src="${this._article.featuredImage}">

  10.      <div class="article-info">

  11.          <h2><a href="${this._article.id}">${this._article.title}</a></h2>

  12.          <p>${this._article.description}</p>

  13.      </div>

  14.    `;

  15.  }

  16. }

  17.  

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

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

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

  1. <!DOCTYPE html>

  2. <html>

  3.  <head>

  4.    <meta charset="utf-8">

  5.    <meta name="viewport" content="width=device-width">

  6.    <title>Handling Complex Data</title>

  7.    <link href="style.css" rel="stylesheet" type="text/css" />

  8.  </head>

  9.  <body>

  10.    <div class="container">

  11.    </div>

  12.    <script src="script.js" type="module"></script>

  13.  </body>

  14. </html>



  1. * {

  2.  padding: 0;

  3.  margin: 0;

  4.  box-sizing: border-box;

  5. }

  6.  

  7. body {

  8.  padding: 16px;

  9. }

  10.  

  11. .container {

  12.  max-width: 800px;

  13.  margin: 0 auto;

  14. }

  15.  

  16.  

  17. article-item {

  18.    display: block;

  19.    margin-bottom: 18px;

  20.    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);

  21.    border-radius: 10px;

  22.    overflow: hidden;

  23. }

  24.  

  25.  

  26. article-item > .featured-image {

  27.    width: 100%;

  28.    max-height: 300px;

  29.    object-fit: cover;

  30.    object-position: center;

  31. }

  32.  

  33.  

  34. article-item > .article-info {

  35.  padding: 24px;

  36. }

  37.  

  38.  

  39. article-item > .article-info > p {

  40.  margin-top: 10px;

  41. }










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

  2. import article from "./article.js";

  3.  

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

  5.  

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

  7. articleItemElement.article = article;

  8.  

  9. containerElement.appendChild(articleItemElement);



  1. class ArticleItem extends HTMLElement {

  2.  set article(article) {

  3.    this._article = article;

  4.    this.render();

  5.  }

  6.  

  7.  render() {

  8.    this.innerHTML = `

  9.      <img class="featured-image" src="${this._article.featuredImage}">

  10.      <div class="article-info">

  11.          <h2><a href="${this._article.id}">${this._article.title}</a></h2>

  12.          <p>${this._article.description}</p>

  13.      </div>

  14.    `;

  15.  }

  16. }

  17.  

  18. customElements.define("article-item", ArticleItem);




  1. const article = {

  2.  id: 1,

  3.  title: "Lorem Ipsum Dolor",

  4.  featuredImage: "https://i.picsum.photos/id/204/536/354.jpg?grayscale",

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

  6. }

  7.  

  8. export default article;


Jika kita jalankan maka browser akan menampilkan element <article-item> dengan data yang didapat dari article.js.
2020031019100333dd180dab2d0832b94ce5a490bc04aa.png

Nested Custom Element


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.
20200310191307a4fe82d6dd70b4daae920036121ba5c4.gif
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:

  1. <!DOCTYPE html>

  2. <html>

  3.  <head>

  4.    <meta charset="utf-8">

  5.    <meta name="viewport" content="width=device-width">

  6.    <title>repl.it</title>

  7.    <link href="style.css" rel="stylesheet" type="text/css" />

  8.  </head>

  9.  <body>

  10.    <script src="script.js" type="module"></script>

  11.  </body>

  12. </html>




  1. import "./article-list.js";

  2. import articles from "./articles.js";

  3.  

  4.  

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

  6. articleListElement.articles = articles;

  7.  

  8.  

  9. document.body.appendChild(articleListElement);




  1. import "./article-item.js"

  2.  

  3.  

  4. class ArticleList extends HTMLElement {

  5.  set articles(articles) {

  6.    this._articles = articles;

  7.    this.render();

  8.  }

  9.  

  10.  

  11.  render() {

  12.    this._articles.forEach(article => {

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

  14.      // memanggil fungsi setter article() pada article-item.

  15.      articleItemElement.article = article;

  16.      this.appendChild(articleItemElement);

  17.    })

  18.  }

  19. }

  20.  

  21. customElements.define("article-list", ArticleList);



  1. class ArticleItem extends HTMLElement {

  2.  set article(article) {

  3.    this._article = article;

  4.    this.render();

  5.  }

  6.  

  7.  

  8.  render() {

  9.    this.innerHTML = `

  10.     <img class="featured-image" src="${this._article.featuredImage}">

  11.     <div class="article-info">

  12.         <h2><a href="${this._article.id}">${this._article.title}</a></h2>

  13.         <p>${this._article.description}</p>

  14.     </div>

  15.   `;

  16.  }

  17. }

  18.  

  19.  

  20. customElements.define("article-item", ArticleItem);




  1. const articles = [

  2.  {

  3.    id: 1,

  4.    title: "Lorem Ipsum Dolor",

  5.    featuredImage: "https://i.picsum.photos/id/204/536/354.jpg",

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

  7.  

  8.  

  9.  },

  10.  {

  11.    id: 2,

  12.    title: "Lorem Ipsum Dolor",

  13.    featuredImage: "https://i.picsum.photos/id/209/536/354.jpg",

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

  15.  

  16.  

  17.  },

  18.  {

  19.    id: 3,

  20.    title: "Lorem Ipsum Dolor",

  21.    featuredImage: "https://i.picsum.photos/id/206/536/354.jpg",

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

  23.  

  24.  

  25.  },

  26.  {

  27.    id: 4,

  28.    title: "Lorem Ipsum Dolor",

  29.    featuredImage: "https://i.picsum.photos/id/212/536/354.jpg",

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

  31.  

  32.  

  33.  }

  34. ]

  35.  

  36.  

  37. export default articles;








  1. * {

  2.     padding: 0;

  3.     margin: 0;

  4.     box-sizing: border-box;

  5.   }

  6.   

  7.   body {

  8.     padding: 16px;

  9.   }

  10.   

  11.   article-list {

  12.     display: block;

  13.     max-width: 800px;

  14.     margin: 0 auto;

  15.   }

  16.   

  17.   article-item {

  18.     display: block;

  19.     margin-bottom: 18px;

  20.     box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);

  21.     border-radius: 10px;

  22.     overflow: hidden;

  23.   }

  24.   

  25.   article-item > .featured-image {

  26.     width: 100%;

  27.     max-height: 300px;

  28.     object-fit: cover;

  29.     object-position: center;

  30.   }

  31.   

  32.   article-item > .article-info {

  33.     padding: 24px;

  34.   }

  35.   

  36.   article-item > .article-info > p {

  37.     margin-top: 10px;

  38.   }








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

  1. set articles(articles) {

  2.    this._articles = articles;

  3.    this.render();

  4. }


Kemudian properti tersebut digunakan pada fungsi render() untuk ditampilkan satu persatu melalui <article-item>.

  1. render() {

  2.    this._articles.forEach(article => {

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

  4.      // memanggil fungsi setter article() pada article-item.

  5.      articleItemElement.article = article;

  6.      this.appendChild(articleItemElement);

  7.    })

  8. }


Dengan begitu, untuk menampilkan data pada script.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.

  1. import "./article-list.js";

  2. import articles from "./articles.js";

  3.  

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

  5. articleListElement.articles = articles;

  6.  

  7. 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:
202003101921481bcb54550f43e9958256358a3c73709b.png
Potongan kode untuk seluruh contoh custom element yang digunakan pada materi ini: https://repl.it/@dicodingacademy/163-03-custom-element?lite=true

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.
20200310192559e057df7d65da0d6b4369a31dfa0bcf60.png
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.

  1. class AppBar extends HTMLElement {

  2.   

  3. }


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

  1. class AppBar extends HTMLElement {

  2.    connectedCallback(){

  3.  

  4.    }

  5.  

  6.    render() {

  7.       

  8.    }

  9. }


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.

  1. class AppBar extends HTMLElement {

  2.    connectedCallback(){

  3.        this.render();

  4.    }

  5.  

  6.    render() {

  7.  

  8.    }

  9. }


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

  1. <header>

  2.        <div id="appBar" class="app-bar">

  3.            <h2>Club Finder</h2>

  4.        </div>

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

  1. class AppBar extends HTMLElement {

  2.    connectedCallback(){

  3.        this.render();

  4.    }

  5.  

  6.    render() {

  7.        this.innerHTML = `<h2>Club Finder</h2>`;

  8.    }

  9. }


Lalu di akhir berkas app-bar.js, jangan lupa untuk definisikan custom element yang kita buat agar dapat digunakan pada DOM.

  1. class AppBar extends HTMLElement {

  2.    connectedCallback(){

  3.        this.render();

  4.    }

  5.  

  6.    render() {

  7.        this.innerHTML = `<h2>Club Finder</h2>`;

  8.    }

  9. }

  10.  

  11. customElements.define("app-bar", AppBar);


Dengan begitu kita dapat mengubah penerapan app-bar pada index.html dengan menggunakan tag <app-bar>.

  1. <header>

  2.       <app-bar></app-bar>

  3. </header>


Terakhir, agar kode pada berkas app-bar.js tereksekusi, impor berkas app-bar.js pada berkas app.js, seperti ini:

  1. import "./src/script/component/app-bar.js";


Tuliskan kode tersebut pada awal berkas app.js, sehingga keseluruhan kode pada berkasnya akan tampak seperti ini:

  1. import "./src/script/component/app-bar.js";

  2. import main from "./src/script/view/main.js";

  3.  

  4. document.addEventListener("DOMContentLoaded", main);


Kemudian coba kita buka proyeknya menggunakan local server. Inilah tampilan hasilnya:
2020031019350448bf1ff8d96767443908e81b171563c3.png
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.

  1. app-bar {

  2.    padding: 16px;

  3.    width: 100%;

  4.    background-color: cornflowerblue;

  5.    color: white;

  6.    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);

  7. }


Lalu lihat kita lihat hasilnya.
2020031019360718a18a9301e96c9237e9d2bd80d3dda2.png
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.

  1. app-bar {

  2.    display: block;

  3.    padding: 16px;

  4.    width: 100%;

  5.    background-color: cornflowerblue;

  6.    color: white;

  7.    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);

  8. }


Dengan begitu tampilan kita berhasil membuat custom element <app-bar> dengan baik!
20200310193753886f026632febaa362d6775a39bbc28d.png

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.

  1. class SearchBar extends HTMLElement {

  2.   

  3. }


Kemudian kita implementasi method connectedCallback dan membuat fungsi render.

  1. class SearchBar extends HTMLElement {

  2.    connectedCallback(){

  3.  

  4.    }

  5.   

  6.    render() {

  7.       

  8.    }

  9. }


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

  1. class SearchBar extends HTMLElement {

  2.    connectedCallback(){

  3.        this.render();

  4.    }

  5.   

  6.    render() {

  7.  

  8.    }

  9. }


Di dalam fungsi render kita ambil elemen yang dibutuhkan untuk ditampilkan dari berkas index.html.

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

  2.    <input placeholder="Search football club" id="searchElement" type="search">

  3.     <button id="searchButtonElement" type="submit">Search</button>

  4. </div>



Agar mudah, copy seluruh kode tersebut dan paste untuk dijadikan nilai this.innerHTML di dalam fungsi render.

  1. class SearchBar extends HTMLElement {

  2.    connectedCallback(){

  3.        this.render();

  4.    }

  5.   

  6.    render() {

  7.        this.innerHTML = `

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

  9.            <input placeholder="Search football club" id="searchElement" type="search">

  10.            <button id="searchButtonElement" type="submit">Search</button>

  11.        </div>

  12.        `;

  13.    }

  14. }


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.
  1. class SearchBar extends HTMLElement {
  2.    connectedCallback(){
  3.        this.render();
  4.    }
  5.   
  6.    set clickEvent(event) {
  7.        this._clickEvent = event;
  8.        this.render();
  9.    }
  10.  
  11.    render() {
  12.        this.innerHTML = `
  13.        <div id="search-container" class="search-container">
  14.            <input placeholder="Search football club" id="searchElement" type="search">
  15.            <button id="searchButtonElement" type="submit">Search</button>
  16.        </div>
  17.        `;
  18.    }
  19. }

Lalu kita terapkan this._clickEvent sebagai event pada element <button> dengan cara menuliskan kode berikut pada akhir fungsi render():

  1. this.querySelector("#searchButtonElement").addEventListener("click", this._clickEvent);



Sehingga kode pada fungsi render akan tampak seperti ini:
  1. render() {
  2.   this.innerHTML = `
  3.     <div id="search-container" class="search-container">
  4.        <input placeholder="Search football club" id="searchElement" type="search">
  5.        <button id="searchButtonElement" type="submit">Search</button>
  6.     </div>`;
  7.  
  8.    this.querySelector("#searchButtonElement").addEventListener("click", this._clickEvent);
  9. }

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.

  1. get value() {

  2.    return this.querySelector("#searchElement").value;

  3. }



Sehingga keseluruhan kode yang terdapat berkas search-bar.js akan terlihat seperti ini:
  1. class SearchBar extends HTMLElement {
  2.    connectedCallback(){
  3.        this.render();
  4.    }
  5.   
  6.    set clickEvent(event) {
  7.        this._clickEvent = event;
  8.        this.render();
  9.    }
  10.  
  11.    get value() {
  12.        return this.querySelector("#searchElement").value;
  13.    }
  14.  
  15.    render() {
  16.        this.innerHTML = `
  17.        <div id="search-container" class="search-container">
  18.            <input placeholder="Search football club" id="searchElement" type="search">
  19.            <button id="searchButtonElement" type="submit">Search</button>
  20.        </div>
  21.        `;
  22.  
  23.        this.querySelector("#searchButtonElement").addEventListener("click", this._clickEvent);
  24.    }
  25. }

Lalu di akhir berkasnya, jangan lupa untuk definisikan custom element yang kita buat agar dapat digunakan pada DOM.
  1. class SearchBar extends HTMLElement {
  2.    connectedCallback(){
  3.        this.render();
  4.    }
  5.   
  6.    set clickEvent(event) {
  7.        this._clickEvent = event;
  8.        this.render();
  9.    }
  10.  
  11.    get value() {
  12.        return this.querySelector("#searchElement").value;
  13.    }
  14.  
  15.    render() {
  16.        this.innerHTML = `
  17.        <div id="search-container" class="search-container">
  18.            <input placeholder="Search football club" id="searchElement" type="search">
  19.            <button id="searchButtonElement" type="submit">Search</button>
  20.        </div>
  21.        `;
  22.  
  23.        this.querySelector("#searchButtonElement").addEventListener("click", this._clickEvent);
  24.    }
  25. }
  26.  
  27. 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:

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

  2.     <input placeholder="Search football club" id="searchElement" type="search">

  3.     <button id="searchButtonElement" type="submit">Search</button>

  4. </div>


Menjadi seperti ini:

  1. <search-bar></search-bar>


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

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


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

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

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


Kemudian kita sesuaikan kembali penerapan event click pada komponen pencarian dengan mengubah kode berikut:

  1. buttonSearchElement.addEventListener("click", onButtonSearchClicked);


Menjadi:

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

  1. import '../component/search-bar.js';


Tuliskan kode tersebut pada awal berkas main.js, sehingga keseluruhan kode pada berkasnya akan tampak seperti ini:
  1. import '../component/search-bar.js';
  2. import DataSource from '../data/data-source.js';
  3.  
  4. const main = () => {
  5.    const searchElement = document.querySelector("search-bar");
  6.    const clubListElement = document.querySelector("#clubList");
  7.  
  8.    const onButtonSearchClicked = async () => {
  9.        try {
  10.            const result = await DataSource.searchClub(searchElement.value);
  11.            renderResult(result);
  12.        } catch (message) {
  13.            fallbackResult(message)
  14.        }
  15.    };
  16.  
  17.    const renderResult = results => {
  18.        clubListElement.innerHTML = "";
  19.        results.forEach(club => {
  20.            const { name, fanArt, description } = club;
  21.            const clubElement = document.createElement("div");
  22.            clubElement.setAttribute("class", "club");
  23.  
  24.            clubElement.innerHTML = `
  25.                <img class="fan-art-club" src="${fanArt}" alt="Fan Art">
  26.                <div class="club-info">
  27.                    <h2>${name}</h2>
  28.                    <p>${description}</p>
  29.                </div>`;
  30.  
  31.            clubListElement.appendChild(clubElement);
  32.        })
  33.    };
  34.  
  35.    const fallbackResult = message => {
  36.        clubListElement.innerHTML = "";
  37.        clubListElement.innerHTML += `<h2 class="placeholder">${message}</h2>`;
  38.    };
  39.  
  40.    searchElement.clickEvent = onButtonSearchClicked;
  41. };
  42.  
  43. export default main;

Kemudian coba kita buka proyeknya menggunakan local server kemudian lakukan pencarian dengan menggunakan kata kunci “Arsenal”.  Hasilnya adalah tampilan berikut:
202003102009124b6eec15399d963670bbeffeecf30a42.png

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.

  1. class ClubList extends HTMLElement {


  2. }


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

  1. class ClubList extends HTMLElement {

  2.    set clubs(clubs) {

  3.  

  4.    }

  5.  

  6.    render() {

  7.       

  8.    }

  9. }


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

  1. set clubs(clubs) {

  2.      this._clubs = clubs;

  3.      this.render();

  4. }


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:

  1. render() {

  2.        this.innerHTML = "";

  3.        this._clubs.forEach(club => {

  4.            const clubItemElement = document.createElement("club-item");

  5.            clubItemElement.club = club

  6.            this.appendChild(clubItemElement);

  7.        })

  8. }


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.

  1. renderError(message) {

  2.  

  3. }


Untuk template html yang akan ditampilkan, kita dapat copy dari fungsi fallbackResult pada berkas src -> script -> view -> main.js.

  1. clubListElement.innerHTML = "";

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


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

  1. renderError(message) {

  2.        this.innerHTML = "";

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

  4. }


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

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

  1. import './club-item.js';


Sehingga sekarang keseluruhan kode yang terdapat pada berkas ini akan tampak seperti ini:
  1. import './club-item.js';
  2.  
  3. class ClubList extends HTMLElement {
  4.    set clubs(clubs) {
  5.        this._clubs = clubs;
  6.        this.render();
  7.    }
  8.  
  9.    renderError(message) {
  10.        this.innerHTML = "";
  11.        this.innerHTML += `<h2 class="placeholder">${message}</h2>`;
  12.    }
  13.  
  14.    render() {
  15.        this.innerHTML = "";
  16.        this._clubs.forEach(club => {
  17.            const clubItemElement = document.createElement("club-item");
  18.            clubItemElement.club = club
  19.            this.appendChild(clubItemElement);
  20.        })
  21.    }
  22. }
  23.  
  24. 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.

  1. class ClubItem extends HTMLElement {

  2.   

  3. }


Kemudian kita buat fungsi setter club dan fungsi render.

  1. class ClubItem extends HTMLElement {

  2.    set club(club) {

  3.  

  4.    }

  5.  

  6.    render() {

  7.       

  8.    }

  9. }


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:

  1. class ClubItem extends HTMLElement {

  2.    set club(club) {

  3.        this._club = club;

  4.        this.render();

  5.    }

  6.  

  7.    render() {

  8.  

  9.    }

  10. }


Lalu kita copy template html yang berada pada fungsi renderResult di berkas src -> script -> view -> main.js.

  1. clubElement.innerHTML = `

  2.          <img class="fan-art-club" src="${fanArt}" alt="Fan Art">

  3.            <div class="club-info">

  4.               <h2>${name}</h2>

  5.               <p>${description}</p>

  6. </div>`;


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

  1. class ClubItem extends HTMLElement {

  2.    set club(club) {

  3.        this._club = club;

  4.        this.render();

  5.    }

  6.  

  7.    render() {

  8.        this.innerHTML = `

  9.            <img class="fan-art-club" src="${fanArt}" alt="Fan Art">

  10.            <div class="club-info">

  11.                <h2>${name}</h2>

  12.                <p>${description}</p>

  13.            </div>`;

  14.    }

  15. }


Lalu kita sesuaikan kembali properti-properti yang digunakan pada html template, menjadi seperti ini:

  1. class ClubItem extends HTMLElement {

  2.    set club(club) {

  3.        this._club = club;

  4.        this.render();

  5.    }

  6.  

  7.    render() {

  8.        this.innerHTML = `

  9.            <img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">

  10.            <div class="club-info">

  11.                <h2>${this._club.name}</h2>

  12.                <p>${this._club.description}</p>

  13.            </div>`;

  14.    }

  15. }


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.

  1. class ClubItem extends HTMLElement {

  2.    set club(club) {

  3.        this._club = club;

  4.        this.render();

  5.    }

  6.  

  7.    render() {

  8.        this.innerHTML = `

  9.            <img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">

  10.            <div class="club-info">

  11.                <h2>${this._club.name}</h2>

  12.                <p>${this._club.description}</p>

  13.            </div>`;

  14.    }

  15. }

  16.  

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

  1. <div id="clubList"></div>


Menjadi:

  1. <club-list></club-list>


Selanjutnya buka berkas src -> script -> view -> main.js. Kita sesuaikan kembali selector pada saat melakukan binding clubListElement. Ubah kode berikut:

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


Menjadi:

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

  1. const renderResult = results => {

  2.        clubListElement.innerHTML = "";

  3.        results.forEach(club => {

  4.            const { name, fanArt, description } = club;

  5.            const clubElement = document.createElement("div");

  6.            clubElement.setAttribute("class", "club");

  7.  

  8.            clubElement.innerHTML = `

  9.                <img class="fan-art-club" src="${fanArt}" alt="Fan Art">

  10.                <div class="club-info">

  11.                    <h2>${name}</h2>

  12.                    <p>${description}</p>

  13.                </div>`;

  14.  

  15.            clubListElement.appendChild(clubElement);

  16.        })

  17. };


Kita cukup menggantinya dengan seperti ini:

  1. const renderResult = results => {

  2.      clubListElement.clubs = results;

  3. };


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

  1. const fallbackResult = message => {

  2.        clubListElement.renderError(message);

  3. };


Karena kita menggunakan elemen <club-list> pada berkas main.js, maka kita perlu melakukan impor berkas club-list.js pada berkas main.js.

  1. import '../component/club-list.js';


Dengan begitu keseluruhan kode pada berkas main.js akan tampak seperti berikut:
  1. import '../component/club-list.js';
  2. import '../component/search-bar.js';
  3. import DataSource from '../data/data-source.js';
  4.  
  5. const main = () => {
  6.    const searchElement = document.querySelector("search-bar");
  7.    const clubListElement = document.querySelector("club-list");
  8.  
  9.    const onButtonSearchClicked = async () => {
  10.        try {
  11.            const result = await DataSource.searchClub(searchElement.value);
  12.            renderResult(result);
  13.        } catch (message) {
  14.            fallbackResult(message)
  15.        }
  16.    };
  17.  
  18.    const renderResult = results => {
  19.        clubListElement.clubs = results;
  20.    };
  21.  
  22.    const fallbackResult = message => {
  23.        clubListElement.renderError(message);
  24.    };
  25.  
  26.    searchElement.clickEvent = onButtonSearchClicked;
  27. };
  28.  
  29. export default main;

Sekarang kita coba buka proyeknya menggunakan local server lalu tekan tombol pencarian. Voila, inilah tampilan hasilnya:
202003102031456392a6f8c00cbb02e2dc53ed846c7d2a.png
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.
  1. club-list {
  2.    margin-top: 32px;
  3.    width: 100%;
  4.    padding: 16px;
  5. }
  6.  
  7. club-list > .placeholder {
  8.    font-weight: lighter;
  9.    color: rgba(0,0,0,0.5);
  10.    -webkit-user-select: none;
  11.    -moz-user-select: none;
  12.    -ms-user-select: none;
  13.    user-select: none;
  14. }
  15.  
  16. club-item {
  17.    margin-bottom: 18px;
  18.    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  19.    border-radius: 10px;
  20.    overflow: hidden;
  21. }
  22.  
  23. club-item .fan-art-club {
  24.    width: 100%;
  25.    max-height: 300px;
  26.    object-fit: cover;
  27.    object-position: center;
  28. }
  29.  
  30. .club-info {
  31.    padding: 24px;
  32. }
  33.  
  34. .club-info > h2 {
  35.    font-weight: lighter;
  36. }
  37.  
  38. .club-info > p {
  39.    margin-top: 10px;
  40.    overflow: hidden;
  41.    text-overflow: ellipsis;
  42.    display: -webkit-box;
  43.    -webkit-box-orient: vertical;
  44.    -webkit-line-clamp: 10; /* number of lines to show */
  45. }

Kemudian tambahkan juga properti display dengan nilai block pada selector club-list dan club-item.

  1. club-list {

  2.    display: block;

  3.    ….

  4. }

  5.  

  6. ….

  7.  

  8. club-item {

  9.    display: block;

  10.    ….

  11. }

  12.  

  13. ….


Sehingga keseluruhan kode pada berkas clublist.css akan tampak seperti ini:
  1. club-list {
  2.    display: block;
  3.    margin-top: 32px;
  4.    width: 100%;
  5.    padding: 16px;
  6. }
  7.  
  8. club-list > .placeholder {
  9.    font-weight: lighter;
  10.    color: rgba(0,0,0,0.5);
  11.    -webkit-user-select: none;
  12.    -moz-user-select: none;
  13.    -ms-user-select: none;
  14.    user-select: none;
  15. }
  16.  
  17. club-item {
  18.    display: block;
  19.    margin-bottom: 18px;
  20.    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  21.    border-radius: 10px;
  22.    overflow: hidden;
  23. }
  24.  
  25. club-item .fan-art-club {
  26.    width: 100%;
  27.    max-height: 300px;
  28.    object-fit: cover;
  29.    object-position: center;
  30. }
  31.  
  32. .club-info {
  33.    padding: 24px;
  34. }
  35.  
  36. .club-info > h2 {
  37.    font-weight: lighter;
  38. }
  39.  
  40. .club-info > p {
  41.    margin-top: 10px;
  42.    overflow: hidden;
  43.    text-overflow: ellipsis;
  44.    display: -webkit-box;
  45.    -webkit-box-orient: vertical;
  46.    -webkit-line-clamp: 10; /* number of lines to show */
  47. }

Sekarang kita coba buka kembali proyek club finder dengan menggunakan local server. Seharusnya kini semuanya sudah berjalan dengan baik.
20200310203504226e0e544df5e8e4b9b389c9fa9f3f0c.png

Langkah dari ketiga solution ini bisa Anda temukan juga pada repository berikut: https://github.com/dicodingacademy/a163-bfwd-labs/tree/109-club-finder-custom-element-solution