DOM: Event Delegation
Teknik satu listener untuk banyak elemen dengan memanfaatkan event bubbling. Solusi wajib untuk list dan tabel dinamis: hemat memori, otomatis berlaku untuk elemen yang dibuat belakangan, plus pemahaman bubbling vs capturing.
Masalah: Seribu Listener untuk Seribu Item
Pendekatan naif menangani klik tiap item list adalah memasang listener ke masing-masing:
// BURUK untuk 1000 item: 1000 listener di memori!
document.querySelectorAll("li").forEach(li => {
li.addEventListener("click", hapusItem);
});Selain boros, ada bug fatal: item yang ditambahkan BELAKANGAN (mis. tugas baru di todo list) tidak punya listener sama sekali karena dipasang sebelum ia lahir. Solusinya adalah membalik logika: pasang SATU listener di parent, dan biarkan event "menggelembung" naik dari anak ke parent.
Bubbling: Cara Event Berjalan
Saat user mengklik <li> di dalam <ul>, browser tidak hanya memberi tahu <li> itu. Event berjalan dalam tiga fase:
- Capturing: turun dari
windowke elemen target. - Target: tiba di elemen yang diklik.
- Bubbling: naik kembali dari target ke
window.
Secara default listener bekerja di fase bubbling. Artinya klik pada <li> akan "terdengar" juga oleh <ul>, <body>, sampai document. Inilah yang dimanfaatkan delegation.
// e.target = elemen yang DIKLIK (li)
// e.currentTarget = elemen pemilik LISTENER (ul)
document.getElementById("daftar").addEventListener("click", (e) => {
console.log("Diklik:", e.target.tagName);
console.log("Listener di:", e.currentTarget.id);
});Solusi: Satu Listener di Parent
Contoh sederhana memakai playground modul ini (ada <ul id="daftar"> berisi li A dan B, plus tombol tambah):
const ul = document.getElementById("daftar");
ul.addEventListener("click", (e) => {
if (e.target.tagName === "LI") {
e.target.remove(); // klik item mana pun -> hapus
}
});Pengecekan e.target.tagName === "LI" penting: tanpa itu, klik di area kosong <ul> ikut terpicu. Listener ini otomatis berlaku untuk item A, B, MAUPUN item baru yang ditambahkan tombol "Tambah", karena event mereka semua menggelembung ke <ul> yang sama.
Contoh realistis: todo list dengan tombol hapus per item, memakai closest agar klik pada ikon di dalam tombol tetap terdeteksi.
const ul = document.getElementById("daftar");
ul.addEventListener("click", (e) => {
const tombolHapus = e.target.closest(".hapus");
if (tombolHapus) {
tombolHapus.closest("li").remove();
return;
}
const item = e.target.closest("li");
if (item) item.classList.toggle("selesai");
});Satu listener menangani dua aksi berbeda (hapus dan toggle selesai) untuk item yang jumlahnya tak terbatas, termasuk yang belum lahir saat halaman dimuat.
Kapan Delegation Wajib Dipakai?
- List/tabel dinamis: todo, chat, hasil pencarian, keranjang belanja.
- Elemen yang dibuat setelah halaman dimuat (dari API, dari input user).
- Jumlah elemen besar (ratusan/ribuan): satu listener jauh lebih hemat memori.
Kapan TIDAK perlu? Form kecil dengan segelintir tombol statis: pasang listener langsung saja, lebih mudah dibaca.
stopPropagation dan Capturing
Kadang kamu ingin menghentikan gelembung agar parent tidak ikut bereaksi:
document.getElementById("modal").addEventListener("click", (e) => {
e.stopPropagation(); // klik di dalam modal tidak menutup modal
});
document.body.addEventListener("click", tutupModal);Dan kalau butuh menangkap event di fase TURUN (capturing), pakai opsi ketiga:
el.addEventListener("click", handler, true); // true = fase capturingJebakan Umum
- Lupa memfilter
e.target: listener parent bereaksi terhadap SEMUA klik di dalamnya, termasuk klik di padding kosong. Selalu cek denganclosest()atautagName. e.targetbisa berupa elemen anak TERDALAM (mis.<span>di dalam<button>). Jangan bandingkan langsung dengan elemen yang diharapkan; gunakane.target.closest("button").- Memakai delegation untuk event yang TIDAK menggelembung (
focus,blur,scrollpada elemen). Untuk focus/blur pakai versi gelembungnya:focusin/focusout.
Ringkasan
Event delegation = satu listener di parent + manfaatkan bubbling + filter via e.target/closest(). Pola ini wajib untuk konten dinamis: hemat memori, otomatis mencakup elemen baru, dan menjadi standar industri untuk list, tabel, dan menu. Ingat bedanya: e.target adalah yang diklik, e.currentTarget adalah pemilik listener.
export default function App(): JSX.Element { return <h1>Hello world</h1> }
Tantangan
Todo Delegasi
Di #daftar yang sudah ada, tambahkan 3 item via JS. Pasang SATU delegation listener di #daftar: klik item men-toggle class "selesai" (bukan hapus). Delegation adalah pola yang dipakai aplikasi chat dan feed nyata untuk menangani ribuan pesan tanpa memasang ribuan listener: satu listener di wadah cukup untuk semuanya, termasuk pesan yang baru masuk.
<!doctype html> <html> <head> <meta charset="utf-8" /> </head> <body> <h1>Halo JS</h1> <p>Buka console preview untuk melihat output.</p> <script src="index.js"></script> </body> </html>