Halaman katalog Budi menampilkan 40 produk dalam sebuah daftar, dan daftarnya terus bertambah: setiap kali stok baru datang, JavaScript menambahkan elemen <li> saat halaman berjalan. Pendekatan naifnya jelas — pasang listener click pada setiap item. Cara itu bekerja, tetapi mahal: 40 pendaftaran untuk 40 item, dan setiap item baru membutuhkan pendaftaran tambahan. Lebih mengganggu lagi, pelepasan listener per item harus diingat saat item dihapus, agar tidak ada listener menganggur yang menempel di memori.
Event delegation menyelesaikan masalah ini dengan membalik cara pandang kita: jangan menanyakan setiap anak apakah ia diklik — pasang satu listener di induknya, lalu biarkan bubbling dari chapter Event Bubbling membawa setiap klik naik ke sana. Satu pendaftaran melayani semua anak, termasuk anak yang belum ada ketika listener dipasang. Chapter ini membangun pola itu langkah demi langkah dengan daftar produk yang kita uji sendiri.
Satu Listener untuk Banyak Anak #
Aturan polanya: daftarkan listener pada elemen induk yang stabil — elemen yang tidak pernah diganti selama halaman berjalan. Mekanismenya berdiri di atas dua fakta yang sudah kita buktikan sebelumnya. Pertama, event dari anak merambat naik ke seluruh leluhurnya, sehingga listener induk menerima klik pada siapa pun di dalamnya. Kedua, event.target sepanjang perjalanan tetap menunjuk elemen asal peristiwa, sehingga induk bisa memeriksa “siapa yang persis diklik” sebelum bereaksi.
Hasilnya sebuah pembagian tugas yang rapi: listener bertindak sebagai petugas satu pintu, sedangkan setiap anak tidak perlu memiliki kode pendengaran sendiri. Jumlah listener di halaman tetap satu, berapa pun jumlah itemnya.
Pola ini juga terasa natural saat dikombinasikan dengan atribut data. Bila setiap <li> membawa data-id produk, handler induk cukup membaca event.target.dataset.id untuk mengetahui produk mana yang dipilih — tanpa mencari di daftar mana pun. Atribut data menjadi kartu identitas setiap anak, dan listener induk menjadi satu-satunya pembacanya.
Memeriksa Asal Klik dengan event.target.matches() #
Method matches() menerima selector CSS apa pun yang juga sah untuk querySelector — class, id, atribut, sampai gabungannya seperti li.item.aktif — sehingga penjaga handler bisa sespesifik kebutuhan. Berikut daftar produk dua item yang akan kita kembangkan menjadi tiga saat halaman berjalan:
<ul id="daftar">
<li class="item">Kopi Gayo</li>
<li class="item">Teh Melati</li>
</ul>// delegasi-daftar.js — satu listener di induk melayani semua item anak
const daftar = document.querySelector("#daftar");
daftar.addEventListener("click", function (event) {
if (!event.target.matches(".item")) {
return; // klik di luar item diabaikan
}
console.log(`Produk dipilih: ${event.target.textContent}`);
});
// item baru ditambahkan saat halaman berjalan
const itemBaru = document.createElement("li");
itemBaru.className = "item";
itemBaru.textContent = "Gula Aren";
daftar.appendChild(itemBaru);Untuk mengujinya, kita mensimulasikan empat klik: item pertama, item kedua, area daftar di luar item, lalu item ketiga yang baru saja ditambahkan. Keluaran berikut dicatat dari eksekusi nyata berkas ini di lingkungan DOM jsdom.
Output yang Dihasilkan (console browser):
Produk dipilih: Kopi Gayo
Produk dipilih: Teh Melati
Produk dipilih: Gula ArenMari kita telaah baris demi baris:
- Baris 4–9 (satu listener untuk seluruh daftar): listener terpasang pada
<ul>, bukan pada<li>. Setiap klik di dalam daftar — pada item mana pun — naik lewat bubbling dan diterima handler ini. - Baris 5–7 (penjaga
matches(".item")): methodmatches()memeriksa apakah elemen target cocok dengan selector CSS.item. Klik pada<ul>itu sendiri (misalnya di ruang kosong antar item) menghasilkantargetberupa<ul>yang tidak cocok, sehinggareturnmengakhiri handler tanpa mencetak apa pun. Inilah sebabnya hanya tiga baris keluaran, bukan empat. - Baris 8 (
event.target.textContent): karena target sudah lolos pemeriksaan, kita boleh membaca isinya langsung — nama produk yang diklik. - Baris 12–15 (item baru tanpa listener baru):
createElementmembuat<li>, properticlassNamedantextContentmengisinya, laluappendChildmemasangkannya ke daftar. Item ini tidak pernah “didaftarkan” ke mana pun — dan tetap merespons klik, karena yang mendengarkan adalah induknya.
Daftar yang Berkembang Tanpa Listener Baru #
Baris terakhir keluaran di atas adalah inti dari seluruh pola: Produk dipilih: Gula Aren dicetak oleh listener yang dipasang jauh sebelum item itu ada. Bila besok daftar berisi 400 produk, atau item dihapus dan diganti setiap detik, jumlah listener di halaman tetap satu. Pekerjaan yang tadinya menyebar di banyak tempat — pasang, perbarui, lepas — kini tidak ada sama sekali.
Manfaat lain menyusul sendiri saat kita merapikan kode: semua logika klik produk terkumpul di satu function, sehingga perubahan aturan (menambah pencatatan, memeriksa stok, menonaktifkan item kosong) cukup dilakukan di satu tempat.
Bandingkan dengan pendekatan per-item: setiap render ulang daftar menuntut pelepasan dan pemasangan ulang seluruh listener, dan satu kelalaian membuat listener menganggur yang mengunci item lama di memori. Dengan delegation, render ulang hanya berarti mengganti isi daftar — listener induk tidak tersentuh sama sekali, karena tidak terikat pada satu item pun.
Kapan Delegation Tepat Digunakan #
Dari sisi performa, keuntungan delegation bukan sekadar jumlah baris. Setiap listener menempati memori dan menambah pekerjaan pelepasan saat elemen dibuang; seribu item berarti seribu pendaftaran yang harus dikelola. Satu listener di induk memangkas seluruh biaya itu menjadi konstanta, dan jumlahnya tidak berubah berapa pun panjang daftarnya.
Delegation paling sepadan ketika banyak elemen sejenis berbagi perlakuan yang sama: baris tabel, item daftar, kartu pada grid, selain itu tombol di dalam panel. Pola ini juga bekerja untuk event lain yang merambat — keydown pada wadah form, input pada sekelompok kolom — asalkan peristiwanya ikut bubbling. Event yang tidak merambat, seperti focus, blur, mouseenter, dan mouseleave, tidak bisa didelegasikan dengan cara ini.
Dua hal perlu dijaga. Pertama, penjaga matches() atau closest() wajib ada; tanpa penjaga, handler bereaksi pada klik yang bukan bagian dari rencana — termasuk klik pada teks penjelasan di dalam wadah. Kedua, pilih induk yang benar-benar stabil: induk yang terlalu tinggi (misalnya document) menerima terlalu banyak klik, sedangkan induk yang ikut terhapus membawa pergi satu-satunya listener kita.
Sampai di sini semua contoh bereaksi terhadap perangkat penunjuk: klik dari mouse atau sentuhan. Chapter berikutnya beralih ke perangkat lain yang sama pentingnya — papan ketik — beserta dua properti yang sering tertukar: event.key dan event.code.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
delegasi-daftar.js
Unduh berkas// delegasi-daftar.js — satu listener di induk melayani semua item anak
const daftar = document.querySelector("#daftar");
daftar.addEventListener("click", function (event) {
if (!event.target.matches(".item")) {
return; // klik di luar item diabaikan
}
console.log(`Produk dipilih: ${event.target.textContent}`);
});
// item baru ditambahkan saat halaman berjalan
const itemBaru = document.createElement("li");
itemBaru.className = "item";
itemBaru.textContent = "Gula Aren";
daftar.appendChild(itemBaru);