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:
- Ketika komponen anak perlu memberi tahu komponen orang tua tentang suatu aksi (misalnya, klik tombol, pengiriman formulir, pemilihan item).
- Ketika Anda ingin decoupling yang kuat antara komponen. Komponen yang memicu event tidak perlu tahu siapa yang mendengarkan.
❌ Hal yang Perlu Diperhatikan:
bubbles: truedancomposed: truesangat penting jika event perlu melintasi Shadow DOM dan didengarkan oleh komponen di luar shadow root.- Jangan terlalu banyak event; bisa sulit dilacak.
- Hindari event yang terlalu generik. Buat nama event sejelas mungkin.
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:
- Atribut: Set atribut pada tag HTML komponen anak (
<user-card user-name="Budi">). Komponen anak bisa “mengamati” perubahan atribut ini. - Properti: Set properti JavaScript secara langsung pada instance komponen anak (
userCardElement.userName = "Budi").
// 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:
- Meneruskan data konfigurasi atau state dari parent ke child.
- Menginisialisasi komponen anak dengan nilai awal.
❌ Hal yang Perlu Diperhatikan:
- Atribut HTML hanya bisa berupa string. Untuk data kompleks (objek, array), gunakan properti JS.
observedAttributesdanattributeChangedCallbackpenting untuk mereaksi perubahan atribut secara deklaratif.- Perubahan properti JS tidak secara otomatis memicu re-render. Anda perlu mengimplementasikan logika update sendiri (seperti
updateContent()di atas).
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:
- Ketika parent perlu memicu tindakan langsung pada anak, seperti
play(),reset(),submit(). - Untuk kontrol yang jelas dan imperatif.
❌ Hal yang Perlu Diperhatikan:
- Penggunaan berlebihan bisa membuat komponen orang tua terlalu bergantung pada implementasi detail komponen anak, mengurangi fleksibilitas.
- Lebih baik digabungkan dengan event kustom; misalnya, setelah
play()dipanggil, komponen anak bisa memicu eventvideoPlayingagar parent tahu statusnya.
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:
- Untuk membuat komponen yang sangat fleksibel dalam hal konten yang mereka tampilkan (misalnya, komponen
Modal,Card,Layout). - Ketika Anda ingin memisahkan struktur internal komponen dari konten yang disediakan oleh parent.
❌ Hal yang Perlu Diperhatikan:
default slot(tanpa atributname) akan menerima semua konten yang tidak disalurkan ke slot bernama.- Gunakan pseudo-element
::slotted()untuk menata gaya konten yang disalurkan dari luar Shadow DOM. - Slot lebih tentang komposisi UI daripada komunikasi data dinamis.
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:
- Event Bus Global: Membuat sebuah objek JavaScript sederhana yang bertindak sebagai pusat untuk memicu dan mendengarkan event. Objek ini bisa diakses secara global (misalnya, melalui
windowatau sebuah modul singleton). - Context API Sederhana: Membuat objek state reaktif global, bisa dengan
Proxyuntuk reaktivitas atau hanya objek biasa yang dimodifikasi. Komponen bisa mendaftar untuk mendengarkan perubahan pada state ini.
// 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>