WEB-COMPONENTS FRONTEND JAVASCRIPT WEB-DEVELOPMENT UI-UX COMPONENT-DEVELOPMENT DESIGN-PATTERNS INTERACTIVITY STATE-MANAGEMENT CUSTOM-ELEMENTS

Komunikasi Antar Web Components: Membangun Interaksi yang Efisien dan Fleksibel

⏱️ 12 menit baca
👨‍💻

Komunikasi Antar Web Components: Membangun Interaksi yang Efisien dan Fleksibel

Selamat datang kembali di blog saya! Kali ini, kita akan menyelami salah satu aspek krusial dalam pengembangan aplikasi modern menggunakan Web Components: bagaimana mereka saling berkomunikasi. Web Components menawarkan modularitas dan reusabilitas yang luar biasa, namun tanpa mekanisme komunikasi yang efektif, komponen-komponen tersebut akan berdiri sendiri dan aplikasi Anda tidak akan bisa berfungsi secara kohesif.

1. Pendahuluan

Bayangkan Anda sedang membangun sebuah rumah. Anda punya berbagai modul pre-fabrikasi: dinding, jendela, pintu, atap. Setiap modul dirancang untuk melakukan tugasnya sendiri, namun agar rumah bisa berdiri kokoh dan berfungsi, semua modul harus bisa “berbicara” satu sama lain. Jendela perlu tahu kapan harus terbuka, pintu perlu tahu kapan harus terkunci, dan seterusnya.

Hal yang sama berlaku untuk Web Components. Komponen seperti <my-button>, <user-profile>, atau <data-table> mungkin terlihat independen, tapi dalam aplikasi nyata, mereka perlu berinteraksi. Tombol perlu memberi tahu parent-nya saat diklik, profil pengguna mungkin perlu memuat data berdasarkan ID yang diberikan, atau tabel data perlu mengirim event saat ada baris yang dipilih.

Tanpa strategi komunikasi yang jelas, proyek Web Components Anda bisa menjadi spaghetti code yang sulit dipelihara. Artikel ini akan membahas berbagai pola komunikasi antar Web Components, mulai dari yang paling dasar hingga yang lebih canggih, lengkap dengan contoh praktis dan kapan harus menggunakannya. Mari kita mulai! 🚀

2. Pola 1: Event Kustom (Custom Events)

📌 Konsep: Custom Events adalah cara standar browser untuk komunikasi dari anak ke orang tua (child-to-parent) atau antar komponen yang tidak terkait secara langsung (sibling-to-sibling melalui parent). Ini adalah mekanisme “memberi tahu” komponen lain tentang sesuatu yang terjadi.

💡 Cara Kerja: Anda membuat instance CustomEvent dan memicunya (dispatchEvent) dari dalam komponen. Komponen lain kemudian dapat “mendengarkan” event ini menggunakan addEventListener.

// my-button.js
class MyButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `<button><slot></slot></button>`;
    this.shadowRoot.querySelector('button').addEventListener('click', () => {
      // Memicu event kustom saat tombol diklik
      this.dispatchEvent(new CustomEvent('buttonClick', {
        bubbles: true, // Event akan 'menggelembung' ke atas DOM tree
        composed: true, // Event bisa melewati Shadow DOM boundary
        detail: {
          timestamp: new Date().toISOString(),
          message: 'Tombol saya diklik!'
        }
      }));
    });
  }
}
customElements.define('my-button', MyButton);

// app.js (komponen parent atau listener)
class MyApp extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <h1>Aplikasi Saya</h1>
      <my-button>Klik Saya</my-button>
      <p id="status"></p>
    `;

    // Mendengarkan event 'buttonClick' dari komponen anak
    this.shadowRoot.querySelector('my-button').addEventListener('buttonClick', (event) => {
      const statusElement = this.shadowRoot.querySelector('#status');
      statusElement.textContent = `Event diterima! ${event.detail.message} pada ${event.detail.timestamp}`;
      console.log('Event detail:', event.detail);
    });
  }
}
customElements.define('my-app', MyApp);

Kapan Digunakan:

Hal yang Perlu Diperhatikan:

3. Pola 2: Properti & Atribut (Properties & Attributes)

📌 Konsep: Ini adalah cara paling umum untuk komunikasi dari orang tua ke anak (parent-to-child). Komponen orang tua meneruskan data ke komponen anak melalui properti JavaScript atau atribut HTML.

💡 Cara Kerja:

// user-card.js
class UserCard extends HTMLElement {
  static get observedAttributes() {
    return ['user-name', 'email']; // Atribut yang akan diamati perubahannya
  }

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        div { border: 1px solid #ccc; padding: 10px; margin: 10px; }
        h3 { color: blue; }
      </style>
      <div>
        <h3></h3>
        <p></p>
      </div>
    `;
  }

  // Dipanggil saat atribut yang diamati berubah
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.updateContent();
    }
  }

  // Setter/Getter untuk properti JS
  set userName(value) {
    this.setAttribute('user-name', value);
  }

  get userName() {
    return this.getAttribute('user-name');
  }

  set email(value) {
    this.setAttribute('email', value);
  }

  get email() {
    return this.getAttribute('email');
  }

  connectedCallback() {
    this.updateContent();
  }

  updateContent() {
    this.shadowRoot.querySelector('h3').textContent = this.userName || 'Nama Tidak Diketahui';
    this.shadowRoot.querySelector('p').textContent = this.email || 'Email Tidak Diketahui';
  }
}
customElements.define('user-card', UserCard);

// app.js (komponen parent)
class MyApp extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <user-card user-name="Budi Santoso" email="budi@example.com"></user-card>
      <user-card id="dianaCard"></user-card>
    `;

    // Mengatur properti melalui JS
    const dianaCard = this.shadowRoot.querySelector('#dianaCard');
    dianaCard.userName = 'Diana Putri';
    dianaCard.email = 'diana@example.com';

    // Contoh perubahan dinamis
    setTimeout(() => {
      dianaCard.userName = 'Diana Sari';
      dianaCard.setAttribute('email', 'diana.sari@example.com');
    }, 2000);
  }
}
customElements.define('my-app', MyApp);

Kapan Digunakan:

Hal yang Perlu Diperhatikan:

4. Pola 3: Metode Publik (Public Methods)

📌 Konsep: Mirip dengan properti, ini adalah cara komunikasi dari orang tua ke anak, tetapi melibatkan pemanggilan fungsi pada instance komponen anak. Ini memungkinkan parent untuk memicu tindakan spesifik pada child.

💡 Cara Kerja: Komponen anak mengekspos metode publik pada instancenya. Komponen orang tua mendapatkan referensi ke instance komponen anak dan memanggil metode tersebut.

// video-player.js
class VideoPlayer extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        div { border: 1px solid #ccc; padding: 10px; margin: 10px; }
        video { width: 100%; max-width: 400px; }
      </style>
      <div>
        <video controls src="https://www.w3schools.com/html/mov_bbb.mp4"></video>
        <p>Status: <span id="status">Stopped</span></p>
      </div>
    `;
    this._video = this.shadowRoot.querySelector('video');
    this._status = this.shadowRoot.querySelector('#status');

    this._video.addEventListener('play', () => this._status.textContent = 'Playing');
    this._video.addEventListener('pause', () => this._status.textContent = 'Paused');
    this._video.addEventListener('ended', () => this._status.textContent = 'Ended');
  }

  play() {
    this._video.play();
  }

  pause() {
    this._video.pause();
  }

  stop() {
    this._video.pause();
    this._video.currentTime = 0;
    this._status.textContent = 'Stopped';
  }

  setVideoSource(src) {
    this._video.src = src;
    this._status.textContent = 'Loaded';
  }
}
customElements.define('video-player', VideoPlayer);

// app.js (komponen parent)
class MyApp extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <video-player id="myVideoPlayer"></video-player>
      <button id="playBtn">Play</button>
      <button id="pauseBtn">Pause</button>
      <button id="stopBtn">Stop</button>
    `;

    const player = this.shadowRoot.querySelector('#myVideoPlayer');
    this.shadowRoot.querySelector('#playBtn').addEventListener('click', () => player.play());
    this.shadowRoot.querySelector('#pauseBtn').addEventListener('click', () => player.pause());
    this.shadowRoot.querySelector('#stopBtn').addEventListener('click', () => player.stop());

    // Mengatur sumber video setelah komponen terhubung
    player.setVideoSource('https://www.w3schools.com/html/mov_bbb.mp4');
  }
}
customElements.define('my-app', MyApp);

Kapan Digunakan:

Hal yang Perlu Diperhatikan:

5. Pola 4: Slot (Slots)

📌 Konsep: Slot bukanlah mekanisme komunikasi data secara langsung, melainkan cara untuk mendistribusikan konten (HTML) dari komponen orang tua ke dalam struktur internal komponen anak. Ini adalah bentuk komunikasi struktur atau komposisi UI.

💡 Cara Kerja: Komponen anak mendefinisikan satu atau lebih elemen <slot> di dalam shadow DOM-nya. Komponen orang tua kemudian menempatkan konten HTML di antara tag komponen anak, dan konten tersebut akan “disalurkan” ke slot yang sesuai.

// my-card.js
class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        .card {
          border: 1px solid #ddd;
          border-radius: 8px;
          padding: 16px;
          margin: 10px;
          box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        ::slotted(h3) { color: purple; } /* Styling konten yang dislot */
        ::slotted(footer) { font-size: 0.8em; color: gray; }
      </style>
      <div class="card">
        <header>
          <slot name="header"></slot>
        </header>
        <main>
          <slot></slot> <!-- Default slot -->
        </main>
        <footer>
          <slot name="footer"></slot>
        </footer>
      </div>
    `;
  }
}
customElements.define('my-card', MyCard);

// app.js
class MyApp extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <my-card>
        <h3 slot="header">Judul Kartu Keren</h3>
        <p>Ini adalah konten utama dari kartu saya. Bisa berupa teks, gambar, atau elemen lain.</p>
        <button>Aksi!</button>
        <footer slot="footer">Dibuat pada ${new Date().getFullYear()}</footer>
      </my-card>

      <my-card>
        <h3 slot="header">Kartu Tanpa Footer</h3>
        <p>Konten lain di sini.</p>
      </my-card>
    `;
  }
}
customElements.define('my-app', MyApp);

Kapan Digunakan:

Hal yang Perlu Diperhatikan:

6. Pola 5: State Management Global (Event Bus atau Context API Sederhana)

📌 Konsep: Untuk komunikasi antar komponen yang berjauhan (misalnya, sibling dari sibling, atau komponen di bagian DOM yang berbeda) atau untuk mengelola state global yang perlu diakses oleh banyak komponen. Ini adalah solusi untuk menghindari “prop drilling” (meneruskan properti melalui banyak level komponen).

💡 Cara Kerja:

// global-event-bus.js
// Sebuah Event Bus sederhana menggunakan CustomEvent pada window
const globalEventBus = {
  emit(eventName, detail) {
    window.dispatchEvent(new CustomEvent(eventName, { detail }));
  },
  on(eventName, callback) {
    window.addEventListener(eventName, callback);
  },
  off(eventName, callback) {
    window.removeEventListener(eventName, callback);
  }
};

// notif-center.js (komponen yang mendengarkan event global)
class NotifCenter extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>