preventDefault
dan stopPropagation
: kapan harus menggunakan yang mana dan apa yang sebenarnya dilakukan setiap metode.
Event.stopPropagation() dan Event.preventDefault()
Penanganan peristiwa JavaScript sering kali mudah dilakukan. Hal ini berlaku terutama ketika menangani
struktur HTML yang sederhana (relatif datar). Ketika peristiwa berpindah (atau menyebar) melalui hierarki elemen, situasi akan menjadi sedikit lebih terlibat. Hal ini biasanya terjadi saat developer membuka
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," artikel ini untuk Anda. Saya akan menjelaskan fungsi dari setiap metode, kapan harus menggunakannya, dan memberi Anda berbagai contoh praktis untuk Anda pelajari. Tujuan saya adalah mengakhiri kebingungan Anda untuk selamanya.
Sebelum kita membahas lebih dalam, sebaiknya kita bahas dua jenis penanganan peristiwa yang dapat dilakukan di JavaScript (di semua browser modern, yaitu—Internet Explorer sebelum versi 9 tidak mendukung perekaman peristiwa sama sekali).
Gaya eventing (menangkap dan menggelembung)
Semua browser modern mendukung pengambilan peristiwa, tetapi sangat jarang digunakan oleh developer.
Menariknya, ini adalah satu-satunya bentuk acara yang awalnya didukung Netscape. Pesaing terbesar Netscape, Microsoft Internet Explorer, tidak mendukung perekaman peristiwa sama sekali, melainkan hanya mendukung gaya eventing lain yang disebut gelembung peristiwa. Saat W3C dibentuk, mereka menemukan manfaat
dalam kedua gaya eventing dan mendeklarasikan bahwa browser harus mendukung keduanya, melalui parameter ketiga ke
metode addEventListener
. Awalnya, parameter tersebut hanya berupa boolean, tetapi semua browser modern mendukung objek options
sebagai parameter ketiga, yang dapat Anda gunakan untuk menentukan (antara lain) apakah Anda ingin menggunakan pengambilan peristiwa atau tidak:
someElement.addEventListener('click', myClickHandler, { capture: true | false });
Perhatikan bahwa objek options
bersifat opsional, seperti halnya properti capture
. Jika salah satunya dihilangkan, nilai default untuk capture
adalah false
, yang berarti gelembung peristiwa akan digunakan.
Perekaman peristiwa
Apa artinya jika pengendali peristiwa Anda "mendengarkan dalam fase pengambilan?" Untuk memahami hal ini, kita perlu mengetahui bagaimana peristiwa berasal dan bagaimana peristiwa itu terjadi. Hal berikut berlaku untuk semua peristiwa, meskipun Anda, sebagai developer, tidak memanfaatkannya, memperhatikannya, atau memikirkannya.
Semua peristiwa dimulai di jendela dan pertama-tama melalui fase perekaman. Artinya, saat dikirim, peristiwa akan memulai jendela dan bergerak "ke bawah" menuju elemen target terlebih dahulu. Hal ini terjadi meskipun Anda hanya mendengarkan dalam fase gelembung. 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 menyebar melalui turunannya seperti 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 proses menuju targetnya). Peristiwa masih
berasal dari window
dan memulai perjalanannya seperti yang baru saja dijelaskan.
Dalam contoh kita, peristiwa klik kemudian akan menyebarkan (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 demikian, pengendali peristiwa yang sesuai akan diaktifkan. Dalam contoh kita, tidak ada yang ada, jadi tidak ada pengendali yang akan diaktifkan.
Selanjutnya, peristiwa tersebut akan disebarkan ke document
dan browser akan bertanya: "Apakah ada yang memproses
peristiwa klik pada 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 demikian, 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 menyebarkan ke elemen #A
. Sekali lagi, browser akan bertanya: "Apakah ada
yang memproses peristiwa klik pada #A
dalam fase pengambilan dan jika demikian, pengendali peristiwa yang sesuai
akan diaktifkan.
Selanjutnya, peristiwa akan diterapkan 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 ini ketika peristiwa pada target, dikenal sebagai "fase target". Pada tahap ini, pengendali peristiwa akan diaktifkan, browser akan console.log "#C was clicks" dan kemudian kita selesai, bukan?
Salah. Kita belum selesai sama sekali. Prosesnya berlanjut, tetapi sekarang berubah menjadi fase gelembung.
Gelembung acara
Browser akan menanyakan:
"Apakah ada yang memproses peristiwa klik pada #C
dalam fase gelembung?" Harap perhatikan.
Memproses klik (atau jenis peristiwa apa pun) dapat dilakukan sepenuhnya dalam kedua fase perekaman dan
bubble. 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
), ya, kedua pengendali peristiwa akan benar-benar diaktifkan untuk elemen yang sama. Namun, penting juga untuk dicatat bahwa
mereka menembak dalam fase yang berbeda (satu dalam fase pengambilan dan satu dalam fase bergelembung).
Selanjutnya, peristiwa akan menyebarkan (lebih sering dinyatakan sebagai "gelembung" karena seolah-olah
peristiwa tersebut bergerak "ke atas" hierarki DOM) ke elemen induknya, #B
, dan browser akan bertanya: "Apakah
ada yang memproses peristiwa klik di #B
dalam fase gelembung?" Dalam contoh kita, tidak ada, jadi
tidak ada pengendali yang akan diaktifkan.
Selanjutnya, peristiwa akan ditampilkan sebagai balon ke #A
dan browser akan menanyakan: "Apakah ada yang memproses peristiwa
klik pada #A
dalam fase gelembung?"
Selanjutnya, peristiwa akan dijadikan balon ke <body>
: "Apakah ada yang memproses peristiwa klik pada elemen <body>
dalam fase gelembung?"
Berikutnya, elemen <html>
: "Apakah ada yang memproses peristiwa klik pada elemen <html>
dalam fase balon?
Berikutnya, document
: "Apakah ada yang memproses peristiwa klik pada document
dalam fase
gelembung?"
Terakhir, window
: "Apakah ada yang memproses peristiwa klik pada jendela dalam fase gelembung?"
Fiuh! Itu adalah perjalanan yang panjang, dan acara kita mungkin sangat lelah saat ini, tetapi percaya atau tidak, itulah perjalanan yang harus dilalui setiap peristiwa! Sering kali, hal ini tidak pernah diperhatikan karena developer biasanya hanya tertarik pada satu fase peristiwa atau fase lainnya (dan biasanya fase menggembung).
Sebaiknya luangkan waktu untuk bermain-main dengan perekaman peristiwa, dan balon peristiwa serta logging beberapa catatan ke konsol saat pengendali diaktifkan. Sangat bermanfaat sekali untuk melihat jalur yang diambil oleh suatu 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"
di hierarki DOM (elemen #C
), Anda akan melihat setiap pengendali peristiwa ini
aktif. Dengan sedikit gaya visual CSS untuk memperjelas elemen mana yang dimaksud, berikut adalah elemen #C
output konsol (dengan screenshot):
"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 dalam demo langsung di bawah ini. Klik elemen #C
dan amati output konsolnya.
event.stopPropagation()
Dengan memahami dari mana peristiwa berasal dan bagaimana peristiwa itu berpindah (yaitu penyebaran) melalui DOM pada fase pengambilan dan fase gelembung, kita sekarang dapat mengalihkan perhatian kita ke event.stopPropagation()
.
Metode stopPropagation()
dapat dipanggil di (sebagian besar) peristiwa DOM native. Saya mengatakan "sebagian besar" karena ada beberapa panggilan yang memanggil metode ini tidak akan melakukan apa pun (karena peristiwa tidak disebarkan dari awal). Peristiwa seperti focus
, blur
, load
, scroll
, dan beberapa peristiwa lainnya termasuk dalam kategori ini. Anda dapat memanggil stopPropagation()
, tetapi tidak ada hal menarik yang akan terjadi karena peristiwa ini tidak disebarkan.
Namun, apa fungsi stopPropagation
?
Yaitu, hampir, persis seperti katanya. Saat Anda memanggilnya, peristiwa tersebut akan berhenti
menyebarkan ke elemen mana pun yang seharusnya menjadi tujuan perjalanan tersebut. Ini berlaku untuk kedua arah
(menangkap dan menggelembung). Jadi, jika Anda memanggil stopPropagation()
di mana pun dalam fase pengambilan,
peristiwa tidak akan pernah mencapai fase target atau fase balon. Jika Anda memanggilnya dalam fase
gelembung, fungsi ini telah melalui fase pengambilan, tetapi akan berhenti "gelembung naik" dari
titik saat Anda memanggilnya.
Dengan kembali ke contoh markup yang sama, menurut Anda apa yang akan terjadi jika kita memanggil stopPropagation()
dalam fase pengambilan di elemen #B
?
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 dalam demo langsung di bawah ini. Klik elemen #C
dalam demo langsung dan amati output konsolnya.
Bagaimana cara menghentikan propagasi pada #A
dalam fase gelembung? 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 dalam demo langsung di bawah ini. Klik elemen #C
dalam demo langsung dan amati output konsolnya.
Satu lagi, untuk bersenang-senang. Apa yang terjadi jika kita memanggil stopPropagation()
dalam fase target untuk #C
?
Ingat kembali bahwa "fase target" adalah nama yang diberikan pada periode waktu saat peristiwa berada di targetnya. 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
yang kita gunakan untuk mencatat "klik #C dalam fase pengambilan" ke dalam log masih
dijalankan, tetapi pengendali yang mencatat "klik pada #C dalam fase gelembung" tidak dicatat. Ini seharusnya
masuk akal. Kita memanggil stopPropagation()
dari yang pertama, sehingga merupakan titik saat
propagasi peristiwa akan berhenti.
Anda dapat memainkannya secara interaktif dalam demo langsung di bawah ini. Klik elemen #C
dalam demo langsung dan amati output konsolnya.
Dalam salah satu demo langsung ini, kami mendorong Anda untuk bermain-main. Coba klik hanya elemen #A
atau
elemen body
saja. Cobalah untuk memprediksi apa yang akan terjadi dan kemudian amati apakah Anda benar. Pada
titik ini, Anda seharusnya dapat memprediksi dengan cukup akurat.
event.stopImmediatePropagation()
Apa metode yang aneh, dan tidak sering digunakan ini? Metode ini mirip dengan stopPropagation
, tetapi bukannya menghentikan peristiwa agar tidak berpindah ke turunan (menangkap) atau ancestor (bergelembung), metode ini hanya berlaku saat Anda memiliki lebih dari satu pengendali peristiwa yang dihubungkan ke satu elemen. Karena addEventListener()
mendukung gaya peristiwa multicast, Anda dapat menghubungkan pengendali peristiwa ke satu elemen lebih dari sekali. Jika hal ini terjadi, (di sebagian besar browser), pengendali peristiwa akan dieksekusi 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, pengendali peristiwa ketiga tidak pernah berjalan karena pengendali peristiwa kedua memanggil e.stopImmediatePropagation()
. Jika kami telah memanggil e.stopPropagation()
, pengendali ketiga akan tetap berjalan.
event.preventDefault()
Jika stopPropagation()
mencegah peristiwa "ke bawah" (menangkap) atau "ke atas"
(bergelembung), apa yang dilakukan preventDefault()
? Sepertinya ada hal serupa. Oke?
Tidak juga. Meskipun keduanya sering bingung, sebenarnya mereka tidak banyak berhubungan dengan satu sama lain.
Saat melihat preventDefault()
di kepala Anda, tambahkan kata "tindakan". Pikirkan "cegah
tindakan {i>default<i}."
Dan apa tindakan default yang mungkin Anda tanyakan? Sayangnya, jawabannya tidak begitu jelas karena sangat bergantung pada kombinasi elemen + peristiwa yang dimaksud. Dan untuk membuat masalahnya semakin membingungkan, terkadang tidak ada tindakan default sama sekali.
Mari kita mulai dengan contoh yang
sangat sederhana untuk dipahami. Apa yang diharapkan akan terjadi ketika
Anda mengklik link di halaman web? Tentu saja Anda berharap browser akan membuka URL yang ditentukan oleh link tersebut.
Dalam hal ini, elemennya 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, Anda ingin mencegah browser
membuka URL yang ditentukan oleh atribut href
elemen <a>
? Inilah yang akan
preventDefault()
lakukan untuk Anda. Perhatikan contoh berikut:
<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 dalam demo langsung di bawah ini. Klik link The Avett Brothers dan amati output konsol (serta fakta bahwa Anda tidak dialihkan ke situs Avett Brothers).
Biasanya, mengklik link berlabel The Avett Brothers akan menghasilkan penjelajahan ke
www.theavettbrothers.com
. Namun, dalam kasus ini, kami telah menghubungkan pengendali peristiwa klik ke elemen <a>
dan menetapkan 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 kita harus memutar musik mereka di sini saja?"
Kombinasi elemen/peristiwa lain apa yang memungkinkan Anda mencegah tindakan default? Saya tidak mungkin membuat daftar semuanya, dan terkadang Anda hanya perlu bereksperimen untuk melihatnya. Namun secara singkat, berikut ini beberapa di antaranya:
Elemen
<form>
+ peristiwa "submit":preventDefault()
untuk kombinasi ini akan mencegah pengiriman formulir. Tindakan ini berguna jika Anda ingin melakukan validasi dan jika terjadi kegagalan, Anda dapat memanggil preventDefault secara bersyarat untuk menghentikan pengiriman formulir.Elemen
<a>
+ peristiwa "klik":preventDefault()
untuk kombinasi ini mencegah browser membuka URL yang ditentukan dalam atribut href elemen<a>
.document
+ peristiwa "mousewheel":preventDefault()
untuk kombinasi ini mencegah scroll halaman dengan roda mouse (men-scroll dengan keyboard akan tetap berfungsi).
↜ Tindakan ini memerlukan pemanggilanaddEventListener()
dengan{ passive: false }
.document
+ peristiwa "keydown":preventDefault()
untuk kombinasi ini bersifat mematikan. Tindakan ini membuat halaman menjadi sangat tidak berguna, sehingga mencegah scroll keyboard, tombol tab, dan penyorotan keyboard.document
+ peristiwa "mousedown":preventDefault()
untuk kombinasi ini akan mencegah teks disorot dengan mouse dan tindakan "default" lainnya yang akan dipanggil dengan mengarahkan mouse ke bawah.Elemen
<input>
+ peristiwa "tekan":preventDefault()
untuk kombinasi ini akan mencegah karakter yang diketik oleh pengguna mencapai elemen input (tetapi jangan lakukan ini; jarang, jika pernah, ada alasan yang valid untuk hal tersebut).document
+ peristiwa "contextmenu":preventDefault()
untuk kombinasi ini mencegah menu konteks browser native muncul saat pengguna mengklik kanan atau menekan lama (atau dengan cara lain yang memunculkan menu konteks).
Ini bukanlah daftar lengkap, tetapi semoga dapat memberi Anda gambaran yang baik tentang cara
penggunaan preventDefault()
.
Lelucon praktis yang menyenangkan?
Apa yang terjadi jika Anda melakukan stopPropagation()
dan preventDefault()
dalam fase pengambilan, mulai dari dokumen? Kegembiraan pun terjadi! Cuplikan kode berikut akan merender halaman web apa pun yang hampir
sama sekali tidak berguna:
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 tahu mengapa Anda ingin melakukan ini (kecuali mungkin untuk bercanda dengan seseorang), tetapi ada baiknya untuk memikirkan apa yang terjadi di sini, dan menyadari mengapa hal itu menciptakan situasi yang terjadi.
Semua peristiwa berasal dari window
, sehingga dalam cuplikan ini, kita menghentikan, mati di jalurnya, semua
peristiwa click
, keydown
, mousedown
, contextmenu
, dan mousewheel
agar tidak pernah terhubung ke elemen
apa pun yang mungkin memprosesnya. Kita juga memanggil stopImmediatePropagation
sehingga setiap pengendali yang dihubungkan ke dokumen setelah ini juga akan digagalkan.
Perhatikan bahwa stopPropagation()
dan stopImmediatePropagation()
bukan (setidaknya tidak sebagian besar) yang
merender halaman menjadi tidak berguna. Fungsi ini hanya mencegah peristiwa masuk ke tempat yang seharusnya.
Namun, kita juga memanggil preventDefault()
, yang akan Anda ingat mencegah tindakan default. Jadi,
tindakan default apa pun (seperti scroll roda mouse, scroll keyboard, atau penandaan atau tab, mengklik link,
tampilan menu konteks, dll.) semuanya akan dicegah, sehingga membuat halaman dalam keadaan yang cukup tidak berguna.
Demo langsung
Untuk mempelajari lagi semua contoh dari artikel ini di satu tempat, lihat demo tersemat di bawah.
Ucapan terima kasih
Banner besar oleh Tom Wilson di Unsplash.