WEB-COMPONENTS FRONTEND WEB-DEVELOPMENT HTML JAVASCRIPT CUSTOM-ELEMENTS SHADOW-DOM HTML-TEMPLATES DESIGN-SYSTEMS MICRO-FRONTENDS REUSABILITY BROWSER FRAMEWORK-AGNOSTIC

Web Components: Membangun Komponen UI yang Reusable dan Framework-Agnostic

⏱️ 18 menit baca
👨‍💻

Web Components: Membangun Komponen UI yang Reusable dan Framework-Agnostic

Sebagai seorang web developer, kita sering kali dihadapkan pada tantangan untuk membangun antarmuka pengguna (UI) yang konsisten, mudah dikelola, dan yang terpenting, reusable. Di era modern ini, kita punya segudang pilihan framework seperti React, Vue, atau Angular. Mereka semua menawarkan cara hebat untuk membangun komponen. Tapi, bagaimana jika Anda perlu berbagi komponen UI antara dua proyek yang menggunakan framework berbeda? Atau bahkan dengan proyek yang tidak menggunakan framework sama sekali?

Di sinilah Web Components datang sebagai pahlawan.

Web Components adalah seperangkat standar web native yang memungkinkan Anda membuat elemen HTML kustom yang sepenuhnya terenkapsulasi dan dapat digunakan kembali. Bayangkan Anda bisa membuat tag HTML Anda sendiri, seperti <my-custom-button> atau <user-profile-card>, yang bekerja di browser mana pun, tanpa perlu framework JavaScript tambahan. Kedengarannya menarik, bukan?

Artikel ini akan membawa Anda menyelami dunia Web Components, mengapa ini penting, dan bagaimana Anda bisa mulai membangun komponen UI yang benar-benar independen dan siap pakai.

1. Pendahuluan: Mengapa Web Components Penting?

Seiring berkembangnya aplikasi web, kompleksitas UI juga meningkat. Kita sering melihat pola-pola seperti:

Framework seperti React, Vue, atau Angular memang sangat membantu dalam memecah UI menjadi komponen-komponen kecil. Namun, komponen yang Anda buat di React, secara default, tidak bisa langsung Anda pakai di proyek Vue, dan sebaliknya. Ini menciptakan vendor lock-in pada framework dan menghambat reusability sejati.

📌 Web Components hadir untuk mengatasi masalah ini. Mereka adalah standar browser, bukan pustaka atau framework pihak ketiga. Ini berarti:

  1. Framework-Agnostic: Komponen Anda bisa digunakan di proyek React, Vue, Angular, Svelte, jQuery, atau bahkan Vanilla JavaScript.
  2. Reusability Maksimal: Buat sekali, gunakan di mana saja. Ini ideal untuk Design Systems atau widget yang perlu disematkan di berbagai situs.
  3. Enkapsulasi Kuat: CSS dan DOM komponen Anda terisolasi dari sisa halaman, mencegah konflik gaya dan perilaku.
  4. Standar Web: Didukung secara native oleh browser modern, mengurangi ketergantungan pada polyfill dan pustaka eksternal.

Mari kita selami tiga pilar utama yang membentuk Web Components.

2. Tiga Pilar Utama Web Components

Web Components dibangun di atas tiga spesifikasi utama yang bekerja sama untuk menciptakan elemen kustom yang kuat:

2.1. Custom Elements

Ini adalah fondasi yang memungkinkan Anda mendefinisikan elemen HTML baru dengan tag kustom Anda sendiri. Anda bisa memberi nama elemen Anda (<my-button>, <user-card>) dan menentukan perilakunya menggunakan JavaScript.

💡 Konsep Kunci:

2.2. Shadow DOM

Shadow DOM adalah fitur yang menyediakan enkapsulasi untuk struktur DOM dan gaya dari komponen Anda. Ini menciptakan “sub-tree” DOM yang terpisah dari DOM utama dokumen, dan gaya yang didefinisikan di dalamnya tidak akan bocor keluar, begitu juga gaya dari luar tidak akan memengaruhi bagian dalam Shadow DOM.

✅ Manfaat Shadow DOM:

2.3. HTML Templates (<template> dan <slot>)

Elemen <template> memungkinkan Anda mendefinisikan markup HTML yang tidak langsung dirender saat halaman dimuat. Markup ini bisa Anda kloning dan gunakan berulang kali di dalam Custom Element Anda.

Elemen <slot> adalah placeholder di dalam <template> Anda yang memungkinkan Anda menyuntikkan konten dari luar ke dalam Shadow DOM komponen Anda. Ini sangat penting untuk membuat komponen yang fleksibel dan dapat dikustomisasi.

🎯 Bayangkan ini:

3. Membangun Web Component Pertama Anda: <my-simple-card>

Mari kita buat komponen kartu sederhana yang menampilkan judul dan deskripsi.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web Components Demo</title>
    <script src="my-simple-card.js" defer></script>
    <style>
      body {
        font-family: sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        min-height: 100vh;
        background-color: #f0f2f5;
      }
    </style>
  </head>
  <body>
    <my-simple-card
      title="Halo Dunia!"
      description="Ini adalah Web Component pertama saya. Sangat mudah dipelajari dan digunakan."
    ></my-simple-card>

    <my-simple-card
      title="Komponen Kedua"
      description="Lihat, saya menggunakan komponen yang sama lagi! Ini kekuatan reusability."
    ></my-simple-card>
  </body>
</html>
// my-simple-card.js
class MySimpleCard extends HTMLElement {
  constructor() {
    super(); // Panggil constructor HTMLElement

    // Buat Shadow DOM untuk enkapsulasi
    this.attachShadow({ mode: "open" }); // 'open' agar bisa diakses dari luar jika perlu, 'closed' untuk enkapsulasi total

    // Definisi template HTML untuk komponen
    const template = document.createElement("template");
    template.innerHTML = `
            <style>
                .card {
                    background-color: white;
                    border-radius: 8px;
                    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
                    padding: 20px;
                    margin: 15px;
                    max-width: 300px;
                    text-align: center;
                }
                h3 {
                    color: #333;
                    margin-top: 0;
                }
                p {
                    color: #666;
                    font-size: 0.9em;
                }
            </style>
            <div class="card">
                <h3 id="card-title"></h3>
                <p id="card-description"></p>
            </div>
        `;

    // Kloning konten template dan tambahkan ke Shadow DOM
    this.shadowRoot.appendChild(template.content.cloneNode(true));

    // Ambil elemen di dalam Shadow DOM untuk diperbarui
    this._titleElement = this.shadowRoot.getElementById("card-title");
    this._descriptionElement =
      this.shadowRoot.getElementById("card-description");
  }

  // Lifecycle Callback: Dipanggil ketika elemen ditambahkan ke DOM
  connectedCallback() {
    this._updateContent();
  }

  // Lifecycle Callback: Mengamati perubahan pada atribut tertentu
  static get observedAttributes() {
    return ["title", "description"];
  }

  // Lifecycle Callback: Dipanggil ketika atribut yang diamati berubah
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this._updateContent();
    }
  }

  _updateContent() {
    // Ambil nilai atribut 'title' dan 'description'
    const title = this.getAttribute("title") || "Judul Default";
    const description =
      this.getAttribute("description") || "Deskripsi default untuk kartu ini.";

    // Perbarui konten elemen di Shadow DOM
    if (this._titleElement) {
      this._titleElement.textContent = title;
    }
    if (this._descriptionElement) {
      this._descriptionElement.textContent = description;
    }
  }
}

// Daftarkan Custom Element ke browser
customElements.define("my-simple-card", MySimpleCard);

Penjelasan Kode:

  1. class MySimpleCard extends HTMLElement: Ini adalah cara kita mendefinisikan Custom Element. Kita memperluas HTMLElement bawaan browser.
  2. constructor(): Dipanggil saat instance elemen dibuat.
    • super(): Wajib dipanggil di awal constructor untuk memanggil constructor kelas induk.
    • this.attachShadow({ mode: 'open' }): Membuat Shadow DOM. Mode 'open' berarti Shadow DOM bisa diakses dari JavaScript eksternal (misalnya element.shadowRoot), sementara 'closed' akan menyembunyikannya sepenuhnya.
    • Template dan Styling: Kita membuat <template> secara dinamis (atau bisa juga didefinisikan langsung di HTML) yang berisi struktur HTML dan CSS untuk komponen kita. CSS di dalam <style> di Shadow DOM bersifat scoped dan tidak akan memengaruhi elemen di luar Shadow DOM.
    • this.shadowRoot.appendChild(template.content.cloneNode(true)): Mengkloning konten template dan menyisipkannya ke dalam Shadow DOM. cloneNode(true) memastikan semua child nodes juga ikut dikloning.
  3. connectedCallback(): Ini adalah lifecycle callback yang dipanggil ketika elemen ditambahkan ke DOM dokumen. Ini adalah tempat yang baik untuk melakukan setup awal atau fetching data.
  4. static get observedAttributes(): Ini adalah static getter yang mengembalikan array nama atribut yang ingin kita “amati” perubahannya.
  5. attributeChangedCallback(name, oldValue, newValue): Dipanggil setiap kali salah satu atribut yang terdaftar di observedAttributes berubah. Kita menggunakan ini untuk memperbarui konten kartu saat atribut title atau description berubah.
  6. _updateContent(): Fungsi helper untuk memperbarui teks di dalam kartu berdasarkan atribut yang diberikan.
  7. customElements.define('my-simple-card', MySimpleCard): Ini adalah langkah krusial. Kita mendaftarkan Custom Element kita ke browser, menghubungkan nama tag HTML kustom (my-simple-card) dengan kelas JavaScript yang kita definisikan. Nama tag kustom harus selalu mengandung tanda hubung (-) untuk menghindari konflik dengan elemen HTML bawaan di masa depan.

Sekarang, Anda bisa menggunakan <my-simple-card> di mana saja di HTML Anda, dan browser akan tahu cara merendernya!

4. Styling dan Fleksibilitas dengan <slot> dan CSS Custom Properties

Komponen yang benar-benar reusable harus fleksibel. Bagaimana jika kita ingin menambahkan ikon atau footer kustom ke dalam kartu kita? Di sinilah <slot> berperan.

Mari kita modifikasi my-simple-card untuk memiliki slot untuk header dan footer.

<!-- index.html (penggunaan dengan slot) -->
<my-simple-card title="Kartu dengan Slot">
  <div slot="header">
    <!-- Konten ini akan masuk ke slot 'header' -->
    <span style="font-size: 1.5em; margin-right: 10px;">✨</span> Judul Kustom
  </div>
  <p>Ini adalah konten utama yang disisipkan ke slot default (tanpa nama).</p>
  <div slot="footer">
    <!-- Konten ini akan masuk ke slot 'footer' -->
    <button>Aksi!</button>
  </div>
</my-simple-card>
// my-simple-card.js (dengan slot)
// ... (bagian atas sama)
class MySimpleCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    const template = document.createElement("template");
    template.innerHTML = `
            <style>
                .card {
                    background-color: var(--card-bg, white); /* Menggunakan CSS Custom Property */
                    border-radius: 8px;
                    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
                    padding: 20px;
                    margin: 15px;
                    max-width: 300px;
                    text-align: center;
                    border: 1px solid var(--card-border-color, #eee);
                }
                h3 {
                    color: var(--card-title-color, #333);
                    margin-top: 0;
                }
                p {
                    color: var(--card-description-color, #666);
                    font-size: 0.9em;
                }
                .card-header, .card-footer {
                    padding: 10px 0;
                    border-bottom: 1px solid #eee;
                    margin-bottom: 10px;
                }
                .card-footer {
                    border-top: 1px solid #eee;
                    border-bottom: none;
                    margin-top: 10px;
                }
                /* Gaya untuk konten yang disisipkan ke slot */
                ::slotted(p) {
                    font-style: italic;
                    color: darkblue;
                }
            </style>
            <div class="card">
                <div class="card-header">
                    <slot name="header"><h3>Judul Default</h3></slot>
                </div>
                <slot></slot> <!-- Slot default (tanpa nama) untuk konten utama -->
                <div class="card-footer">
                    <slot name="footer"></slot>
                </div>
            </div>
        `;
    this.shadowRoot.appendChild(template.content.cloneNode(true));

    // Tidak perlu lagi _titleElement dan _descriptionElement karena kita pakai slot
  }

  connectedCallback() {
    // Jika menggunakan slot, kita tidak perlu lagi memanipulasi teks secara langsung dari atribut
    // Konten akan disisipkan secara otomatis oleh browser ke slot yang sesuai
  }

  // ... (observedAttributes dan attributeChangedCallback bisa dihapus jika tidak ada atribut yang langsung memengaruhi teks)
  // Atau bisa tetap ada jika Anda ingin atribut seperti 'title' memengaruhi teks default slot 'header'
  // Untuk contoh ini, kita biarkan saja atribut title tetap ada, tapi tidak langsung mengisi H3.
  // Kita bisa mengambil nilai atribut 'title' untuk digunakan sebagai fallback jika slot header kosong.
  static get observedAttributes() {
    return ["title"];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === "title" && oldValue !== newValue) {
      // Contoh: Jika slot header kosong, kita bisa menampilkan judul dari atribut
      const headerSlot = this.shadowRoot.querySelector('slot[name="header"]');
      if (headerSlot && headerSlot.assignedNodes().length === 0) {
        // Ini hanya contoh sederhana, implementasi lebih kompleks mungkin perlu
        // Untuk mengisi fallback content dari atribut jika slot kosong.
        // Atau biarkan slot 'header' punya fallback langsung di template.
      }
    }
  }
}
customElements.define("my-simple-card", MySimpleCard);

Penjelasan Tambahan:

5. Kapan Menggunakan Web Components?

Web Components bukan pengganti framework JavaScript, melainkan pelengkap. Ada beberapa skenario di mana mereka bersinar terang:

Kapan mungkin bukan pilihan terbaik?

Kesimpulan

Web Components adalah standar web yang kuat dan sering diremehkan, menawarkan solusi elegan untuk reusability dan enkapsulasi komponen UI. Dengan Custom Elements, Shadow DOM, dan HTML Templates, Anda bisa membangun elemen HTML kustom yang berfungsi di mana saja, kapan saja, dan dengan framework apa saja.

Memahami Web Components adalah investasi berharga bagi setiap developer web. Ini memberi Anda fleksibilitas dan kemandirian yang tidak bisa ditawarkan oleh framework saja. Mulailah bereksperimen, bangun komponen kecil Anda sendiri, dan rasakan kekuatan standar web yang sesungguhnya!

🔗 Baca Juga