Kartu produk milik Budi punya satu handler untuk seluruh kartu: saat kartu diklik, halaman detail terbuka. Masalahnya muncul ketika di dalam kartu ada tombol kecil “Simpan untuk Nanti” dengan handler sendiri. Setiap kali tombol itu ditekan, handler kartu ikut terpanggil — seolah kartunya juga diklik. Budi mengira kodenya keliru, padahal perilaku itu memang disengaja oleh spesifikasi: peristiwa tidak berhenti di elemen target, melainkan merambat naik ke seluruh leluhurnya.
Perambatan naik ini disebut event bubbling — peristiwanya seperti gelembung udara yang naik dari dasar air ke permukaan. Memahaminya penting karena hampir semua event di halaman bersifat begitu: klik, ketikan, dan pergerakan pointer tidak hanya sampai ke elemen yang disentuh, tetapi juga ke setiap induknya, satu per satu, sampai puncak dokumen. Chapter ini membuktikannya dengan percobaan tiga lapis elemen yang urutan lognya kita uji sendiri, sekaligus menjadi dasar bagi pola event delegation pada chapter lanjutan.
Cara Peristiwa Naik ke Atas #
Aturannya: setelah handler pada elemen target selesai dijalankan, browser mencari listener click pada induk target langsung, menjalankannya, lalu naik ke induk berikutnya, dan seterusnya hingga mencapai document. Perjalanan ini mengikuti rantai leluhur persis seperti struktur bersarang HTML — tidak ada lompatan, tidak ada cabang samping.
Rantai leluhur juga menentukan siapa yang berhak ikut. Handler hanya berjalan bila elemennya berada di jalur naik — target itu sendiri atau salah satu leluhurnya. Elemen lain di luar rantai, semirip bentuknya sekalipun, tidak akan menerima peristiwa tersebut. Aturan sederhana ini membuat perilaku bubbling mudah diprediksi: lihat struktur HTML, susuri rantai induk dari elemen target, dan seluruh peserta perjalanan langsung terlihat.
Bukan hanya handler induk yang ikut bekerja: object event yang diterima setiap handler adalah object yang sama. Properti event.target tetap menunjuk elemen asal peristiwa di sepanjang perjalanan, sedangkan elemen yang sedang menjalankan handler terbaca lewat event.currentTarget. Alur lengkapnya seperti ini:
Tidak semua event ikut naik. Peristiwa seperti focus, blur, load, serta mouseenter dan mouseleave tidak merambat, sedangkan click, input, keydown, dan sebagian besar peristiwa pointer merambat. Properti boolean event.bubbles pada object event menunjukkan statusnya. Jika kita perlu membatalkan perjalanan ini, method stopPropagation() yang dikenal pada chapter sebelumnya adalah tombol penutupnya.
Saat Event Tidak Ikut Naik #
Daftar pembatas tadi menimbulkan pertanyaan praktis: bagaimana bila kita justru butuh mendengarkan focus di level induk — misalnya untuk menandai seluruh baris saat kolom di dalamnya difokuskan? Untungnya, hampir setiap peristiwa yang tidak merambat punya saudara yang merambat, dengan momen yang sama:
| Tidak merambat | Pengganti yang merambat |
|---|---|
focus, blur | focusin, focusout |
mouseenter, mouseleave | mouseover, mouseout |
Pasangan di kolom kanan terpicu pada momen yang sama dengan kolom kiri, tetapi mengikuti bubbling — sehingga bisa didelegasikan ke induk. Satu perbedaan kecil perlu dijaga: mouseover dan mouseout juga menyala saat pointer berpindah antar elemen anak di dalam target, sehingga handler kadang perlu menyaring dengan event.target agar reaksi tidak terpicu dua kali.
Percobaan Tiga Lapis #
Mari kita buktikan urutan itu. Struktur HTML-nya tiga lapis — sebuah tombol di dalam dua kotak bersarang:
<div id="luar">
<div id="tengah">
<button id="tombol">Kirim Pesan</button>
</div>
</div>Ketiga lapis kita pasangi listener click yang masing-masing mencatat dirinya beserta elemen target menurut object event:
// bubbling-tiga-level.js — mengamati urutan bubbling pada tiga lapis elemen
const luar = document.querySelector("#luar");
const tengah = document.querySelector("#tengah");
const tombol = document.querySelector("#tombol");
tombol.addEventListener("click", function (event) {
console.log(`handler #tombol | target: #${event.target.id}`);
});
tengah.addEventListener("click", function (event) {
console.log(`handler #tengah | target: #${event.target.id}`);
});
luar.addEventListener("click", function (event) {
console.log(`handler #luar | target: #${event.target.id}`);
});Kita jalankan dua aksi: menekan tombol #tombol, lalu menekan langsung kotak #luar. Keluaran berikut dicatat dari eksekusi nyata berkas ini di lingkungan DOM jsdom, dengan urutan yang sama di console browser.
Output yang Dihasilkan (console browser):
handler #tombol | target: #tombol
handler #tengah | target: #tombol
handler #luar | target: #tombol
handler #luar | target: #luarMari kita telaah:
- Baris 6–8 (listener tombol): handler target berjalan lebih dulu. Urutan ini tidak bisa ditawar — bubbling selalu dimulai dari elemen terdalam tempat peristiwa berasal.
- Baris 10–12 (listener tengah): event naik satu tingkat dan handler
#tengahberjalan. Perhatikantargetyang tercatat tetap#tombol, bukan#tengah. - Baris 14–16 (listener luar): perjalanan berlanjut ke
#luar, dan handler ketiga berjalan dengantargetyang masih#tombol. Tiga baris pertama keluaran adalah jejak lengkap bubbling. - Baris terakhir keluaran (klik langsung pada
#luar): ketika targetnya#luarsendiri, hanya handler#luaryang berjalan, dengantarget: #luar. Handler#tengahdan#tomboltidak terlibat karena keduanya bukan leluhur dari target.
target Tetap, Rantai yang Berubah #
Keluaran di atas memperlihatkan dua fakta yang saling melengkapi. Pertama, event.target adalah satu-satunya alamat asal peristiwa — nilainya sama di tiga handler yang berbeda, sehingga kode yang perlu tahu “apa yang persis diklik” boleh ditaruh di handler mana pun di sepanjang rantai. Kedua, yang menentukan handler mana yang berjalan adalah rantai leluhur target: klik pada tombol menghidupkan tiga handler, klik pada kotak luar hanya satu.
Fakta kedua inilah yang menjelaskan kasus Budi di awal chapter: handler kartu tidak pernah salah terpanggil, karena tombol di dalam kartu memang berada di jalur naiknya. Bila bagian kartu tidak boleh ikut bereaksi, pilihannya jelas — jangan menaruh keduanya di satu rantai, atau hentikan perambatan di tempat yang tepat.
Mengapa Bubbling Berguna #
Dari satu sisi, bubbling terasa seperti efek samping yang harus dijaga dengan stopPropagation(). Dari sisi lain, bubbling adalah mesin di balik salah satu pola paling ekonomis dalam pemrograman DOM: memasang satu listener pada induk untuk melayani puluhan anak sekaligus, termasuk anak yang baru lahir setelah halaman berjalan. Pola itu bernama event delegation dan menjadi bahasan penuh dua chapter lagi.
Bubbling juga membantu saat kita mencari sumber perilaku yang tak terduga. Satu listener sementara pada document cukup untuk mencetak setiap event.type dan event.target yang lewat, dan jejak perjalanan event langsung terlihat tanpa menyentuh kode widget mana pun. Teknik pelacakan kecil ini bekerja karena tidak ada peristiwa yang bisa menghindar dari rantai naiknya.
Sebelum sampai ke sana, ada satu bagian perjalanan yang belum kita lihat: sebelum event naik dari tombol ke atas, browser sempat menurunkannya dari puncak dokumen ke elemen target terlebih dahulu. Fase turun inilah topik chapter berikutnya.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
bubbling-tiga-level.js
Unduh berkas// bubbling-tiga-level.js — mengamati urutan bubbling pada tiga lapis elemen
const luar = document.querySelector("#luar");
const tengah = document.querySelector("#tengah");
const tombol = document.querySelector("#tombol");
tombol.addEventListener("click", function (event) {
console.log(`handler #tombol | target: #${event.target.id}`);
});
tengah.addEventListener("click", function (event) {
console.log(`handler #tengah | target: #${event.target.id}`);
});
luar.addEventListener("click", function (event) {
console.log(`handler #luar | target: #${event.target.id}`);
});