news

Rabu, 10 Juni 2020

Belajar Shadow DOM Dasar Untuk Web Components


Halaman website yang ditampilkan terbentuk dari HTML. HTML sangat membantu kita karena ia cukup mudah dipelajari dan digunakan. HTML mudah dipahami oleh kita namun tidak untuk mesin.
sehingga terciptalah DOM (Document Object Model) sebagai penghubung antara HTML dengan bahasa pemrograman. 
Di dalam DOM seluruh struktur HTML dapat digambarkan dalam bentuk objek yang dapat dimanipulasi melalui bahasa pemrograman, salah satunya JavaScript.
Ketika browser memuat halaman, HTML akan secara otomatis dimodelkan menjadi sebuah object dan nodes hingga membentuk “DOM Tree”. Berikut contoh DOM Tree yang terbuat:
20200310203728706a9133fa852226f12e0fbff86a3c37.png
Dari struktur HTML berikut:

  1. <html>

  2.   <head>

  3.     <title>Web Components</title>

  4.   </head>

  5.   <body>

  6.     <h1>Mari belajar Shadow DOM</h1>

  7.   </body>

  8. </html>



Object dan nodes yang dihasilkan dari DOM akan memiliki properti dan method yang dapat kita manfaatkan untuk memanipulasi konten di dalamnya. 
Seluruh elemen dan style pada HTML (apapun yang berada di dalam DOM) akan terekspos secara global dan nilainya dapat kita peroleh dari mana saja. 
Biasanya untuk mendapatkan element kita gunakan document.querySelector, setelah itu kita dapat leluasa mengontrol elemen, mengubah konten di dalamnya ataupun mengubah styling yang diterapkan.

Encapsulation

Saat ini banyak web yang dibangun melalui arsitektur berbasis komponen sehingga diharapkan komponen tersebut dapat digunakan kembali. 
Namun bukankah tidak baik jika komponen tersebut dapat diganggu dan diubah dari luar? Sebaiknya komponen dapat bertahan dari gangguan luar agar secara visual atau fungsinya agar tetap dalam keadaan aslinya. Maka dari itu, kita perlu menerapkan konsep enkapsulasi pada komponen tersebut

What is Shadow DOM?

Saat ini kita mungkin bisa menerapkan konsep enkapsulasi dengan menggunakan <iframe> agar komponen terpisah dari gangguan luar. Namun teknik ini bukan cara yang baik, berat, dan dapat menimbulkan masalah. Lantas bagaimana solusinya? Gunakanlah Shadow DOM.
Shadow DOM dapat mengisolasi sebagian struktur DOM di dalam komponen sehingga tidak dapat disentuh dari luar komponen atau nodenya. Singkatnya kita bisa sebut Shadow DOM sebagai “DOM dalam DOM”. Bagaimana ia bekerja? Perhatikan ilustrasi berikut:
Shadow DOM dapat membuat DOM Tree lain terbentuk secara terisolasi melalui host yang merupakan komponen dari regular DOM Tree (Document Tree). Shadow DOM Tree ini dimulai dari root bayangan (Shadow root), yang dibawahnya dapat memiliki banyak element lagi layaknya Document Tree.
Terdapat beberapa terminologi yang perlu kita ketahui dari ilustrasi di atas:
  • Shadow host : Merupakan komponen/node yang terdapat pada regular DOM di mana shadow DOM terlampir pada komponen/node ini.
  • Shadow tree : DOM Tree di dalam shadow DOM.
  • Shadow boundary : Batas dari shadow DOM dengan regular DOM.
  • Shadow root : Root node dari shadow tree.
Kita dapat memanipulasi elemen yang terdapat di dalam shadow tree layaknya pada document tree, namun cakupannya selama kita berada di dalam shadow boundary. Dengan kata lain, jika kita berada di document tree kita tidak dapat memanipulasi elemen bahkan menerapkan styling pada elemen yang terdapat di dalam shadow tree. Itulah mengapa shadow DOM dapat membuat komponen terenkapsulasi

Basic Usage

Potongan kode untuk materi ini:
Untuk melampirkan Shadow DOM pada elemen penggunaan sangat mudah, yaitu dengan menggunakan properti attachShadow pada elemen-nya seperti ini:

  1. // Shadow Host

  2. const divElement = document.createElement("div");

  3.  

  4.  

  5. // element yang berada di dalam Shadow DOM

  6. const headingElement = document.createElement("h1");

  7. headingElement.innerText = "Ini merupakan konten di dalam shadow DOM";

  8.  

  9.  

  10. // Melampirkan shadow root pada shadow host

  11. // Mengatur mode shadow dengan nilai open

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

  13.  

  14.  

  15. // Memasukkan element heading ke dalam shadow root

  16. shadowRoot.appendChild(headingElement);

  17.  

  18.  

  19. // Memasukkan elemen shadow host ke regular DOM

  20. document.body.appendChild(divElement);



  1. <!DOCTYPE html>

  2. <html>

  3.  <head>

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

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

  6.    <title>Shadow DOM Basic Usage</title>

  7.  </head>

  8.  <body>

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

  10.  </body>

  11. </html>


Jika kita lihat pada browser, maka struktur HTML yang akan dihasilkan adalah seperti ini:
2020031020442138d22cf8f4814aacc795f563c3015892.png
Dan struktur DOM tree yang terbentuk akan tampak seperti ini:
20200310204452a9e4788ac5f7f206c20debfa0bc87524.png
Dalam penggunaan attachShadow() kita melampirkan objek dengan properti mode yang memiliki nilai ‘open’. Sebenarnya terdapat dua opsi nilai yang dapat digunakan dalam properti mode, yaitu “open” dan “closed”. 
Menggunakan nilai open berarti kita memperbolehkan untuk mengakses properti shadowRoot melalui elemen yang melampirkan Shadow DOM. 

  1. divElement.attachShadow;


properti shadowRoot mengembalikan struktur DOM yang berada pada shadow tree.
20200310204643a7a1f999761eaf74f9476e50013bb373.gif
Namun jika kita menggunakan nilai closed maka properti shadowRoot akan mengembalikan nilai null

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

  2. divElement.shadowRoot // null;


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

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

  2. divElement.shadowRoot // null;

  3. shadowRoot // # shadow-root (closed)


2020031020483296ef7fce493fd7c0528697c3fa03565b.gif
Karena Shadow DOM terisolasi dari document tree maka element yang terdapat di dalamnya pun tidak akan terpengaruh oleh styling yang berada diluar dari shadow root-nya.

  1. <!DOCTYPE html>

  2. <html>

  3.  <head>

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

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

  6.    <title>Shadow DOM Basic Usage</title>

  7.    <style>

  8.        h1 {

  9.          color: red;

  10.        }

  11.    </style>

  12.  </head>

  13.  <body>

  14.    <h1>Ini merupakan konten yang berada di Document tree</h1>

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

  16.  </body>

  17. </html>



  1. // Shadow Host

  2. const divElement = document.createElement("div");

  3.  

  4.  

  5. // element yang berada di dalam Shadow DOM

  6. const headingElement = document.createElement("h1");

  7. headingElement.innerText = "Ini merupakan konten di dalam shadow DOM";

  8.  

  9.  

  10. // Melampirkan shadow root pada shadow host

  11. // Mengatur mode shadow dengan nilai open

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

  13.  

  14.  

  15. // Memasukkan element heading ke dalam shadow root

  16. shadowRoot.appendChild(headingElement);

  17.  

  18.  

  19. // Memasukkan elemen shadow host ke regular DOM

  20. document.body.appendChild(divElement);


Jika dilihat pada browser maka hasilnya akan seperti ini:
20200310205023649403bfa8d61e879afc65f4a050c502.png
Berdasarkan hasil di atas, styling hanya akan diterapkan pada elemen <h1> yang berada di document tree. Sedangkan elemen <h1> yang berada pada shadow dom tidak akan terpengaruh dengan styling tersebut. Lantas, bagaimana caranya kita melakukan styling pada Shadow DOM?
Kita dapat melakukannya dengan menambahkan template <style> di dalam shadowRoot.innerHTML.  Contohnya seperti ini:

  1. // menetapkan styling pada Shadow DOM

  2. shadowRoot.innerHTML += `

  3.  <style>

  4.    h1 {

  5.      color: green;

  6.    }

  7.  </style>

  8. `;


Maka element <style> tersebut akan berada di dalam shadow tree dan akan berdampak pada elemen yang ada di dalamnya.
20200310205206823dcc0fb3c6ef38cb5e0a7563f0590d.png

Shadow DOM in Web Components

Untuk membantu menerapkan enkapsulasi pada custom element, Shadow DOM berperan sebagai salah satu API standar yang digunakan dalam membuat Web Component (Hal ini distandarisasi oleh W3C). Kita sudah belajar bagaimana menerapkan Shadow DOM pada elemen yang berada pada Document Tree, namun bagaimana caranya bila itu diterapkan pada custom element?
Jawabannya cukup mudah! Mari kita lihat kembali contoh custom element yang pernah kita buat pada materi Styling Custom Element without Shadow DOM

  1. <!DOCTYPE html>

  2. <html>

  3.  <head>

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

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

  6.    <title>Styling without Shadow DOM</title>

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

  8.  </head>

  9. <body>

  10.    <image-figure

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

  12.      alt="Dicoding Logo"

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

  14.      </image-figure>

  15.  

  16.  

  17.      <!-- Styling di dalam image-figure akan mempengaruhi juga elemen di luarnya -->

  18.      <figure>

  19.        <img src="https://i.imgur.com/iJq78XH.jpg"

  20.        alt="Dicoding logo"/>

  21.        <figcaption>Huruf g dalam logo Dicoding</figcaption>

  22.      </figure>

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

  24.  </body>

  25. </html>



  1. class ImageFigure extends HTMLElement {

  2.  

  3.  

  4.  connectedCallback() {

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

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

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

  8.    this.render();

  9.  }

  10.  

  11.  

  12.  render() {

  13.    this.innerHTML = `

  14.      <style>

  15.        figure {

  16.          border: thin #c0c0c0 solid;

  17.          display: flex;

  18.          flex-flow: column;

  19.          padding: 5px;

  20.          max-width: 220px;

  21.          margin: auto;

  22.        }

  23.  

  24.  

  25.        figure > img {

  26.          max-width: 220px;

  27.        }

  28.  

  29.  

  30.        figure > figcaption {

  31.          background-color: #222;

  32.          color: #fff;

  33.          font: italic smaller sans-serif;

  34.          padding: 3px;

  35.          text-align: center;

  36.        }

  37.      </style>

  38.  

  39.  

  40.      <figure>

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

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

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

  44.      </figure>

  45.    `;

  46.  }

  47.  

  48.  

  49.  attributeChangedCallback(name, oldValue, newValue) {

  50.    this[name] = newValue;

  51.    this.render();

  52.  }

  53.  

  54.  

  55.  static get observedAttributes() {

  56.    return ["caption"];

  57.  }

  58. }

  59.  

  60.  

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


















Ketika kode tersebut dijalankan pada browser, kita bisa melihat terdapat dua komponen <figure> yang ditampilkan, salah satunya adalah custom element.
20200310205631fa13fdb32383c4c52b3566d37527bf5e.png
Kita bisa melihat juga bahwa keduanya memiliki styling yang sama, padahal kita hanya menetapkan styling di dalam komponen ImageFigure saja. 
Yup, hal tersebut wajar terjadi karena pada custom element kita tidak menetapkan Shadow DOM sehingga styling pada custom element akan berdampak juga terhadap komponen di luarnya.
Dalam melampirkan Shadow DOM pada custom element sama seperti pada elemen biasanya, yaitu menggunakan attachShadow
Namun dalam custom element, kita lakukan pada constructor class-nya seperti ini:

  1. class ImageFigure extends HTMLElement {

  2.  

  3.  constructor() {

  4.    super();

  5.    this._shadowRoot = this.attachShadow({mode: "open"});

  6.  }

  7.  

  8.   .....

  9. }


Agar nilai shadowRoot dapat diakses pada fungsi mana saja di class, maka kita perlu memasukkan nilai shadowRoot pada properti class menggunakan this
Kita bebas menentukan nama properti sesuai keinginan, namun untuk memudahkan kita gunakan nama _shadowRoot. Lalu mengapa penamaannya menggunakan tanda underscore (_) di depannya? Jawabannya, this pada konteks class ini merupakan HTMLElement dan ia sudah memiliki properti dengan nama shadowRoot
Untuk membedakan properti _shadowRoot asli dengan properti baru yang kita buat, kita bisa tambahkan underscore di awal penamaannya. 
Hal ini dibutuhkan karena jika kita menerapkan mode closed pada Shadow DOM, nilai properti shadowRoot akan mengembalikan null, sehingga tidak ada cara lain untuk kita mengakses Shadow Tree.
Setelah menerapkan Shadow DOM pada constructor, ketika ingin mengakses apapun yang merupakan properti dari DOM kita harus melalui _shadowRoot
Contohnya ketika ingin menerapkan template HTML, kita tidak bisa menggunakan langsung this.innerHTML, namun perlu melalui this._shadowRoot.innerHTML.
Sehingga kita perlu menyesuaikan kembali beberapa kode yang terdapat pada fungsi render menjadi seperti ini:
  1. render() {
  2.    this._shadowRoot.innerHTML = `
  3.      <style>
  4.        figure {
  5.          border: thin #c0c0c0 solid;
  6.          display: flex;
  7.          flex-flow: column;
  8.          padding: 5px;
  9.          max-width: 220px;
  10.          margin: auto;
  11.        }
  12.  
  13.        figure > img {
  14.          max-width: 220px;
  15.        }
  16.  
  17.        figure > figcaption {
  18.          background-color: #222;
  19.          color: #fff;
  20.          font: italic smaller sans-serif;
  21.          padding: 3px;
  22.          text-align: center;
  23.        }
  24.      </style>
  25.  
  26.      <figure>
  27.        <img src="${this.src}"
  28.            alt="${this.alt}">
  29.        <figcaption>${this.caption}</figcaption>
  30.      </figure>
  31.    `;
  32. }

Dengan begitu sekarang styling pada komponen hanya berlaku pada komponen itu sendiri. Begitu juga sebaliknya, styling yang dituliskan di luar dari komponen tidak akan berdampak pada elemen di dalam komponen.
20200310210356544578a1669328f878cb86f6e1ff1d35.png

Solution: Menerapkan Shadow DOM pada Proyek Club Finder

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

Menerapkan Shadow DOM pada App Bar

Kita mulai dari <app-bar> component yuk. Pertama kita buka dulu proyek club finder dengan text editor yang kita gunakan.
20200313095904249af3ba1d687f1216c8dc44d5fa11fc.png
Kemudian buka berkas script -> component -> app-bar.js, buat constructor dari class tersebut dan di dalamnya kita tetapkan shadow root seperti ini:
  1. class AppBar extends HTMLElement {
  2.  
  3.    constructor() {
  4.        super();
  5.        this.shadowDOM = this.attachShadow({mode: "open"});
  6.    }
  7.  
  8.    connectedCallback(){
  9.        this.render();
  10.    }
  11.  
  12.    render() {
  13.        this.innerHTML = `<h2>Club Finder</h2>`;
  14.    }
  15. }
  16.  
  17. 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.

  1. class AppBar extends HTMLElement {

  2.  

  3.    constructor() {

  4.        super();

  5.        this.shadowDOM = this.attachShadow({mode: "open"});

  6.    }

  7.  

  8.    connectedCallback(){

  9.        this.render();

  10.    }

  11.  

  12.    render() {

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

  14.    }

  15. }

  16.  

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


Kemudian buka berkas style -> appbar.css dan pindahkan (cut) seluruh kode yang ada pada berkas tersebut.

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



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:
  1. class AppBar extends HTMLElement {
  2.  
  3.    constructor() {
  4.        super();
  5.        this.shadowDOM = this.attachShadow({mode: "open"});
  6.    }
  7.  
  8.    connectedCallback(){
  9.        this.render();
  10.    }
  11.  
  12.    render() {
  13.        this.shadowDOM.innerHTML = `
  14.        <style>
  15.            app-bar {
  16.                display: block;
  17.                padding: 16px;
  18.                width: 100%;
  19.                background-color: cornflowerblue;
  20.                color: white;
  21.                box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  22.            }
  23.        </style>
  24.        <h2>Club Finder</h2>`;
  25.    }
  26. }
  27.  
  28. customElements.define("app-bar", AppBar);

Coba kita simpan perubahan yang diterapkan kemudian lihat perubahannya pada browser.

202003131025191a8dcdd75e697b30cba119d06defa3e1.png
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:
  1. class AppBar extends HTMLElement {
  2.  
  3.    constructor() {
  4.        super();
  5.        this.shadowDOM = this.attachShadow({mode: "open"});
  6.    }
  7.  
  8.    connectedCallback(){
  9.        this.render();
  10.    }
  11.  
  12.    render() {
  13.        this.shadowDOM.innerHTML = `
  14.        <style>
  15.            * {
  16.                margin: 0;
  17.                padding: 0;
  18.                box-sizing: border-box;
  19.            }
  20.            :host {
  21.                display: block;
  22.                width: 100%;
  23.                background-color: cornflowerblue;
  24.                color: white;
  25.                box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  26.            }
  27.            h2 {
  28.                padding: 16px;
  29.            }
  30.        </style>
  31.        <h2>Club Finder</h2>`;
  32.    }
  33. }
  34.  
  35. customElements.define("app-bar", AppBar);

Pada perubahan styling tersebut kita menambahkan

  1. * {

  2.     margin: 0;

  3.     padding: 0;

  4.     box-sizing: border-box;

  5. }


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.
20200313102750873f1782c7d560202cb13b08a7eb95ea.png
Karena kita sudah tidak membutuhkan lagi berkas src -> styles -> appbar.css, kita dapat menghapus berkas tersebut. 
20200313102817f716d852a60606d8028fd6f20b702380.png
Jangan lupa untuk menghapus import css tersebut pada src -> styles -> style.css.

  1. @import "clublist.css";

  2. @import "searchbar.css";

  3.  

  4. * {

  5.    padding: 0;

  6.    margin: 0;

  7.    box-sizing: border-box;

  8. }

  9.  

  10. body {

  11.    font-family: sans-serif;

  12. }

  13.  

  14. main {

  15.    width: 90%;

  16.    max-width: 800px;

  17.    margin: 32px auto;

  18. }



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.
  1. class SearchBar extends HTMLElement {
  2.  
  3.    constructor() {
  4.        super();
  5.        this.shadowDOM = this.attachShadow({mode: "open"});
  6.    }
  7.  
  8.    connectedCallback(){
  9.        this.render();
  10.    }
  11.   
  12.    set clickEvent(event) {
  13.        this._clickEvent = event;
  14.        this.render();
  15.    }
  16.  
  17.    get value() {
  18.        return this.querySelector("#searchElement").value;
  19.    }
  20.  
  21.    render() {
  22.        this.innerHTML = `
  23.        <div id="search-container" class="search-container">
  24.            <input placeholder="Search football club" id="searchElement" type="search">
  25.            <button id="searchButtonElement" type="submit">Search</button>
  26.        </div>
  27.        `;
  28.  
  29.        this.querySelector("#searchButtonElement").addEventListener("click", this._clickEvent);
  30.    }
  31. }
  32.  
  33. 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().
  1. class SearchBar extends HTMLElement {
  2.  
  3.    constructor() {
  4.        super();
  5.        this.shadowDOM = this.attachShadow({mode: "open"});
  6.    }
  7.  
  8.    connectedCallback(){
  9.        this.render();
  10.    }
  11.   
  12.    set clickEvent(event) {
  13.        this._clickEvent = event;
  14.        this.render();
  15.    }
  16.  
  17.    get value() {
  18.        return this.querySelector("#searchElement").value;
  19.    }
  20.  
  21.    render() {
  22.        this.shadowDOM.innerHTML = `
  23.        <div id="search-container" class="search-container">
  24.            <input placeholder="Search football club" id="searchElement" type="search">
  25.            <button id="searchButtonElement" type="submit">Search</button>
  26.        </div>
  27.        `;
  28.  
  29.        this.querySelector("#searchButtonElement").addEventListener("click", this._clickEvent);
  30.    }
  31. }
  32.  
  33. customElements.define("search-bar", SearchBar);

Selain itu juga kita ubah pemanggilan this.querySelector menjadi this.shadowDOM.querySelector pada fungsi render() dan get value().
  1. class SearchBar extends HTMLElement {
  2.  
  3.  ..........
  4.  
  5.    get value() {
  6.        return this.shadowDOM.querySelector("#searchElement").value;
  7.    }
  8.  
  9.    render() {
  10.       .........
  11.        this.shadowDOM.querySelector("#searchButtonElement").addEventListener("click", this._clickEvent);
  12.    }
  13. }
  14.  
  15.  
  16. ...........

Kemudian buka berkas src -> styles -> searchbar.css, pindahkan (cut) seluruh kode yang terdapat pada berkas tersebut.
  1. .search-container {
  2.    max-width: 800px;
  3.    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  4.    padding: 16px;
  5.    border-radius: 5px;
  6.    display: flex;
  7.    position: sticky;
  8.    top: 10px;
  9.    background-color: white;
  10. }
  11.  
  12. .search-container > input {
  13.    width: 75%;
  14.    padding: 16px;
  15.    border: 0;
  16.    border-bottom: 1px solid cornflowerblue;
  17.    font-weight: bold;
  18. }
  19.  
  20. .search-container > input:focus {
  21.    outline: 0;
  22.    border-bottom: 2px solid cornflowerblue;
  23. }
  24.  
  25. .search-container > input:focus::placeholder {
  26.    font-weight: bold;
  27. }
  28.  
  29. .search-container >  input::placeholder {
  30.    color: cornflowerblue;
  31.    font-weight: normal;
  32. }
  33.  
  34. .search-container > button {
  35.    width: 23%;
  36.    cursor: pointer;
  37.    margin-left: auto;
  38.    padding: 16px;
  39.    background-color: cornflowerblue;
  40.    color: white;
  41.    border: 0;
  42.    text-transform: uppercase;
  43. }
  44.  
  45. @media screen and (max-width: 550px){
  46.    .search-container {
  47.        flex-direction: column;
  48.        position: static;
  49.    }
  50.  
  51.    .search-container > input {
  52.        width: 100%;
  53.        margin-bottom: 12px;
  54.    }
  55.  
  56.    .search-container > button {
  57.        width: 100%;
  58.    }
  59. }

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:
  1. class SearchBar extends HTMLElement {
  2.  
  3. .........
  4.  
  5. render() {
  6. this.shadowDOM.innerHTML = `
  7. <style>
  8. .search-container {
  9. max-width: 800px;
  10. box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  11. padding: 16px;
  12. border-radius: 5px;
  13. display: flex;
  14. position: sticky;
  15. top: 10px;
  16. background-color: white;
  17. }
  18. .search-container > input {
  19. width: 75%;
  20. padding: 16px;
  21. border: 0;
  22. border-bottom: 1px solid cornflowerblue;
  23. font-weight: bold;
  24. }
  25. .search-container > input:focus {
  26. outline: 0;
  27. border-bottom: 2px solid cornflowerblue;
  28. }
  29. .search-container > input:focus::placeholder {
  30. font-weight: bold;
  31. }
  32. .search-container > input::placeholder {
  33. color: cornflowerblue;
  34. font-weight: normal;
  35. }
  36. .search-container > button {
  37. width: 23%;
  38. cursor: pointer;
  39. margin-left: auto;
  40. padding: 16px;
  41. background-color: cornflowerblue;
  42. color: white;
  43. border: 0;
  44. text-transform: uppercase;
  45. }
  46. @media screen and (max-width: 550px){
  47. .search-container {
  48. flex-direction: column;
  49. position: static;
  50. }
  51. .search-container > input {
  52. width: 100%;
  53. margin-bottom: 12px;
  54. }
  55. .search-container > button {
  56. width: 100%;
  57. }
  58. }
  59. </style>
  60. <div id="search-container" class="search-container">
  61. <input placeholder="Search football club" id="searchElement" type="search">
  62. <button id="searchButtonElement" type="submit">Search</button>
  63. </div>
  64. `;
  65. .......
  66. }
  67. }
  68.  
  69. customElements.define("search-bar", SearchBar);

Simpan perubahan yang dilakukan kemudian lihat hasilnya pada browser.
202003131034259aa04d43b00877a7efd968c647ba2ae0.png
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. 
20200313103452d010492337b319b09b72b75402e29540.png
Jangan lupa untuk menghapus import css tersebut pada src -> styles -> style.css.

  1. @import "clublist.css";

  2.  

  3. * {

  4.    padding: 0;

  5.    margin: 0;

  6.    box-sizing: border-box;

  7. }

  8.  

  9. body {

  10.    font-family: sans-serif;

  11. }

  12.  

  13. main {

  14.    width: 90%;

  15.    max-width: 800px;

  16.    margin: 32px auto;

  17. }



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.

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

  2.  



  1. import './club-item.js';
  2.  
  3. class ClubList extends HTMLElement {
  4.  
  5.    constructor() {
  6.        super();
  7.        this.shadowDOM = this.attachShadow({mode: "open"});
  8.    }
  9.  
  10.    set clubs(clubs) {
  11.        this._clubs = clubs;
  12.        this.render();
  13.    }
  14.  
  15.    renderError(message) {
  16.        this.innerHTML = "";
  17.        this.innerHTML += `<h2 class="placeholder">${message}</h2>`;
  18.    }
  19.  
  20.    render() {
  21.        this.innerHTML = "";
  22.        this._clubs.forEach(club => {
  23.            const clubItemElement = document.createElement("club-item");
  24.            clubItemElement.club = club
  25.            this.appendChild(clubItemElement);
  26.        })
  27.    }
  28. }
  29.  
  30. customElements.define("club-list", ClubList);

Kemudian ubah seluruh kode this.innerHTML menjadi this.shadowDOM.innerHTML dan this.appendChild menjadi this.shadowDOM.appendChild.
  1. import './club-item.js';
  2.  
  3. class ClubList extends HTMLElement {
  4.  
  5.    constructor() {
  6.        super();
  7.        this.shadowDOM = this.attachShadow({mode: "open"});
  8.    }
  9.  
  10.    set clubs(clubs) {
  11.        this._clubs = clubs;
  12.        this.render();
  13.    }
  14.  
  15.    renderError(message) {
  16.        this.shadowDOM.innerHTML = "";
  17.        this.shadowDOM.innerHTML += `<h2 class="placeholder">${message}</h2>`;
  18.    }
  19.  
  20.    render() {
  21.        this.shadowDOM.innerHTML = "";
  22.        this._clubs.forEach(club => {
  23.            const clubItemElement = document.createElement("club-item");
  24.            clubItemElement.club = club
  25.            this.shadowDOM.appendChild(clubItemElement);
  26.        })
  27.    }
  28. }
  29.  
  30. customElements.define("club-list", ClubList);

Kemudian buka berkas src -> styles -> clublist.css dan pindahkan (cut) kode styling dengan selector club-list > .placeholder

  1. club-list > .placeholder {

  2.    font-weight: lighter;

  3.    color: rgba(0,0,0,0.5);

  4.    -webkit-user-select: none;

  5.    -moz-user-select: none;

  6.    -ms-user-select: none;

  7.    user-select: none;

  8. }


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:
  1. import './club-item.js';
  2.  
  3. class ClubList extends HTMLElement {
  4.  
  5.  .........
  6.  
  7.    renderError(message) {
  8.        this.shadowDOM.innerHTML = `
  9.        <style>
  10.            club-list > .placeholder {
  11.                font-weight: lighter;
  12.                color: rgba(0,0,0,0.5);
  13.                -webkit-user-select: none;
  14.                -moz-user-select: none;
  15.                -ms-user-select: none;
  16.                user-select: none;
  17.            }
  18.        </style>
  19.        `;
  20.        this.shadowDOM.innerHTML += `<h2 class="placeholder">${message}</h2>`;
  21.    }
  22.  
  23.  .......
  24. }
  25.  
  26. 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:
  1. import './club-item.js';
  2.  
  3. class ClubList extends HTMLElement {
  4.  
  5.    constructor() {
  6.        super();
  7.        this.shadowDOM = this.attachShadow({mode: "open"});
  8.    }
  9.  
  10.    set clubs(clubs) {
  11.        this._clubs = clubs;
  12.        this.render();
  13.    }
  14.  
  15.    renderError(message) {
  16.        this.shadowDOM.innerHTML = `
  17.        <style>
  18.            .placeholder {
  19.                font-weight: lighter;
  20.                color: rgba(0,0,0,0.5);
  21.                -webkit-user-select: none;
  22.                -moz-user-select: none;
  23.                -ms-user-select: none;
  24.                user-select: none;
  25.            }
  26.        </style>
  27.        `;
  28.        this.shadowDOM.innerHTML += `<h2 class="placeholder">${message}</h2>`;
  29.    }
  30.  
  31.    render() {
  32.        this.shadowDOM.innerHTML = "" ;
  33.        this._clubs.forEach(club => {
  34.            const clubItemElement = document.createElement("club-item");
  35.            clubItemElement.club = club
  36.            this.shadowDOM.appendChild(clubItemElement);
  37.        })
  38.    }
  39. }
  40.  
  41. customElements.define("club-list", ClubList);

Simpan perubahan tersebut dan lihat hasilnya pada browser, tampilan dari daftar club akan sangat berantakan.
202003131040024f3e1630a04c7004c7c638df8dbe006e.png
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.
  1. class ClubItem extends HTMLElement {
  2.  
  3.    constructor() {
  4.        super();
  5.        this.shadowDOM = this.attachShadow({mode: "open"});
  6.    }
  7.  
  8.    set club(club) {
  9.        this._club = club;
  10.        this.render();
  11.    }
  12.  
  13.    render() {
  14.        this.innerHTML = `
  15.            <img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">
  16.            <div class="club-info">
  17.                <h2>${this._club.name}</h2>
  18.                <p>${this._club.description}</p>
  19.            </div>`;
  20.    }
  21. }
  22.  
  23. customElements.define("club-item", ClubItem);

Seperti biasa jangan lupa untuk mengubah this.innerHTML menjadi this.shadowDOM.innerHTML ya.
  1. class ClubItem extends HTMLElement {
  2.  
  3.    constructor() {
  4.        super();
  5.        this.shadowDOM = this.attachShadow({mode: "open"});
  6.    }
  7.  
  8.    set club(club) {
  9.        this._club = club;
  10.        this.render();
  11.    }
  12.  
  13.    render() {
  14.        this.shadowDOM.innerHTML = `
  15.            <img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">
  16.            <div class="club-info">
  17.                <h2>${this._club.name}</h2>
  18.                <p>${this._club.description}</p>
  19.            </div>`;
  20.    }
  21. }
  22.  
  23. customElements.define("club-item", ClubItem);

Selanjutnya buka kembali berkas src -> styles -> clublist.css dan pindahkan styling berikut:
  1. club-item {
  2.    display: block;
  3.    margin-bottom: 18px;
  4.    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  5.    border-radius: 10px;
  6.    overflow: hidden;
  7. }
  8.  
  9. club-item .fan-art-club {
  10.    width: 100%;
  11.    max-height: 300px;
  12.    object-fit: cover;
  13.    object-position: center;
  14. }
  15.  
  16. .club-info {
  17.    padding: 24px;
  18. }
  19.  
  20. .club-info > h2 {
  21.    font-weight: lighter;
  22. }
  23.  
  24. .club-info > p {
  25.    margin-top: 10px;
  26.    overflow: hidden;
  27.    text-overflow: ellipsis;
  28.    display: -webkit-box;
  29.    -webkit-box-orient: vertical;
  30.    -webkit-line-clamp: 10; /* number of lines to show */
  31. }

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:
  1. class ClubItem extends HTMLElement {
  2.  
  3.  .......
  4.  
  5.    render() {
  6.        this.shadowDOM.innerHTML = `
  7.            <style>
  8.                club-item {
  9.                    display: block;
  10.                    margin-bottom: 18px;
  11.                    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  12.                    border-radius: 10px;
  13.                    overflow: hidden;
  14.                }
  15.               
  16.                club-item .fan-art-club {
  17.                    width: 100%;
  18.                    max-height: 300px;
  19.                    object-fit: cover;
  20.                    object-position: center;
  21.                }
  22.               
  23.                .club-info {
  24.                    padding: 24px;
  25.                }
  26.               
  27.                .club-info > h2 {
  28.                    font-weight: lighter;
  29.                }
  30.               
  31.                .club-info > p {
  32.                    margin-top: 10px;
  33.                    overflow: hidden;
  34.                    text-overflow: ellipsis;
  35.                    display: -webkit-box;
  36.                    -webkit-box-orient: vertical;
  37.                    -webkit-line-clamp: 10; /* number of lines to show */
  38.                }
  39.            </style>
  40.            <img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">
  41.            <div class="club-info">
  42.                <h2>${this._club.name}</h2>
  43.                <p>${this._club.description}</p>
  44.            </div>`;
  45.    }
  46. }
  47.  
  48. ......

Sesuaikan kembali selector pada styling tersebut menjadi seperti ini:
  1. class ClubItem extends HTMLElement {
  2.  
  3.   .....
  4.  
  5.    render() {
  6.        this.shadowDOM.innerHTML = `
  7.            <style>
  8.                * {
  9.                    margin: 0;
  10.                    padding: 0;
  11.                    box-sizing: border-box;
  12.                }
  13.                :host {
  14.                    display: block;
  15.                    margin-bottom: 18px;
  16.                    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  17.                    border-radius: 10px;
  18.                    overflow: hidden;
  19.                }
  20.               
  21.                .fan-art-club {
  22.                    width: 100%;
  23.                    max-height: 300px;
  24.                    object-fit: cover;
  25.                    object-position: center;
  26.                }
  27.               
  28.                .club-info {
  29.                    padding: 24px;
  30.                }
  31.               
  32.                .club-info > h2 {
  33.                    font-weight: lighter;
  34.                }
  35.               
  36.                .club-info > p {
  37.                    margin-top: 10px;
  38.                    overflow: hidden;
  39.                    text-overflow: ellipsis;
  40.                    display: -webkit-box;
  41.                    -webkit-box-orient: vertical;
  42.                    -webkit-line-clamp: 10; /* number of lines to show */
  43.                }
  44.            </style>
  45.            <img class="fan-art-club" src="${this._club.fanArt}" alt="Fan Art">
  46.            <div class="club-info">
  47.                <h2>${this._club.name}</h2>
  48.                <p>${this._club.description}</p>
  49.            </div>`;
  50.    }
  51. }
  52.  
  53. ........

Simpan perubahan tersebut dan lihat pada browser, seharusnya tampilan daftar tim sudah kembali normal.
202003131046287ce90b85747ab537976360b93f4a2da2.png
Oh ya, sebelum beranjak kita buka kembali berkas src -> styles -> clublist.css. Di sana masih terdapat satu rule styling berikut:

  1. club-list {

  2.    display: block;

  3.    margin-top: 32px;

  4.    width: 100%;

  5.    padding: 16px;

  6. }


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.
  1. @import "clublist.css";
  2.  
  3. * {
  4.    padding: 0;
  5.    margin: 0;
  6.    box-sizing: border-box;
  7. }
  8.  
  9. body {
  10.    font-family: sans-serif;
  11. }
  12.  
  13. main {
  14.    width: 90%;
  15.    max-width: 800px;
  16.    margin: 32px auto;
  17. }
  18.  
  19. club-list {
  20.    display: block;
  21.    margin-top: 32px;
  22.    width: 100%;
  23.    padding: 16px;
  24. }

Dengan begitu kita dapat leluasa menghapus berkas clublist.css dan menghapus @import pada berkas style.css.
20200313104746dc2f4116f225f458ab6541ca396d2223.png
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