Belajar dengan membaca secara berurutan

Event — addEventListener dan Propagation

Menangani klik dengan addEventListener dan event.target, mendelegasikannya lewat bubbling, serta memakai stopPropagation, once, dan removeEventListener.

Assignment ke textContent hanya mengubah element sekali, yaitu saat baris itu dijalankan. Kode yang menambah jumlah setiap kali tombol diklik tidak bisa ditulis hanya dengan baris-baris yang berjalan dari atas ke bawah, karena kapan tombol akan diklik tidak bisa diketahui dari kodenya.

Artikel ini membahas addEventListener, yang memanggil function saat terjadi klik, dan cara menangani klik pada child element dari parent element-nya.

Memanggil Function Saat Diklik — addEventListener

Misalkan tombol "Tambah ke keranjang" di halaman produk harus menambah jumlah barang di keranjang sebanyak 1 dan menampilkannya setiap kali diklik. Kalau baris yang mengubah jumlah itu kamu tulis begitu saja, baris itu hanya berjalan sekali saat halaman dibuka, dan jumlahnya tidak berubah saat tombol diklik.

Untuk memanggil function saat event (kejadian di halaman, seperti klik atau input) terjadi, daftarkan nama event dan function-nya dengan addEventListener("click", function). Function yang didaftarkan ini disebut event listener (selanjutnya disebut listener). Di console kamu tidak bisa mengklik dengan mouse, jadi klik dipicu dengan method click, seperti cartButton.click().

document.body.innerHTML = `<button id="add-to-cart">Tambah ke keranjang</button><p>Keranjang: <span id="cart-count">0</span> barang</p>`;
const cartButton = document.querySelector("#add-to-cart");
const cartCount = document.querySelector("#cart-count");

// Function yang dipanggil saat diklik (di sini baru didefinisikan)
function addToCart() {
  cartCount.textContent = Number(cartCount.textContent) + 1;
}

// Berikan function-nya sendiri untuk mendaftarkannya (addToCart tidak dipanggil di baris ini)
cartButton.addEventListener("click", addToCart);
console.log(cartCount.textContent);   // 0

// Setiap kali klik dipicu, addToCart yang didaftarkan dipanggil
cartButton.click();
cartButton.click();
console.log(cartCount.textContent);   // 2
Baris Tempat Isi addToCart Berjalan
addEventListener("click", addToCart)cartButton.click()(ke-1)cartButton.click()(ke-2)Hanya mendaftar;jumlah tetap 0addToCart jalan;jumlah jadi 1Dipanggil lagi;jumlah jadi 2
Baris pendaftaran tidak memanggil addToCart, jadi jumlahnya tetap 0. Isi function berjalan di baris tempat klik terjadi.

Di halaman sungguhan, kamu tidak perlu menulis click(): browser memanggil addToCart begitu pengguna mengklik tombolnya. Yang ditanggapi hanya klik yang terjadi setelah pendaftaran, jadi klik sebelum baris pendaftaran tidak ikut dihitung.

Dengan (), Function Berjalan Sekali di Baris Pendaftaran

Kalau kamu menulis addEventListener("click", addToCart()), addToCart dipanggil sekali di baris pendaftaran, dan yang diberikan adalah return value-nya, yaitu undefined. Tidak ada error yang muncul, jadi sulit menemukan kenapa jumlahnya tidak bertambah saat diklik. Saat mendaftarkan, berikan nama function-nya saja tanpa ().

Di halaman penulis, ubah tampilan tombol follow setiap kali diklik. Tombol followButton sudah dideklarasikan.

① Daftarkan function yang, setiap kali diklik, men-toggle class active dan menukar teksnya antara "Ikuti" dan "Mengikuti".

② Picu klik sekali, lalu tampilkan HTML tombol, termasuk tag-nya.

③ Picu klik sekali lagi, lalu tampilkan HTML tombol.

(Kalau berjalan dengan benar, penjelasan akan muncul.)

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Menangani Klik Child di Parent Element — Bubbling dan Delegation

Berikutnya, saat sebuah baris di daftar pesanan diklik, kamu ingin membaca nomor pesanan di baris itu. Kalau baris-barisnya kamu kumpulkan dengan querySelectorAll lalu kamu pasangi listener satu per satu dengan forEach, hanya baris yang ada di halaman saat itu yang mendapat listener. Baris yang ditambahkan belakangan dengan append tidak bereaksi apa-apa saat diklik.

Karena ada bubbling (mekanisme ketika event yang terjadi di sebuah element diteruskan ke parent element-nya, lalu ke parent dari parent itu, dan seterusnya), listener yang didaftarkan di ul sebagai parent juga dipanggil saat li diklik. Browser memberikan event object (object yang berisi informasi tentang event yang terjadi) ke listener sebagai argument. Di kode di bawah, argument itu diberi nama event, dan target-nya menunjuk element tempat klik itu awalnya terjadi.

document.body.innerHTML = `<section id="order-panel"><ul id="order-list"><li data-id="A-101">Pesanan A-101</li><li data-id="A-102">Pesanan A-102</li></ul></section>`;
const orderList = document.querySelector("#order-list");
const orderPanel = document.querySelector("#order-panel");

// Daftarkan di ul sebagai parent dan di section yang membungkusnya, bukan di li
orderList.addEventListener("click", (event) => {
  console.log(`Diterima di ul: ${event.target.dataset.id}`);
});
orderPanel.addEventListener("click", (event) => {
  console.log(`Diterima di section: ${event.target.dataset.id}`);
});

// Picu klik di li A-102
document.querySelector('[data-id="A-102"]').click();
// Diterima di ul: A-102
// Diterima di section: A-102
Jalur Klik pada A-102
Element section — listener-nya dipanggil kedua
  • event.target.dataset.id adalah A-102
Element ul — listener-nya dipanggil pertama
  • event.target.dataset.id adalah A-102
li A-101 — tidak diklik
  • Klik tidak diteruskan ke li yang sejajar
li A-102 — element tempat click() dipanggil
  • event.target adalah li ini
  • Dari sini klik mulai diteruskan ke luar
Klik diteruskan dari li ke ul, lalu ke section, sedangkan li di sebelahnya tidak ikut menerimanya. Di mana pun dibaca, event.target adalah li A-102.

Cara mendaftarkan hanya 1 listener di parent element lalu memakai event.target untuk mengenali child yang diklik disebut event delegation. Klik pada li ditangani oleh ul setelah diteruskan ke sana, jadi listener yang sama tetap dipanggil untuk li yang ditambahkan dengan append setelah pendaftaran. event.target adalah element paling dalam yang diklik, jadi kalau di dalam li ada span, yang ditunjuk adalah span itu.

Di layar pemesanan ruang rapat, tangani slot waktu yang diklik di daftar. Element ul untuk daftar slot waktu, slotList, sudah dideklarasikan.

① Daftarkan hanya 1 listener di daftar itu yang menampilkan jam slot yang diklik dan menambahkan class selected ke slot tersebut.

② Picu klik pada slot 10:00.

③ Buat slot 16:00, tambahkan ke akhir daftar, lalu picu klik padanya.

④ Tampilkan HTML daftar, termasuk tag ul.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Menahan Klik Agar Tidak Sampai ke Parent — stopPropagation

Bayangkan baris pesanan yang membuka detailnya saat diklik, dan di dalamnya ada tombol "Hapus" untuk menghapus pesanan itu. Tombol itu adalah child dari baris, jadi klik pada "Hapus" juga diteruskan ke baris lewat bubbling, dan detail pesanan ikut terbuka padahal pesanannya sedang dihapus.

Event yang diteruskan ke element di luarnya disebut propagation. Untuk menghentikannya, panggil stopPropagation (method yang menghentikan propagation di element yang listener-nya sedang berjalan) di dalam listener tombol, seperti event.stopPropagation(). Setelah dihentikan, listener yang didaftarkan di element luar, seperti baris atau daftar, tidak dipanggil.

document.body.innerHTML = `<ul><li id="order-row">Pesanan A-201 <button id="favorite">Favorit</button><button id="delete">Hapus</button></li></ul>`;
const orderRow = document.querySelector("#order-row");
const favoriteButton = document.querySelector("#favorite");
const deleteButton = document.querySelector("#delete");

// Klik pada baris membuka detailnya
orderRow.addEventListener("click", () => console.log("Buka detail"));

// Favorit: detail boleh ikut terbuka, jadi klik dibiarkan sampai ke baris
favoriteButton.addEventListener("click", () => console.log("Ditambahkan ke favorit"));
favoriteButton.click();   // Ditambahkan ke favorit, Buka detail (2 baris)

// Hapus: panggil stopPropagation() supaya klik tidak sampai ke baris
deleteButton.addEventListener("click", (event) => {
  event.stopPropagation();
  console.log("Pesanan dihapus");
});
deleteButton.click();     // Pesanan dihapus
Klik yang Diteruskan vs. Klik yang Dihentikan
favoriteButton.click()"Ditambahkan kefavorit" tampilTidak dihentikan,jadi sampai ke li"Buka detail"juga tampildeleteButton.click()stopPropagation(),lalu hapus pesananTidak sampaike li"Buka detail"tidak tampil
Detail boleh ikut terbuka saat Favorit diklik, jadi kliknya tidak dihentikan dan sampai ke baris. Setelah stopPropagation() dipanggil, listener baris tidak dipanggil.

Di listener hapus, "Pesanan dihapus" tetap tampil meskipun ditulis setelah stopPropagation(). Yang dihentikan adalah penerusan event ke element luar, bukan sisa baris di listener yang memanggilnya.

Klik yang Dihentikan Juga Tidak Sampai ke ul yang Memakai Delegation

stopPropagation() tidak hanya menahan event agar tidak sampai ke baris yang langsung membungkus tombol, tetapi juga ke semua element di luarnya. Meskipun ul daftar punya listener delegation yang mencatat aksi pengguna, klik pada tombol hapus tidak akan tercatat. Hentikan hanya klik yang memang tidak perlu ditangani oleh element luar mana pun.

Di daftar notifikasi aplikasi chat kantor, klik pada baris membuka notifikasi, sedangkan tombol Bisukan di dalam baris hanya membisukan notifikasi itu. noticeRow (baris notifikasi yang sudah punya listener) sudah dideklarasikan.

① Daftarkan listener di tombol Bisukan yang menampilkan "Notifikasi dibisukan" dan menahan klik agar tidak sampai ke baris.

② Picu klik pada tombol Bisukan.

③ Tampilkan HTML baris, termasuk tag li.

④ Picu klik pada baris itu sendiri.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Melepas Listener — once dan removeEventListener

Kalau pengguna mengklik tombol "Buat pesanan" 2 kali berturut-turut, listener yang didaftarkan juga dipanggil 2 kali, dan pesanan yang sama terkirim 2 kali. Setelah pengiriman pertama diterima, kamu perlu memastikan listener tidak dipanggil lagi meskipun tombol yang sama diklik.

Kalau kamu memberikan { once: true } sebagai argument ketiga addEventListener, listener otomatis dilepas setelah dipanggil sekali. Untuk melepasnya kapan pun kamu mau, panggil removeEventListener (method yang menerima nama event dan function, mencari listener terdaftar yang === dengan function itu, lalu melepasnya).

document.body.innerHTML = `<button id="confirm">Buat pesanan</button>`;
const confirmButton = document.querySelector("#confirm");
const sendOrder = () => console.log("Pesanan dikirim");

// { once: true } — otomatis dilepas setelah dipanggil sekali
confirmButton.addEventListener("click", sendOrder, { once: true });
confirmButton.click();   // Pesanan dikirim
confirmButton.click();   // (tidak ada yang tampil)

// Memberikan sendOrder yang didaftarkan akan melepas listener itu
confirmButton.addEventListener("click", sendOrder);
confirmButton.removeEventListener("click", sendOrder);
confirmButton.click();   // (tidak ada yang tampil)

// Arrow function yang baru ditulis adalah function lain meskipun isinya sama, jadi tidak ada yang dilepas
confirmButton.addEventListener("click", () => console.log("Pesanan dikirim"));
confirmButton.removeEventListener("click", () => console.log("Pesanan dikirim"));
confirmButton.click();   // Pesanan dikirim
Apakah Masih Dipanggil di Klik Berikutnya?
confirmButton:3 cara melepasDaftarkan dengan{ once: true }removeEventListener("click", sendOrder)removeEventListener("click", () => …)Dilepas otomatissetelah sekaliFunction sama,jadi dilepas=== false, jaditidak dilepasTidak dipanggil diklik berikutnyaTidak dipanggil diklik berikutnyaKlik berikutnyatetap mengirim
Dengan once, atau dengan memberikan function yang sama seperti saat didaftarkan, listener dilepas. Memberikan arrow function yang baru ditulis tidak melepasnya.

() => … membuat function baru setiap kali ditulis, jadi meskipun isinya sama, hasil perbandingannya dengan function yang didaftarkan memakai === adalah false. Simpan dulu listener yang nanti akan dilepas di variabel seperti sendOrder, lalu daftarkan variabel itu. Tabel di bawah merangkum penulisan terkait event yang dibahas di artikel ini.

Cara menulisFungsinyaPerhatikan
addEventListener("click", fn)Memanggil fn setiap klikBerikan fn tanpa ()
event.targetElement yang diklikPaling dalam, meski di parent
Listener di parent (delegation)Menangani klik semua childChild baru juga tertangani
event.stopPropagation()Menahan penerusan ke luarDelegation pun tak dipanggil
{ once: true }Melepas setelah 1 panggilanArgument ke-3 addEventListener
removeEventListener("click", fn)Melepas listenerBerikan function yang sama

Ini layar pendaftaran sebuah webinar. registerButton, waitButton, attendeeCount, dan waitingCount sudah dideklarasikan.

① Daftarkan listener di tombol daftar yang menambah jumlah peserta sebanyak 1, hanya sekali.

② Picu klik 2 kali pada tombol daftar, lalu tampilkan jumlahnya.

③ Daftarkan function yang menambah 1 orang ke daftar tunggu, lalu picu klik.

④ Lepas function dari ③, picu klik lagi, lalu tampilkan jumlahnya.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output
QUIZ

Cek Pemahaman

Jawab setiap pertanyaan satu per satu.

Soal 1Dengan saveButton.addEventListener("click", save());, kapan save dipanggil?

Soal 2Di dalam listener yang didaftarkan di ul, apa event.target saat kamu memanggil click() pada li di dalam ul itu?

Soal 3Setelah mendaftarkan () => send(), apa yang terjadi kalau kamu memanggil removeEventListener("click", () => send())?