Pembahasan mendalam tentang peristiwa JavaScript

preventDefault dan stopPropagation: kapan harus menggunakan metode mana dan apa yang sebenarnya dilakukan oleh setiap metode.

Penanganan peristiwa JavaScript sering kali mudah. Hal ini terutama berlaku saat menangani struktur HTML sederhana (relatif datar). Namun, hal-hal menjadi sedikit lebih rumit saat peristiwa berpindah (atau menyebar) melalui hierarki elemen. Hal ini biasanya terjadi saat developer menghubungi stopPropagation() dan/atau preventDefault() untuk menyelesaikan masalah yang mereka alami. Jika Anda pernah berpikir "Saya akan mencoba preventDefault() dan jika tidak berhasil, saya akan mencoba stopPropagation() dan jika tidak berhasil, saya akan mencoba keduanya", maka artikel ini cocok untuk Anda. Saya akan menjelaskan dengan tepat fungsi setiap metode, kapan harus menggunakan metode mana, dan memberikan berbagai contoh yang berfungsi untuk Anda jelajahi. Tujuan saya adalah mengakhiri kebingungan Anda selamanya.

Namun, sebelum kita membahasnya lebih dalam, penting untuk membahas secara singkat dua jenis penanganan peristiwa yang mungkin dilakukan di JavaScript (di semua browser modern—Internet Explorer sebelum versi 9 tidak mendukung pengambilan peristiwa sama sekali).

Gaya peristiwa (perekaman dan bubble)

Semua browser modern mendukung pengambilan peristiwa, tetapi sangat jarang digunakan oleh developer. Menariknya, ini adalah satu-satunya bentuk peristiwa yang awalnya didukung Netscape. Saingan terbesar Netscape, Microsoft Internet Explorer, sama sekali tidak mendukung pengambilan peristiwa, tetapi hanya mendukung gaya peristiwa lain yang disebut bubbling peristiwa. Saat W3C dibentuk, mereka menemukan manfaat dalam kedua gaya peristiwa dan menyatakan bahwa browser harus mendukung keduanya, melalui parameter ketiga ke metode addEventListener. Awalnya, parameter tersebut hanyalah boolean, tetapi semua browser modern mendukung objek options sebagai parameter ketiga, yang dapat Anda gunakan untuk menentukan (di antara hal lainnya) apakah Anda ingin menggunakan pengambilan peristiwa atau tidak:

someElement.addEventListener('click', myClickHandler, { capture: true | false });

Perhatikan bahwa objek options bersifat opsional, begitu juga dengan properti capture-nya. Jika salah satunya dihilangkan, nilai default untuk capture adalah false, yang berarti bubbling peristiwa akan digunakan.

Perekaman peristiwa

Apa artinya jika pengendali peristiwa Anda "mendengarkan dalam fase pengambilan?" Untuk memahami hal ini, kita perlu mengetahui asal peristiwa dan cara peristiwa tersebut menyebar. Hal berikut berlaku untuk semua peristiwa, meskipun Anda, sebagai developer, tidak memanfaatkannya, tidak peduli, atau tidak memikirkannya.

Semua peristiwa dimulai di jendela dan pertama-tama melalui fase pengambilan. Artinya, saat peristiwa dikirim, peristiwa akan memulai jendela dan bergerak "ke bawah" ke elemen targetnya terlebih dahulu. Hal ini terjadi meskipun Anda hanya memproses di fase bubbling. Perhatikan contoh markup dan JavaScript berikut:

<html>
 
<body>
   
<div id="A">
     
<div id="B">
       
<div id="C"></div>
     
</div>
   
</div>
 
</body>
</html>
document.getElementById('C').addEventListener(
 
'click',
 
function (e) {
    console
.log('#C was clicked');
 
},
 
true,
);

Saat pengguna mengklik elemen #C, peristiwa yang berasal dari window akan dikirim. Peristiwa ini kemudian akan disebarkan melalui turunannya sebagai berikut:

window => document => <html> => <body> => dan seterusnya, hingga mencapai target.

Tidak masalah jika tidak ada yang memproses peristiwa klik di elemen window atau document atau <html> atau elemen <body> (atau elemen lain dalam perjalanan ke targetnya). Peristiwa masih berasal dari window dan memulai perjalanannya seperti yang baru saja dijelaskan.

Dalam contoh kita, peristiwa klik kemudian akan disebarkan (ini adalah kata penting karena akan terkait langsung dengan cara kerja metode stopPropagation() dan akan dijelaskan nanti dalam dokumen ini) dari window ke elemen targetnya (dalam hal ini, #C) melalui setiap elemen di antara window dan #C.

Artinya, peristiwa klik akan dimulai pada window dan browser akan mengajukan pertanyaan berikut:

"Apakah ada yang memproses peristiwa klik pada window dalam fase pengambilan?" Jika ya, pengendali peristiwa yang sesuai akan diaktifkan. Dalam contoh kita, tidak ada yang diaktifkan, sehingga tidak ada pengendali yang akan diaktifkan.

Selanjutnya, peristiwa akan disebarkan ke document dan browser akan bertanya: "Apakah ada yang memproses peristiwa klik di document dalam fase pengambilan?" Jika demikian, pengendali peristiwa yang sesuai akan diaktifkan.

Selanjutnya, peristiwa akan disebarkan ke elemen <html> dan browser akan bertanya: "Apakah ada yang memproses klik pada elemen <html> dalam fase pengambilan?" Jika ya, pengendali peristiwa yang sesuai akan diaktifkan.

Selanjutnya, peristiwa akan disebarkan ke elemen <body> dan browser akan bertanya: "Apakah ada yang memproses peristiwa klik pada elemen <body> dalam fase pengambilan?" Jika demikian, pengendali peristiwa yang sesuai akan diaktifkan.

Selanjutnya, peristiwa akan disebarkan ke elemen #A. Sekali lagi, browser akan bertanya: "Apakah ada yang memproses peristiwa klik di #A dalam fase pengambilan dan jika ya, pengendali peristiwa yang sesuai akan diaktifkan.

Selanjutnya, peristiwa akan disebarkan ke elemen #B (dan pertanyaan yang sama akan diajukan).

Terakhir, peristiwa akan mencapai targetnya dan browser akan bertanya: "Apakah ada yang memproses peristiwa klik pada elemen #C dalam fase pengambilan?" Jawabannya kali ini adalah "ya". Periode waktu singkat saat peristiwa di target ini dikenal sebagai "fase target". Pada tahap ini, pengendali peristiwa akan diaktifkan, browser akan console.log "#C diklik", lalu kita selesai, bukan? Salah. Kita belum selesai sama sekali. Prosesnya berlanjut, tetapi sekarang berubah menjadi fase bubbling.

Pembentukan gelembung peristiwa

Browser akan menanyakan:

"Apakah ada yang memproses peristiwa klik pada #C dalam fase bubbling?" Perhatikan dengan cermat di sini. Anda dapat memproses klik (atau jenis peristiwa apa pun) di fase pengambilan dan bubbling. Dan jika Anda telah menghubungkan pengendali peristiwa di kedua fase (misalnya, dengan memanggil .addEventListener() dua kali, sekali dengan capture = true dan sekali dengan capture = false), maka ya, kedua pengendali peristiwa akan benar-benar diaktifkan untuk elemen yang sama. Namun, penting juga untuk diperhatikan bahwa kedua peristiwa tersebut diaktifkan dalam fase yang berbeda (satu dalam fase pengambilan dan satu dalam fase bubble).

Selanjutnya, peristiwa akan disebarkan (lebih umum dinyatakan sebagai "bubble" karena sepertinya peristiwa tersebut "naik" ke hierarki DOM) ke elemen induknya, #B, dan browser akan bertanya: "Apakah ada yang memproses peristiwa klik di #B dalam fase bubbling?" Dalam contoh kita, tidak ada yang terdeteksi, sehingga tidak ada pengendali yang akan diaktifkan.

Selanjutnya, peristiwa akan di-bubble ke #A dan browser akan bertanya: "Apakah ada yang memproses peristiwa klik di #A dalam fase bubble?"

Selanjutnya, peristiwa akan di-bubble ke <body>: "Apakah ada yang memproses peristiwa klik pada elemen <body> dalam fase bubble?"

Selanjutnya, elemen <html>: "Apakah ada yang memproses peristiwa klik pada elemen <html> dalam fase bubbling?

Selanjutnya, document: "Apakah ada yang memproses peristiwa klik pada document dalam fase bubbling?"

Terakhir, window: "Apakah ada yang memproses peristiwa klik pada jendela dalam fase bubbling?"

Fiuh! Perjalanan ini panjang, dan acara kita mungkin sudah sangat lelah sekarang, tetapi percaya atau tidak, itulah perjalanan yang dilalui setiap peristiwa. Sering kali, hal ini tidak pernah diketahui karena developer biasanya hanya tertarik pada satu fase peristiwa atau fase lainnya (dan biasanya fase bubbling).

Sebaiknya luangkan waktu untuk mencoba pengambilan peristiwa dan bubble peristiwa serta mencatat beberapa catatan ke konsol saat pengendali diaktifkan. Sangatlah penting untuk melihat jalur yang diambil peristiwa. Berikut adalah contoh yang memproses setiap elemen dalam kedua fase.

<html>
 
<body>
   
<div id="A">
     
<div id="B">
       
<div id="C"></div>
     
</div>
   
</div>
 
</body>
</html>
document.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on document in capturing phase');
 
},
 
true,
);
// document.documentElement == <html>
document
.documentElement.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on <html> in capturing phase');
 
},
 
true,
);
document
.body.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on <body> in capturing phase');
 
},
 
true,
);
document
.getElementById('A').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #A in capturing phase');
 
},
 
true,
);
document
.getElementById('B').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #B in capturing phase');
 
},
 
true,
);
document
.getElementById('C').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #C in capturing phase');
 
},
 
true,
);

document
.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on document in bubbling phase');
 
},
 
false,
);
// document.documentElement == <html>
document
.documentElement.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on <html> in bubbling phase');
 
},
 
false,
);
document
.body.addEventListener(
 
'click',
 
function (e) {
    console
.log('click on <body> in bubbling phase');
 
},
 
false,
);
document
.getElementById('A').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #A in bubbling phase');
 
},
 
false,
);
document
.getElementById('B').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #B in bubbling phase');
 
},
 
false,
);
document
.getElementById('C').addEventListener(
 
'click',
 
function (e) {
    console
.log('click on #C in bubbling phase');
 
},
 
false,
);

Output konsol akan bergantung pada elemen yang Anda klik. Jika Anda mengklik elemen "terdalam" dalam hierarki DOM (elemen #C), Anda akan melihat setiap pengendali peristiwa ini diaktifkan. Dengan sedikit gaya CSS untuk memperjelas elemen mana yang mana, berikut adalah elemen #C output konsol (dengan screenshot juga):

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"

Anda dapat memainkannya secara interaktif di demo langsung di bawah. Klik elemen #C dan amati output konsol.

event.stopPropagation()

Dengan pemahaman tentang asal peristiwa dan cara peristiwa berpindah (yaitu menyebar) melalui DOM baik dalam fase pengambilan maupun fase bubbling, sekarang kita dapat mengalihkan perhatian ke event.stopPropagation().

Metode stopPropagation() dapat dipanggil pada (sebagian besar) peristiwa DOM native. Saya mengatakan "sebagian besar" karena ada beberapa yang memanggil metode ini tidak akan melakukan apa pun (karena peristiwa tidak di-propagate untuk memulai). Peristiwa seperti focus, blur, load, scroll, dan beberapa lainnya termasuk dalam kategori ini. Anda dapat memanggil stopPropagation(), tetapi tidak akan terjadi hal yang menarik, karena peristiwa ini tidak di-propagate.

Namun, apa fungsi stopPropagation?

Fungsinya, pada dasarnya, sama seperti namanya. Saat Anda memanggilnya, peristiwa akan berhenti menyebar ke elemen mana pun yang akan ditujunya. Hal ini berlaku untuk kedua arah (perekaman dan bubble). Jadi, jika Anda memanggil stopPropagation() di mana saja dalam fase pengambilan, peristiwa tidak akan pernah sampai ke fase target atau fase bubbling. Jika Anda memanggilnya dalam fase bubbling, kode tersebut akan telah melalui fase pengambilan, tetapi akan berhenti "bubbling up" dari titik saat Anda memanggilnya.

Kembali ke markup contoh yang sama, menurut Anda apa yang akan terjadi, jika kita memanggil stopPropagation() dalam fase pengambilan di elemen #B?

Tindakan ini akan menghasilkan output berikut:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"

Anda dapat memainkannya secara interaktif di demo langsung di bawah. Klik elemen #C di demo langsung dan amati output konsol.

Bagaimana dengan menghentikan penyebaran di #A pada fase bubbling? Tindakan ini akan menghasilkan output berikut:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"

Anda dapat memainkannya secara interaktif di demo langsung di bawah. Klik elemen #C di demo langsung dan amati output konsol.

Satu lagi, hanya untuk bersenang-senang. Apa yang terjadi jika kita memanggil stopPropagation() di fase target untuk #C? Ingat bahwa "fase target" adalah nama yang diberikan untuk jangka waktu saat peristiwa di targetnya. Tindakan ini akan menghasilkan output berikut:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"

Perhatikan bahwa pengendali peristiwa untuk #C tempat kita mencatat "klik pada #C dalam fase pengambilan" masih dijalankan, tetapi pengendali peristiwa tempat kita mencatat "klik pada #C dalam fase bubbling" tidak. Hal ini seharusnya sangat masuk akal. Kita memanggil stopPropagation() dari yang pertama, jadi itulah titik saat penyebaran peristiwa akan dihentikan.

Anda dapat memainkannya secara interaktif di demo langsung di bawah. Klik elemen #C di demo langsung dan amati output konsol.

Dalam demo langsung ini, sebaiknya Anda mencobanya. Coba klik elemen #A saja atau elemen body saja. Coba prediksi apa yang akan terjadi, lalu amati apakah Anda benar. Pada saat ini, Anda seharusnya dapat memprediksi dengan cukup akurat.

event.stopImmediatePropagation()

Apa metode aneh dan jarang digunakan ini? Ini mirip dengan stopPropagation, tetapi bukan menghentikan peristiwa agar tidak berpindah ke turunan (menangkap) atau ancestor (bubbling), metode ini hanya berlaku jika Anda memiliki lebih dari satu pengendali peristiwa yang terhubung ke satu elemen. Karena addEventListener() mendukung gaya peristiwa multicast, Anda dapat menghubungkan pengelola peristiwa ke satu elemen lebih dari sekali. Jika hal ini terjadi, (di sebagian besar browser), pengendali peristiwa akan dijalankan sesuai urutan penyambungannya. Memanggil stopImmediatePropagation() akan mencegah pengendali berikutnya diaktifkan. Perhatikan contoh berikut:

<html>
 
<body>
   
<div id="A">I am the #A element</div>
 
</body>
</html>
document.getElementById('A').addEventListener(
 
'click',
 
function (e) {
    console
.log('When #A is clicked, I shall run first!');
 
},
 
false,
);

document
.getElementById('A').addEventListener(
 
'click',
 
function (e) {
    console
.log('When #A is clicked, I shall run second!');
    e
.stopImmediatePropagation();
 
},
 
false,
);

document
.getElementById('A').addEventListener(
 
'click',
 
function (e) {
    console
.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
 
},
 
false,
);

Contoh di atas akan menghasilkan output konsol berikut:

"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"

Perhatikan bahwa pengendali peristiwa ketiga tidak pernah berjalan karena pengendali peristiwa kedua memanggil e.stopImmediatePropagation(). Jika kita memanggil e.stopPropagation(), pengendali ketiga akan tetap berjalan.

event.preventDefault()

Jika stopPropagation() mencegah peristiwa berjalan "ke bawah" (membuat rekaman) atau "ke atas" (membuat gelembung), apa yang dilakukan preventDefault()? Sepertinya keduanya melakukan hal yang sama. Apakah begitu?

Tidak juga. Meskipun sering kali tertukar, keduanya sebenarnya tidak memiliki banyak hubungan satu sama lain. Saat Anda melihat preventDefault(), tambahkan kata "tindakan" di dalam kepala Anda. Pikirkan "cegah tindakan default".

Dan apa tindakan default yang dapat Anda minta? Sayangnya, jawabannya tidak terlalu jelas karena sangat bergantung pada kombinasi elemen + peristiwa yang dimaksud. Dan untuk membuat masalah menjadi lebih membingungkan, terkadang tidak ada tindakan default sama sekali.

Mari kita mulai dengan contoh yang sangat sederhana untuk dipahami. Apa yang Anda harapkan akan terjadi saat mengklik link di halaman web? Tentu saja, Anda mengharapkan browser membuka URL yang ditentukan oleh link tersebut. Dalam hal ini, elemen adalah tag anchor dan peristiwanya adalah peristiwa klik. Kombinasi tersebut (<a> + click) memiliki "tindakan default" untuk membuka href link. Bagaimana jika Anda ingin mencegah browser melakukan tindakan default tersebut? Artinya, misalnya Anda ingin mencegah browser membuka URL yang ditentukan oleh atribut href elemen <a>? Inilah yang akan dilakukan preventDefault() untuk Anda. Perhatikan contoh ini:

<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
 
'click',
 
function (e) {
    e
.preventDefault();
    console
.log('Maybe we should just play some of their music right here instead?');
 
},
 
false,
);

Anda dapat memainkannya secara interaktif di demo langsung di bawah. Klik link The Avett Brothers dan amati output konsol (dan fakta bahwa Anda tidak dialihkan ke situs Avett Brothers).

Biasanya, mengklik link berlabel The Avett Brothers akan membuka www.theavettbrothers.com. Namun, dalam hal ini, kita telah menghubungkan pengendali peristiwa klik ke elemen <a> dan menentukan bahwa tindakan default harus dicegah. Jadi, saat pengguna mengklik link ini, mereka tidak akan diarahkan ke mana pun, dan sebagai gantinya konsol hanya akan mencatat "Mungkin sebaiknya kita putar beberapa musiknya di sini?"

Kombinasi elemen/peristiwa lain apa yang memungkinkan Anda mencegah tindakan default? Saya tidak mungkin mencantumkan semuanya, dan terkadang Anda harus bereksperimen untuk melihatnya. Namun, secara singkat, berikut adalah beberapa di antaranya:

  • Elemen <form> + peristiwa "kirim": preventDefault() untuk kombinasi ini akan mencegah pengiriman formulir. Hal ini berguna jika Anda ingin melakukan validasi dan jika ada yang gagal, Anda dapat memanggil preventDefault secara kondisional untuk menghentikan pengiriman formulir.

  • Elemen <a> + peristiwa "click": preventDefault() untuk kombinasi ini mencegah browser membuka URL yang ditentukan dalam atribut href elemen <a>.

  • Peristiwa document + "mousewheel": preventDefault() untuk kombinasi ini mencegah scroll halaman dengan roda mouse (scrolling dengan keyboard tetap berfungsi).
    ↜ Tindakan ini memerlukan panggilan addEventListener() dengan { passive: false }.

  • Peristiwa document + "keydown": preventDefault() untuk kombinasi ini sangat berbahaya. Hal ini membuat halaman tidak berguna, sehingga mencegah scroll keyboard, penggunaan tombol tab, dan penyorotan keyboard.

  • Peristiwa document + "mousedown": preventDefault() untuk kombinasi ini akan mencegah teks ditandai dengan mouse dan tindakan "default" lainnya yang akan dipanggil dengan mouse ditekan.

  • Elemen <input> + peristiwa "keypress": preventDefault() untuk kombinasi ini akan mencegah karakter yang diketik oleh pengguna mencapai elemen input (tetapi jangan lakukan ini; jarang ada alasan yang valid untuk melakukannya).

  • Peristiwa document + "contextmenu": preventDefault() untuk kombinasi ini mencegah menu konteks browser native muncul saat pengguna mengklik kanan atau menekan lama (atau cara lain yang dapat membuat menu konteks muncul).

Ini bukanlah daftar lengkap, tetapi semoga daftar ini memberi Anda gambaran yang baik tentang cara preventDefault() dapat digunakan.

Lelucon praktis yang menyenangkan?

Apa yang terjadi jika Anda stopPropagation() dan preventDefault() dalam fase pengambilan, dimulai dari dokumen? Kelucuan pun terjadi. Cuplikan kode berikut akan merender halaman web apa pun yang hampir tidak berguna sama sekali:

function preventEverything(e) {
  e
.preventDefault();
  e
.stopPropagation();
  e
.stopImmediatePropagation();
}

document
.addEventListener('click', preventEverything, true);
document
.addEventListener('keydown', preventEverything, true);
document
.addEventListener('mousedown', preventEverything, true);
document
.addEventListener('contextmenu', preventEverything, true);
document
.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });

Saya tidak begitu tahu mengapa Anda ingin melakukannya (kecuali mungkin untuk mengerjai seseorang), tetapi sebaiknya pikirkan apa yang terjadi di sini, dan pahami mengapa hal itu menciptakan situasi seperti itu.

Semua peristiwa berasal dari window, jadi dalam cuplikan ini, kita akan menghentikan semua peristiwa click, keydown, mousedown, contextmenu, dan mousewheel agar tidak pernah sampai ke elemen apa pun yang mungkin memprosesnya. Kita juga memanggil stopImmediatePropagation sehingga pengendali apa pun yang terhubung ke dokumen setelah ini, juga akan digagalkan.

Perhatikan bahwa stopPropagation() dan stopImmediatePropagation() bukan (setidaknya tidak sebagian besar) yang membuat halaman tidak berguna. Peristiwa tersebut hanya mencegah peristiwa agar tidak sampai ke tempat yang seharusnya.

Namun, kita juga memanggil preventDefault(), yang akan Anda ingat mencegah tindakan default. Jadi, semua tindakan default (seperti scroll mousewheel, scroll keyboard atau sorotan atau tab, klik link, tampilan menu konteks, dll.) semuanya dicegah, sehingga halaman berada dalam status yang cukup tidak berguna.

Demo langsung

Untuk menjelajahi semua contoh dari artikel ini lagi di satu tempat, lihat demo tersemat di bawah.

Ucapan terima kasih

Gambar hero oleh Tom Wilson di Unsplash.