Belajar Membuat Custom Element Dan Shadow DOM Dasar di Javascript
Ihsan Magazine - Kali ini kita akan mencoba belajar membuat Custom Element dasar di Javascript, apa itu Custom Element? Gampang nya Custom Element seperti kita Membuat Tag HTML baru untuk Front End Developer.
Bisa untuk memperluas elemen bawaan HTML, dan masih banyak lagi, dasarnya menggunakan\ Elements global digunakan untuk mendefinisikan elemen khusus dan mengajarkan tag baru pada browser.
Panggil customElements.define() dengan nama tag yang ingin Anda buat dan class JavaScript yang memperluas HTMLElement dasar.
Selengkapnya Baca Disini
Kali ini Study Kasus dengan ClubFinder.zip
Jika kita telaah dengan seksama, pada proyek Club Finder terdapat 4 (empat) bagian yang berpotensi untuk dijadikan custom element, yaitu:
Shadow DOM
Menerapkan Shadow DOM pada Proyek Club Finder
Bisa untuk memperluas elemen bawaan HTML, dan masih banyak lagi, dasarnya menggunakan\ Elements global digunakan untuk mendefinisikan elemen khusus dan mengajarkan tag baru pada browser.
Panggil customElements.define() dengan nama tag yang ingin Anda buat dan class JavaScript yang memperluas HTMLElement dasar.
Selengkapnya Baca Disini
Kali ini Study Kasus dengan ClubFinder.zip
Jika kita telaah dengan seksama, pada proyek Club Finder terdapat 4 (empat) bagian yang berpotensi untuk dijadikan custom element, yaitu:
- App Bar : Komponen di posisi atas yang menunjukkan identitas atau nama dari aplikasi web.
- Search Bar : Komponen yang terdiri dari elemen <input> dan <button> dan berfungsi untuk melakukan pencarian club sesuai dengan input pengguna.
- Club List : Komponen yang berfungsi untuk menampung data dari hasil pencarian, kemudian menampilkannya dalam bentuk list.
- Club Item : Komponen yang menampilkan data individual club yang diberikan dari club list. Komponen ini terdiri dari gambar, nama, dan deskripsi singkat club.
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!
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 main.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:
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">
- <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._club.name}</h2>
- <p>${this._club.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.
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
Shadow DOM
Menerapkan Shadow DOM pada Proyek Club Finder
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-item.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!
Langkah dari solution ini bisa Anda temukan juga pada repository berikut: https://github.com/dicodingacademy/a163-bfwd-labs/tree/110-club-finder-shadow-dom-solution