preventDefault
dan stopPropagation
: kapan harus menggunakan metode mana dan apa yang sebenarnya dilakukan oleh setiap metode.
Event.stopPropagation() dan Event.preventDefault()
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 panggilanaddEventListener()
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.