Bab 2: Pengantar Event

4 menit baca

Event = sesuatu yang terjadi. Klik, ketik, scroll, load — browser memberitahu kita, dan kita bisa merespons.


2.1 Pengantar Browser Events

💡Analogi

Event itu kayak bel pintu. Seseorang menekan bel (event terjadi), dan kamu merespons dengan membuka pintu (event handler). Kamu tidak perlu terus-terusan mengintip dari jendela — bel yang memberitahu kamu.

Apa Itu Event?

Event adalah sinyal bahwa sesuatu terjadi. Contoh event:

EventKapan Terjadi
clickElemen diklik
dblclickElemen di-double-click
mouseenterMouse masuk area elemen
mouseleaveMouse keluar area elemen
keydownTombol keyboard ditekan
keyupTombol keyboard dilepas
submitForm di-submit
inputIsi input berubah
focusElemen mendapat fokus
blurElemen kehilangan fokus
loadHalaman/resource selesai dimuat
scrollHalaman/elemen di-scroll

Cara Memasang Event Handler

Cara 1: HTML attribute (JANGAN pakai ini)

html
<!-- Tidak recommended — campur HTML dan JS -->
<button onclick="alert('Diklik!')">Klik saya</button>

Cara 2: DOM property (Oke, tapi terbatas)

javascript
const btn = document.querySelector("button");
btn.onclick = function() {
  alert("Diklik!");
};

// ⚠️ Masalah: hanya bisa SATU handler per event
btn.onclick = function() {
  alert("Handler kedua"); // Yang pertama HILANG!
};
javascript
const btn = document.querySelector("button");

// Bisa banyak handler untuk satu event
btn.addEventListener("click", function() {
  alert("Handler 1");
});

btn.addEventListener("click", function() {
  alert("Handler 2"); // Keduanya jalan!
});

addEventListener Lengkap

javascript
element.addEventListener(event, handler, options);

// options (opsional):
// - once: true → handler otomatis dihapus setelah 1x jalan
// - capture: true → tangkap di fase capturing (nanti dibahas)
// - passive: true → handler tidak akan panggil preventDefault()

btn.addEventListener("click", handler, { once: true });
// Klik pertama → handler jalan. Klik kedua → tidak ada efek.

Menghapus Event Handler

javascript
function handleClick() {
  alert("Diklik!");
}

// Pasang
btn.addEventListener("click", handleClick);

// Hapus — HARUS fungsi yang SAMA (referensi sama)
btn.removeEventListener("click", handleClick);

// ⚠️ Ini TIDAK BEKERJA:
btn.addEventListener("click", function() { alert("Halo"); });
btn.removeEventListener("click", function() { alert("Halo"); }); // GAGAL! Beda referensi!

Objek Event

Saat event terjadi, browser membuat objek event berisi detail:

javascript
btn.addEventListener("click", function(event) {
  console.log(event.type);    // "click"
  console.log(event.target);  // Elemen yang diklik
  console.log(event.clientX); // Posisi X mouse
  console.log(event.clientY); // Posisi Y mouse
  console.log(event.currentTarget); // Elemen yang punya handler (= btn)
});

Handler sebagai Objek: handleEvent

javascript
// Bisa pakai objek dengan method handleEvent
const handler = {
  handleEvent(event) {
    if (event.type === "click") {
      alert("Diklik di " + event.clientX + ", " + event.clientY);
    } else if (event.type === "mouseenter") {
      event.target.style.background = "yellow";
    }
  }
};

btn.addEventListener("click", handler);
btn.addEventListener("mouseenter", handler);
⚠️Jebakan!
javascript
// 1. addEventListener("click", handler()) ← SALAH! (langsung dipanggil)
btn.addEventListener("click", handler);   // ✅ Tanpa ()

// 2. removeEventListener butuh referensi SAMA
// Simpan fungsi di variabel kalau mau bisa dihapus

// 3. this di dalam handler = elemen yang punya handler
btn.addEventListener("click", function() {
  console.log(this); // = btn (elemen yang di-addEventListener)
});

// 4. Arrow function: this BUKAN elemen!
btn.addEventListener("click", () => {
  console.log(this); // = window (atau undefined di strict mode)
});

🎯 Challenge

html
<button id="counter-btn">Diklik: 0 kali</button>
<button id="reset-btn">Reset</button>

<!-- Tulis JavaScript untuk:
1. Klik counter-btn → angka bertambah, teks update
2. Klik reset-btn → angka kembali ke 0
3. Setelah 10 klik, hapus event listener dari counter-btn (pakai removeEventListener)
4. Tampilkan pesan "Sudah selesai!" setelah listener dihapus
-->

2.2 Bubbling dan Capturing

💡Analogi

Bayangin kamu teriak di dalam ruangan yang ada di dalam gedung. Suaramu:

  1. Capturing (menangkap) — suara masuk dari luar gedung → lantai → ruangan → kamu
  2. Target — suara sampai di kamu (sumber)
  3. Bubbling (menggelembung) — suara keluar dari kamu → ruangan → lantai → gedung

Event di browser bekerja sama persis!

Bubbling

Saat event terjadi di elemen, handler di elemen itu jalan dulu, lalu naik ke parent, terus ke atas sampai document:

html
<div id="grandparent">
  <div id="parent">
    <button id="child">Klik saya</button>
  </div>
</div>
javascript
document.getElementById("grandparent").addEventListener("click", () => {
  console.log("3. Grandparent diklik");
});

document.getElementById("parent").addEventListener("click", () => {
  console.log("2. Parent diklik");
});

document.getElementById("child").addEventListener("click", () => {
  console.log("1. Child diklik");
});

// Klik button → output:
// 1. Child diklik
// 2. Parent diklik
// 3. Grandparent diklik

event.target vs event.currentTarget

javascript
document.getElementById("parent").addEventListener("click", function(event) {
  console.log("target:", event.target);        // Elemen yang BENAR-BENAR diklik
  console.log("currentTarget:", event.currentTarget); // Elemen yang punya handler (= this)
});

// Klik button (child) →
// target: <button> (yang diklik)
// currentTarget: <div#parent> (yang punya handler)

Menghentikan Bubbling

javascript
btn.addEventListener("click", function(event) {
  event.stopPropagation(); // Event BERHENTI di sini, tidak naik ke parent
  alert("Hanya handler ini yang jalan");
});

// stopImmediatePropagation — hentikan SEMUA handler (termasuk di elemen yang sama)
btn.addEventListener("click", function(event) {
  event.stopImmediatePropagation();
  alert("Handler 1");
});
btn.addEventListener("click", function(event) {
  alert("Handler 2"); // TIDAK JALAN!
});

Capturing Phase

Secara default, handler jalan di fase bubbling. Untuk menangkap di fase capturing:

javascript
// Fase capturing (jarang dipakai, tapi penting dipahami)
elem.addEventListener("click", handler, { capture: true });
// atau
elem.addEventListener("click", handler, true);

Urutan Lengkap Event

1. CAPTURING: document → html → body → div → ... → target 2. TARGET: handler di elemen target 3. BUBBLING: target → ... → div → body → html → document
javascript
// Demo urutan
for (const elem of document.querySelectorAll("*")) {
  elem.addEventListener("click", (e) => {
    console.log(`Capturing: ${elem.tagName}`);
  }, true); // capture phase

  elem.addEventListener("click", (e) => {
    console.log(`Bubbling: ${elem.tagName}`);
  }); // bubble phase
}
⚠️Jebakan!
javascript
// 1. Tidak semua event bubble!
// focus, blur, load → TIDAK bubble
// focusin, focusout → bubble (alternatifnya)

// 2. stopPropagation() jarang dibutuhkan
// Biasanya ada cara lebih baik (event delegation)
// Hati-hati: bisa break fitur lain yang bergantung pada bubbling

// 3. event.target bisa elemen DALAM yang tidak kamu expect
// Misal klik <b> di dalam <button> → target = <b>, bukan <button>

🎯 Challenge

html
<div id="outer" style="padding:30px; background:lightblue;">
  OUTER
  <div id="middle" style="padding:30px; background:lightgreen;">
    MIDDLE
    <div id="inner" style="padding:30px; background:lightyellow;">
      INNER
    </div>
  </div>
</div>

<!-- Tulis JavaScript untuk:
1. Pasang click handler di ketiga div (log nama div yang diklik)
2. Klik INNER → lihat urutan log (bubbling)
3. Tambahkan stopPropagation() di MIDDLE → klik INNER lagi, apa yang berubah?
4. Pasang handler capturing di OUTER → perhatikan urutan baru
-->

2.3 Event Delegation

💡Analogi

Bayangin kamu punya 100 karyawan. Kamu bisa:

  • ❌ Kasih HP ke masing-masing dan telepon satu-satu (100 event listener)
  • ✅ Pasang 1 pengumuman di speaker gedung, karyawan yang relevan merespons (1 event listener di parent)

Event delegation = pasang satu handler di parent, lalu cek siapa yang sebenarnya diklik.

Masalah: Banyak Handler

javascript
// ❌ BURUK — 100 handler untuk 100 tombol
document.querySelectorAll(".btn").forEach(btn => {
  btn.addEventListener("click", function() {
    // handle...
  });
});

Solusi: Event Delegation

javascript
// ✅ BAGUS — 1 handler di parent
document.getElementById("button-container").addEventListener("click", function(event) {
  const btn = event.target.closest(".btn"); // Cari tombol terdekat
  if (!btn) return; // Klik bukan di tombol → abaikan
  if (!this.contains(btn)) return; // Safety check
  
  // Handle berdasarkan tombol mana
  console.log("Tombol diklik:", btn.dataset.action);
});

Contoh Praktis: Menu

html
<div id="menu">
  <button data-action="save">💾 Save</button>
  <button data-action="load">📂 Load</button>
  <button data-action="search">🔍 Search</button>
</div>
javascript
const menu = document.getElementById("menu");

const actions = {
  save() { alert("Menyimpan..."); },
  load() { alert("Memuat..."); },
  search() { alert("Mencari..."); }
};

menu.addEventListener("click", function(event) {
  const btn = event.target.closest("[data-action]");
  if (!btn) return;
  
  const action = btn.dataset.action;
  if (actions[action]) {
    actions[action]();
  }
});

Contoh Praktis: Tabel yang Bisa Diurutkan

html
<table id="data-table">
  <thead>
    <tr>
      <th data-sort="name">Nama ↕</th>
      <th data-sort="age">Umur ↕</th>
      <th data-sort="city">Kota ↕</th>
    </tr>
  </thead>
  <tbody>
    <tr><td>Budi</td><td>25</td><td>Jakarta</td></tr>
    <tr><td>Ani</td><td>30</td><td>Bandung</td></tr>
  </tbody>
</table>
javascript
document.getElementById("data-table").addEventListener("click", function(event) {
  const th = event.target.closest("th[data-sort]");
  if (!th) return;
  
  const column = th.dataset.sort;
  console.log("Sort by:", column);
  // Logic sorting di sini...
});

Pattern: Behavior dengan data-* Attribute

html
<!-- Deklaratif: behavior ditentukan di HTML -->
<button data-toggle-id="menu">Toggle Menu</button>
<button data-toggle-id="sidebar">Toggle Sidebar</button>

<div id="menu" hidden>Menu content</div>
<div id="sidebar" hidden>Sidebar content</div>
javascript
// Satu handler untuk semua toggle behavior
document.addEventListener("click", function(event) {
  const btn = event.target.closest("[data-toggle-id]");
  if (!btn) return;
  
  const targetId = btn.dataset.toggleId;
  const target = document.getElementById(targetId);
  target.hidden = !target.hidden;
});

Kapan Pakai Event Delegation?

SituasiDelegation?
Banyak elemen serupa (list, tabel)✅ Ya
Elemen ditambah/dihapus dinamis✅ Ya (handler tetap bekerja!)
Satu elemen spesifik❌ Langsung addEventListener
Event yang tidak bubble (focus/blur)❌ Pakai focusin/focusout
⚠️Jebakan!
javascript
// 1. event.target bisa elemen DALAM tombol
// <button><b>Bold text</b></button>
// Klik bold → target = <b>, bukan <button>!
// Solusi: pakai closest()

// 2. Jangan lupa cek contains()
parent.addEventListener("click", function(event) {
  const btn = event.target.closest(".btn");
  if (!btn) return;
  if (!this.contains(btn)) return; // Penting! btn bisa di luar parent
  // handle...
});

// 3. CPU overhead minimal, tapi logic bisa jadi kompleks
// Jangan over-delegate — kalau cuma 2-3 tombol, langsung aja

🎯 Challenge

html
<ul id="todo-list">
  <li>Belajar DOM <button class="delete">❌</button></li>
  <li>Latihan Event <button class="delete">❌</button></li>
  <li>Buat Project <button class="delete">❌</button></li>
</ul>
<button id="add-todo">+ Tambah Todo</button>

<!-- Tulis JavaScript dengan EVENT DELEGATION:
1. Klik tombol ❌ → hapus <li> parent-nya
2. Klik "Tambah Todo" → tambah <li> baru (dengan tombol ❌)
3. Todo baru juga harus bisa dihapus (tanpa addEventListener baru!)
4. Semua pakai SATU event listener di #todo-list
-->

2.4 Browser Default Actions

💡Analogi

Browser punya kebiasaan bawaan untuk event tertentu. Klik link → pindah halaman. Submit form → reload. Klik kanan → muncul menu. Kadang kita mau mencegah kebiasaan ini.

Default Action yang Umum

EventDefault Action
Klik <a href>Navigasi ke URL
Submit <form>Kirim data & reload
Klik kananMuncul context menu
keydown di inputKarakter muncul
mousedown pada teksMulai seleksi teks

Mencegah Default Action

javascript
// Cara 1: event.preventDefault()
const link = document.querySelector("a");
link.addEventListener("click", function(event) {
  event.preventDefault(); // Link tidak navigasi
  alert("Link dicegah! URL: " + this.href);
});

// Cara 2: return false (hanya untuk on-event property, BUKAN addEventListener)
link.onclick = function() {
  return false; // Sama dengan preventDefault + stopPropagation
};

Contoh: Form Validation

html
<form id="register">
  <input type="text" id="username" placeholder="Username (min 3 huruf)">
  <button type="submit">Daftar</button>
</form>
javascript
document.getElementById("register").addEventListener("submit", function(event) {
  const username = document.getElementById("username").value;
  
  if (username.length < 3) {
    event.preventDefault(); // Form TIDAK di-submit
    alert("Username minimal 3 karakter!");
  }
});

Contoh: Custom Context Menu

javascript
document.addEventListener("contextmenu", function(event) {
  event.preventDefault(); // Menu bawaan tidak muncul
  
  // Tampilkan menu custom
  const menu = document.getElementById("custom-menu");
  menu.style.left = event.clientX + "px";
  menu.style.top = event.clientY + "px";
  menu.hidden = false;
});

// Klik di mana saja → sembunyikan menu
document.addEventListener("click", function() {
  document.getElementById("custom-menu").hidden = true;
});

Cek Apakah Default Dicegah

javascript
elem.addEventListener("click", function(event) {
  if (event.defaultPrevented) {
    // Seseorang sudah preventDefault sebelumnya (di child)
    return;
  }
  // Handle...
});

Option passive

javascript
// Untuk scroll/touch event — bilang ke browser: "Saya TIDAK akan preventDefault"
// Browser bisa optimize scrolling (lebih smooth)
document.addEventListener("touchstart", handler, { passive: true });

// Kalau passive:true tapi tetap panggil preventDefault → warning di console
⚠️Jebakan!
javascript
// 1. preventDefault() TIDAK menghentikan bubbling!
// Kalau mau keduanya: preventDefault() + stopPropagation()

// 2. Beberapa event tidak bisa dicegah
// Cek event.cancelable sebelum preventDefault
if (event.cancelable) {
  event.preventDefault();
}

// 3. Jangan disable context menu di seluruh halaman
// User butuh itu! Hanya disable di area spesifik kalau memang perlu

// 4. return false di addEventListener TIDAK sama dengan preventDefault
elem.addEventListener("click", function() {
  return false; // Tidak ada efek! Hanya bekerja di onclick property
});

🎯 Challenge

html
<nav>
  <a href="https://google.com" data-confirm="true">Google (konfirmasi dulu)</a>
  <a href="https://github.com" data-confirm="true">GitHub (konfirmasi dulu)</a>
  <a href="https://javascript.info">JS Info (langsung)</a>
</nav>

<!-- Tulis JavaScript untuk:
1. Link dengan data-confirm="true" → tanya konfirmasi dulu sebelum navigasi
2. Kalau user cancel → preventDefault, tampilkan "Navigasi dibatalkan"
3. Link tanpa data-confirm → biarkan normal
4. Pakai event delegation (satu handler di <nav>)
-->

2.5 Dispatching Custom Events

💡Analogi

Selama ini kita mendengarkan event yang dibuat browser (klik, ketik, dll). Sekarang kita belajar membuat event sendiri — kayak bikin bel custom yang bisa kamu tekan kapan saja.

Membuat dan Mengirim Event

javascript
// 1. Buat event
const myEvent = new Event("hello");

// 2. Kirim (dispatch) ke elemen
const elem = document.getElementById("target");
elem.dispatchEvent(myEvent);

// 3. Handler yang sudah dipasang akan jalan
elem.addEventListener("hello", function(event) {
  console.log("Event hello diterima!");
});

CustomEvent — Dengan Data Tambahan

javascript
// CustomEvent bisa bawa data via detail
const event = new CustomEvent("user-login", {
  detail: {
    username: "budi",
    role: "admin"
  },
  bubbles: true,    // Bisa bubble ke parent
  cancelable: true  // Bisa di-preventDefault
});

elem.dispatchEvent(event);

// Handler menerima data
elem.addEventListener("user-login", function(event) {
  console.log("User:", event.detail.username); // "budi"
  console.log("Role:", event.detail.role);     // "admin"
});

Event Options

javascript
new CustomEvent("nama", {
  bubbles: true,     // Default: false. True = event naik ke parent
  cancelable: true,  // Default: false. True = bisa preventDefault
  detail: { ... }    // Data custom apapun
});

Cek Apakah Event Dicegah

javascript
const event = new CustomEvent("my-event", { cancelable: true });

elem.addEventListener("my-event", function(event) {
  event.preventDefault(); // Cegah!
});

const wasNotCancelled = elem.dispatchEvent(event);
// dispatchEvent return false kalau ada yang preventDefault

if (!wasNotCancelled) {
  console.log("Event dicegah oleh handler");
} else {
  console.log("Event tidak dicegah, lanjutkan aksi");
}

Contoh Praktis: Komponen Tab

javascript
// Komponen tab mengirim event saat tab berubah
class TabPanel {
  constructor(elem) {
    this.elem = elem;
    this.elem.addEventListener("click", (e) => {
      const tab = e.target.closest("[data-tab]");
      if (!tab) return;
      this.switchTab(tab.dataset.tab);
    });
  }
  
  switchTab(tabName) {
    // Kirim event sebelum switch
    const event = new CustomEvent("tab-change", {
      detail: { tab: tabName },
      bubbles: true,
      cancelable: true
    });
    
    if (this.elem.dispatchEvent(event)) {
      // Tidak dicegah → lakukan switch
      console.log("Switching to:", tabName);
    }
  }
}

// Parent bisa mendengarkan dan mencegah
document.addEventListener("tab-change", function(event) {
  if (event.detail.tab === "admin" && !isAdmin) {
    event.preventDefault();
    alert("Akses ditolak!");
  }
});

Event dalam Event (Synchronous)

javascript
// dispatchEvent itu SYNCHRONOUS — handler langsung jalan
btn.addEventListener("click", function() {
  console.log("1. Sebelum dispatch");
  
  elem.dispatchEvent(new Event("custom"));
  
  console.log("3. Setelah dispatch");
});

elem.addEventListener("custom", function() {
  console.log("2. Custom handler"); // Jalan di tengah!
});

// Output: 1, 2, 3
⚠️Jebakan!
javascript
// 1. Custom event TIDAK bubble secara default
new Event("my-event"); // bubbles: false!
new Event("my-event", { bubbles: true }); // Harus eksplisit

// 2. addEventListener harus dipasang SEBELUM dispatchEvent
// Kalau dispatch dulu, handler belum ada → tidak tertangkap

// 3. Nama event: gunakan kebab-case untuk custom event
// "user-login" ✅ (jelas custom)
// "userLogin" ⚠️ (bisa bingung dengan native event)

// 4. event.isTrusted
// true = dari user (klik asli)
// false = dari script (dispatchEvent)

🎯 Challenge

javascript
// Buat sistem notifikasi custom:
// 1. Buat fungsi notify(message, type) yang dispatch CustomEvent "notification"
//    - detail: { message, type } (type: "success", "error", "info")
//    - bubbles: true
// 2. Pasang listener di document yang menangkap event "notification"
// 3. Handler: buat <div class="notification {type}"> dan tampilkan di halaman
// 4. Notifikasi hilang otomatis setelah 3 detik
// 5. Test: notify("Berhasil disimpan!", "success")

Ringkasan Bab 2

Sub-babInti
Browser EventsaddEventListener (recommended), event object, handler
Bubbling & CapturingEvent naik dari target ke document (bubble), turun saat capture
Event DelegationSatu handler di parent, cek target — efisien untuk banyak elemen
Default ActionspreventDefault() untuk cegah behavior bawaan browser
Custom Eventsnew CustomEvent() + dispatchEvent() — buat event sendiri

Prinsip utama:

  • Selalu pakai addEventListener (bukan onclick)
  • Manfaatkan bubbling dengan event delegation
  • event.target = yang diklik, event.currentTarget = yang punya handler
  • Custom event untuk komunikasi antar komponen

Next: Bab 3 — UI Events (mouse, keyboard, scroll — detail interaksi user)

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.