Pembahasan mendalam tentang peristiwa JavaScript

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 pemanggilan addEventListener() 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.