← Kursa Dön
📄 Text · 30 min

Event Handling

Giriş — Web Sayfalarına Hayat Vermek

Şimdiye kadar HTML elementlerini seçmeyi ve değiştirmeyi öğrendik. Ama tüm bunları elle, konsoldan yapıyorduk. Gerçek bir web uygulamasında, değişiklikleri kullanıcının eylemi tetikler: butona tıklamak, form doldurmak, sayfayı kaydırmak, klavyeye basmak, fareyi hareket ettirmek. İşte event handling (olay yönetimi), JavaScript'i kullanıcı eylemleriyle buluşturan mekanizmadır.

Her gün kullandığınız web sitelerini düşünün — Instagram'da bir fotoğrafı beğenmek, YouTube'da videoyu duraklatmak, Google Maps'te haritayı sürüklemek. Bunların hepsi event'lerle çalışır. Bir event gerçekleşir, JavaScript onu "dinler" ve tepki verir.

Analoji: Bir apartmandaki kapı zili sistemini düşünün. Her dairenin bir zili (event) ve zilini duyan bir kişi (event listener) vardır. Zile basan misafir (kullanıcı eylemi), zilin çalmasına neden olur. Zilini duyan kişi kapıyı açar (handler fonksiyonu çalışır). Zili sökerseniz, kimse gelmez. Üç ayrı kişiyi dinlemeye koyarsanız, üçü de gelir. Event sistemi tam olarak böyle çalışır.


Event Nedir?

Event (olay), tarayıcıda gerçekleşen herhangi bir şeydir. Kullanıcı kaynaklı olabilir (tıklama, yazma) veya sistem kaynaklı olabilir (sayfa yüklendi, resim yüklendi, ağ bağlantısı koptu).

Yaygın Event Tipleri

KategoriEventNe Zaman Tetiklenir
FareclickElement tıklandığında
dblclickÇift tıklandığında
mouseenterFare elementin üzerine girdiğinde
mouseleaveFare elementten ayrıldığında
mousemoveFare hareket ettiğinde
contextmenuSağ tıklandığında
KlavyekeydownTuşa basıldığında
keyupTuş bırakıldığında
FormsubmitForm gönderildiğinde
inputInput değeri değiştiğinde (her tuşta)
changeInput değeri değişip focus kaybolduğunda
focusElement focus aldığında
blurElement focus kaybettiğinde
BelgeDOMContentLoadedDOM hazır olduğunda
loadTüm kaynaklar yüklendiğinde
scrollSayfa kaydırıldığında
resizePencere boyutu değiştiğinde
DokunmatouchstartParmak dokunduğunda
touchendParmak kalktığında
touchmoveParmak kaydırdığında

addEventListener — Event Dinleyici

addEventListener, bir elemente event dinleyici ekleyen modern ve önerilen yöntemdir.

const buton = document.querySelector("#tikla-btn");

// Temel sözdizimi
buton.addEventListener("click", function() {
  console.log("Butona tıklandı!");
});

// Arrow function ile
buton.addEventListener("click", () => {
  console.log("Arrow function ile dinleme");
});

// Ayrı fonksiyon referansı ile (önerilen)
function butonTiklandi() {
  console.log("Buton tıklandı!");
}
buton.addEventListener("click", butonTiklandi);

// Aynı elemente birden fazla listener eklenebilir
buton.addEventListener("click", () => console.log("Birinci listener"));
buton.addEventListener("click", () => console.log("İkinci listener"));
buton.addEventListener("click", () => console.log("Üçüncü listener"));
// Üçü de çalışır, sırasıyla!

Listener'ı Kaldırma (removeEventListener)

function handler() {
  console.log("Tıklandı!");
}

// Ekle
buton.addEventListener("click", handler);

// Kaldır — aynı fonksiyon REFERANSINI geçmeniz gerekir!
buton.removeEventListener("click", handler);

// ❌ Bu ÇALIŞMAZ — farklı fonksiyon referansı
buton.addEventListener("click", () => console.log("Merhaba"));
buton.removeEventListener("click", () => console.log("Merhaba")); // Kaldırmaz!
// İki arrow function birbirine eşit değildir — farklı objelerdir

// ✅ Bir kez çalışıp kendi kendini kaldıran listener
buton.addEventListener("click", function tekSeferlik() {
  console.log("Sadece bir kez çalışır");
  buton.removeEventListener("click", tekSeferlik);
});

// ✅ Daha kolay yöntem: { once: true }
buton.addEventListener("click", () => {
  console.log("Sadece bir kez çalışır — otomatik kaldırılır");
}, { once: true });

Eski Yöntemler (Kullanmayın)

// ❌ Inline HTML event — bakımı zor, güvenlik riski
// <button onclick="alert('Tıklandı!')">Tıkla</button>

// ❌ DOM property — sadece tek listener atanabilir
buton.onclick = function() { console.log("Birinci"); };
buton.onclick = function() { console.log("İkinci"); };
// Sadece "İkinci" çalışır — birinci override edildi!

// ✅ addEventListener — birden fazla listener, temiz kaldırma
buton.addEventListener("click", () => console.log("Birinci"));
buton.addEventListener("click", () => console.log("İkinci"));
// İkisi de çalışır!

Event Object — Olay Bilgileri

Her event tetiklendiğinde, tarayıcı otomatik olarak bir event nesnesi oluşturur ve handler fonksiyonuna parametre olarak geçer. Bu nesne, olay hakkında her türlü bilgiyi içerir.

const buton = document.querySelector("#btn");

buton.addEventListener("click", function(event) {
  // event (veya kısaca e) — olay hakkında bilgi
  console.log(event.type);        // "click"
  console.log(event.target);      // Tıklanan element
  console.log(event.currentTarget); // Listener'ın bağlı olduğu element
  console.log(event.timeStamp);   // Olayın zamanı (ms)

  // Fare pozisyonu
  console.log(event.clientX, event.clientY); // Viewport'a göre
  console.log(event.pageX, event.pageY);     // Sayfaya göre (scroll dahil)
  console.log(event.screenX, event.screenY); // Ekrana göre

  // Modifier tuşları
  console.log(event.shiftKey);   // Shift basılı mı?
  console.log(event.ctrlKey);    // Ctrl basılı mı?
  console.log(event.altKey);     // Alt basılı mı?
  console.log(event.metaKey);    // Cmd (Mac) / Win tuşu basılı mı?
});

Klavye Event'leri

document.addEventListener("keydown", (e) => {
  console.log(e.key);     // "Enter", "a", "ArrowUp", "Escape"
  console.log(e.code);    // "Enter", "KeyA", "ArrowUp", "Escape"
  console.log(e.keyCode); // 13 (⚠️ deprecated — kullanmayın)

  // key vs code farkı:
  // key → basılan tuşun DEĞERI (dile bağlı: "a" veya "ş")
  // code → fiziksel tuşun KODU (her zaman aynı: "KeyA")

  // Kısayol tuşları
  if (e.ctrlKey && e.key === "s") {
    e.preventDefault(); // Tarayıcının kaydetme dialogunu engelle
    console.log("Ctrl+S — kaydet!");
  }

  if (e.key === "Escape") {
    modalKapat();
  }
});

// Input'ta yazma takibi
const arama = document.querySelector("#arama-input");
arama.addEventListener("input", (e) => {
  console.log(e.target.value);       // Input'un anlık değeri
  console.log(e.data);               // Girilen karakter
  console.log(e.inputType);          // "insertText", "deleteContentBackward"...
});

Form Event'leri

const form = document.querySelector("#kayit-formu");

form.addEventListener("submit", (e) => {
  e.preventDefault(); // Sayfanın yenilenmesini engelle!

  // Form verilerini oku
  const formData = new FormData(form);
  const veri = {
    isim: formData.get("isim"),
    email: formData.get("email"),
    mesaj: formData.get("mesaj")
  };

  console.log("Form verisi:", veri);
  // API'ye gönder...
});

// Input event — her değişiklikte (canlı arama için ideal)
const aramaInput = document.querySelector("#arama");
aramaInput.addEventListener("input", (e) => {
  const sorgu = e.target.value.trim().toLowerCase();
  urunleriFiltrele(sorgu);
});

// Change event — değer değişip focus kaybedildiğinde
const selectBox = document.querySelector("#sehir-select");
selectBox.addEventListener("change", (e) => {
  console.log("Seçilen şehir:", e.target.value);
});

// Focus ve Blur — input'a odaklanma/ayrılma
const emailInput = document.querySelector("#email");
emailInput.addEventListener("focus", () => {
  emailInput.parentElement.classList.add("aktif");
});
emailInput.addEventListener("blur", () => {
  emailInput.parentElement.classList.remove("aktif");
  emailDogrula(emailInput.value);
});

preventDefault ve stopPropagation

// preventDefault — tarayıcının varsayılan davranışını engeller
const link = document.querySelector("a.ozel-link");
link.addEventListener("click", (e) => {
  e.preventDefault(); // Sayfaya yönlendirmeyi engelle
  console.log("Link tıklandı ama sayfa değişmedi");
  // Kendi navigasyon mantığını yaz...
});

// Form submit'te sayfa yenilemeyi engelleme
form.addEventListener("submit", (e) => {
  e.preventDefault(); // Sayfa yenilenmez
  // AJAX ile form gönder...
});

// Sağ tıklamayı engelleme
document.addEventListener("contextmenu", (e) => {
  e.preventDefault();
  // Özel sağ tık menüsü göster...
  ozelMenuGoster(e.clientX, e.clientY);
});

// stopPropagation — event'in üst elementlere yayılmasını durdurur
// (aşağıdaki bubbling bölümünde detaylı anlatılacak)

Event Bubbling ve Capturing

Event sistemi, sadece tıklanan elementte değil, DOM ağacı boyunca çalışır. Bu mekanizmayı anlamak, kompleks uygulamalarda event yönetimi için kritiktir.

Bubbling — Olayın Yukarı Yayılması

Bir elemente tıkladığınızda, event o elementten başlayıp DOM ağacında yukarı doğru tüm atalara yayılır. Tıpkı sudaki bir hava kabarcığının (bubble) dipten yüzeye yükselmesi gibi.

<!-- HTML yapısı -->
<div id="buyukbaba">
  Büyükbaba
  <div id="baba">
    Baba
    <button id="cocuk">Çocuk Buton</button>
  </div>
</div>
document.querySelector("#buyukbaba").addEventListener("click", () => {
  console.log("3️⃣ Büyükbaba tıklandı");
});

document.querySelector("#baba").addEventListener("click", () => {
  console.log("2️⃣ Baba tıklandı");
});

document.querySelector("#cocuk").addEventListener("click", () => {
  console.log("1️⃣ Çocuk tıklandı");
});

// "Çocuk Buton"a tıklandığında konsol çıktısı:
// 1️⃣ Çocuk tıklandı
// 2️⃣ Baba tıklandı
// 3️⃣ Büyükbaba tıklandı

// Sadece butona tıkladık ama üç listener da çalıştı!
// Event, cocuk → baba → buyukbaba sırasıyla YUKARI yayıldı.

Capturing — Olayın Aşağı İnmesi

Bubbling'in tersi olan capturing fazında, event en üstten başlayıp hedef elemente doğru aşağı iner. Capturing listener'ları { capture: true } ile eklenir.

// Capturing + Bubbling — tam event akışı
document.querySelector("#buyukbaba").addEventListener("click", () => {
  console.log("📥 Büyükbaba — CAPTURING");
}, { capture: true }); // veya true

document.querySelector("#buyukbaba").addEventListener("click", () => {
  console.log("📤 Büyükbaba — BUBBLING");
});

document.querySelector("#cocuk").addEventListener("click", () => {
  console.log("🎯 Çocuk — HEDEF");
});

// Çocuk butonuna tıklandığında:
// 📥 Büyükbaba — CAPTURING  (1. Aşağı iniyor)
// 🎯 Çocuk — HEDEF           (2. Hedefe ulaştı)
// 📤 Büyükbaba — BUBBLING    (3. Yukarı çıkıyor)

Tam event akışı:

          Capturing fazı         Bubbling fazı
          (yukarıdan aşağı)      (aşağıdan yukarı)
          
document ──→                  ──→ document
  html   ──→                  ──→ html
   body  ──→                  ──→ body
    div  ──→                  ──→ div
     button ─── HEDEF ────────→ button

stopPropagation — Yayılmayı Durdurma

document.querySelector("#baba").addEventListener("click", () => {
  console.log("Baba tıklandı");
});

document.querySelector("#cocuk").addEventListener("click", (e) => {
  e.stopPropagation(); // Yayılma burada durur!
  console.log("Çocuk tıklandı — baba bunu bilmeyecek");
});

// Çocuğa tıklanınca:
// "Çocuk tıklandı — baba bunu bilmeyecek" (sadece bu)
// Baba'nın listener'ı çalışMAZ — propagation durduruldu

⚠️ Dikkat: stopPropagation()'ı dikkatli kullanın. Event yayılımını durdurmak, üst elementlerdeki event delegation gibi mekanizmaları bozabilir. Sadece gerçekten gerekli olduğunda kullanın.

event.target vs event.currentTarget

Bu ayrım, özellikle bubbling'de çok önemlidir:

document.querySelector("#baba").addEventListener("click", (e) => {
  console.log("target:", e.target);         // Tıklanan asıl element
  console.log("currentTarget:", e.currentTarget); // Listener'ın bağlı olduğu element
});

// Çocuk butona tıklanınca:
// target: <button id="cocuk">       — gerçekte tıklanan
// currentTarget: <div id="baba">    — listener'ın sahibi

// Baba div'e tıklanınca:
// target: <div id="baba">           — gerçekte tıklanan
// currentTarget: <div id="baba">    — listener'ın sahibi (aynı)

Event Delegation — Akıllı Olay Yönetimi

Event delegation, bubbling mekanizmasını kullanarak tek bir üst elementte dinleyip, tüm alt elementlerin event'lerini yakalama tekniğidir. Bu, dinamik içerikte (sonradan eklenen elementler) ve performansta büyük avantaj sağlar.

Problem: Her Elemente Ayrı Listener

// ❌ Her liste öğesine ayrı listener — kötü pratik
const listeOgeleri = document.querySelectorAll("#gorev-listesi li");
listeOgeleri.forEach(li => {
  li.addEventListener("click", () => {
    li.classList.toggle("tamamlandi");
  });
});

// Problemler:
// 1. 1000 öğe = 1000 listener = bellek israfı
// 2. Sonradan eklenen öğeler listener'a sahip OLMAZ
// 3. Toplu kaldırma zor

Çözüm: Event Delegation

// ✅ Tek listener — parent'ta dinle, target ile ayırt et
const liste = document.querySelector("#gorev-listesi");

liste.addEventListener("click", (e) => {
  // Tıklanan element bir <li> mi?
  const li = e.target.closest("li");
  if (!li) return; // li değilse hiçbir şey yapma
  if (!liste.contains(li)) return; // Liste dışındaki bir li'ye tıklanmışsa

  li.classList.toggle("tamamlandi");
});

// Artık sonradan eklenen öğeler de otomatik çalışır!
const yeniGorev = document.createElement("li");
yeniGorev.textContent = "Yeni görev";
liste.appendChild(yeniGorev);
// Tıklanınca çalışır — ayrı listener eklemeye gerek yok!

closest() — En Yakın Atayı Bul

closest(), event delegation'ın en iyi arkadaşıdır. Bir elementten başlayarak DOM ağacında yukarı doğru ilerleyip, verilen CSS selector'ına uyan ilk atayı bulur.

// HTML:
// <ul id="urun-listesi">
//   <li class="urun" data-id="1">
//     <span class="urun-isim">Laptop</span>
//     <span class="urun-fiyat">54999 ₺</span>
//     <button class="sil-btn">🗑️</button>
//   </li>
// </ul>

const urunListesi = document.querySelector("#urun-listesi");

urunListesi.addEventListener("click", (e) => {
  // Silme butonuna mı tıklandı?
  const silBtn = e.target.closest(".sil-btn");
  if (silBtn) {
    const urunLi = silBtn.closest(".urun");
    const urunId = urunLi.dataset.id;
    urunSil(urunId);
    urunLi.remove();
    return;
  }

  // Ürün satırına mı tıklandı?
  const urunLi = e.target.closest(".urun");
  if (urunLi) {
    urunDetayGoster(urunLi.dataset.id);
  }
});

Delegation ile Dinamik Tablo

// Tablo satırlarında düzenleme ve silme butonları
const tablo = document.querySelector("#kullanici-tablosu tbody");

tablo.addEventListener("click", (e) => {
  const btn = e.target.closest("button");
  if (!btn) return;

  const satir = btn.closest("tr");
  const kullaniciId = satir.dataset.id;

  if (btn.classList.contains("duzenle-btn")) {
    duzenlemeModuAc(satir, kullaniciId);
  } else if (btn.classList.contains("sil-btn")) {
    if (confirm("Silmek istediğinize emin misiniz?")) {
      satir.remove();
      kullaniciSil(kullaniciId);
    }
  }
});

// Yeni satır eklendiğinde listener eklemeye gerek yok
function kullaniciEkle(kullanici) {
  tablo.insertAdjacentHTML("beforeend", `
    <tr data-id="${kullanici.id}">
      <td>${kullanici.isim}</td>
      <td>${kullanici.email}</td>
      <td>
        <button class="duzenle-btn">✏️</button>
        <button class="sil-btn">🗑️</button>
      </td>
    </tr>
  `);
  // Event delegation sayesinde butonlar otomatik çalışır!
}

💡 İpucu: Event delegation'ı üç durumda tercih edin: (1) çok sayıda benzer element varsa, (2) elementler dinamik olarak eklenip çıkarılıyorsa, (3) bellek kullanımını optimize etmek istiyorsanız. Temel kural: "listener'ı mümkün olduğunca üst seviyede tutun."


Throttle ve Debounce — Performans Kontrolleri

Bazı event'ler çok sık tetiklenir: scroll, resize, mousemove, input. Her tetiklendiğinde ağır bir işlem yaparsanız (API çağrısı, DOM güncelleme, hesaplama), sayfa performansı ciddi şekilde düşer. Throttle ve debounce, bu sorunu çözen iki klasik tekniktir.

Debounce — "Son Hareketten Sonra Bekle"

Debounce, event tetiklenmeyi durdurana kadar bekler, sonra bir kez çalıştırır. Kullanıcı yazmayı bitirdiğinde arama yapmak için idealdir.

Analoji: Asansör kapısı gibi düşünün. Birisi gelip butona basıyor, kapı kapanmaya başlıyor — ama o sırada bir kişi daha geliyor ve butona basıyor, kapı tekrar açılıp bekliyor. Bu sürekli devam eder. Kapı ancak kimse gelmeyince (belirli bir süre geçince) kapanır. Debounce de böyledir — her tetiklemede zamanlayıcı sıfırlanır, ancak tetikleme durunca fonksiyon çalışır.

function debounce(fn, bekleme) {
  let zamanlayici;

  return function(...args) {
    clearTimeout(zamanlayici); // Önceki zamanlayıcıyı iptal et
    zamanlayici = setTimeout(() => {
      fn.apply(this, args); // Bekleme süresi dolunca çalıştır
    }, bekleme);
  };
}

// Kullanım: Arama input'u — kullanıcı yazmayı bitirince ara
const aramaInput = document.querySelector("#arama");
const aramaYap = debounce((sorgu) => {
  console.log("API'de aranıyor:", sorgu);
  // fetch(`/api/arama?q=${sorgu}`)...
}, 300); // 300ms bekle

aramaInput.addEventListener("input", (e) => {
  aramaYap(e.target.value);
});

// Kullanıcı "javascript" yazarken:
// j → (zamanlayıcı başladı)
// ja → (zamanlayıcı sıfırlandı)
// jav → (zamanlayıcı sıfırlandı)
// java → (zamanlayıcı sıfırlandı)
// javas → (zamanlayıcı sıfırlandı)
// ...
// javascript → (zamanlayıcı sıfırlandı)
// (300ms bekleme...)
// API çağrısı: "javascript" — TEK bir çağrı!

Throttle — "Belirli Aralıklarla Çalıştır"

Throttle, bir fonksiyonu belirli bir zaman aralığında en fazla bir kez çalıştırır. Sürekli tetiklenen ama periyodik güncelleme gerektiren durumlar için idealdir.

Analoji: Bir musluktan su damlatmak gibi düşünün. Musluğu ne kadar açarsanız açın, damlatıcı her 2 saniyede bir damla bırakır — ne daha fazla, ne daha az. Throttle de budur: event ne kadar sık tetiklenirse tetiklensin, fonksiyon belirli aralıklarla çalışır.

function throttle(fn, aralik) {
  let sonCalisma = 0;

  return function(...args) {
    const simdi = Date.now();
    if (simdi - sonCalisma >= aralik) {
      sonCalisma = simdi;
      fn.apply(this, args);
    }
  };
}

// Kullanım: Scroll pozisyonunu takip — her 100ms'de bir güncelle
const scrollTakip = throttle(() => {
  const scrollY = window.scrollY;
  const sayfaYuzdesi = Math.round(
    (scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
  );
  document.querySelector("#ilerleme-bar").style.width = `${sayfaYuzdesi}%`;
}, 100);

window.addEventListener("scroll", scrollTakip);

// Pencere boyutu değişikliği — her 200ms'de bir layout hesapla
const boyutDegisti = throttle(() => {
  console.log(`Pencere: ${window.innerWidth}x${window.innerHeight}`);
  layoutYenidenHesapla();
}, 200);

window.addEventListener("resize", boyutDegisti);

Debounce vs Throttle — Ne Zaman Hangisi?

SenaryoDoğru SeçimNeden
Arama input'uDebounceYazmayı bitirince ara — gereksiz API çağrısı yapma
Otomatik kaydetmeDebounceDüzenleme bitince kaydet
Scroll pozisyon takibiThrottleDüzenli güncelleme gerekli — ama her scroll event'inde değil
Pencere resizeThrottleDüzenli aralıklarla layout hesapla
Buton tıklama (çift tıklama önleme)Throttleİlk tıklamayı al, kısa süre tekrar tıklamayı engelle
Form validasyonu (input)DebounceKullanıcı yazarken değil, durduktan sonra doğrula
Oyun fizik hesaplamaThrottleSabit FPS'te hesaplama yap
Sonsuz scrollThrottleBelirli aralıklarla scroll pozisyonunu kontrol et
// Gerçek dünya: Pencere resize + debounce (SON boyut yeterli)
window.addEventListener("resize", debounce(() => {
  gridYenidenOlustur(); // Sadece resize durduğunda çalışır
}, 250));

// Gerçek dünya: Mouse move + throttle (ARA DEĞERLER önemli)
canvas.addEventListener("mousemove", throttle((e) => {
  cizgiCiz(e.clientX, e.clientY); // Her 16ms'de bir çiz (60fps)
}, 16));

Özel Event'ler (Custom Events)

Kendi event'lerinizi oluşturup tetikleyebilirsiniz. Bu, bileşenler arası iletişim için güçlü bir mekanizmadır.

// Custom event oluşturma
const sepetGuncellendi = new CustomEvent("sepet:guncellendi", {
  detail: {
    urunSayisi: 5,
    toplamFiyat: 349.99,
    sonEklenen: "Kulaklık"
  },
  bubbles: true // Üst elementlere yayılsın
});

// Dinleme
document.addEventListener("sepet:guncellendi", (e) => {
  console.log(`Sepette ${e.detail.urunSayisi} ürün`);
  console.log(`Toplam: ${e.detail.toplamFiyat} ₺`);
  sepetBadgeGuncelle(e.detail.urunSayisi);
});

// Tetikleme
document.dispatchEvent(sepetGuncellendi);

// Pratik kullanım: Sepete ürün eklendiğinde
function sepeteEkle(urun) {
  sepet.push(urun);

  // Custom event ile diğer bileşenleri haberdar et
  document.dispatchEvent(new CustomEvent("sepet:guncellendi", {
    detail: {
      urunSayisi: sepet.length,
      toplamFiyat: sepet.reduce((t, u) => t + u.fiyat, 0),
      sonEklenen: urun.isim
    },
    bubbles: true
  }));
}

Gerçek Dünya Örneği: Etkileşimli Form Validasyonu

Bu örnek, event handling'in birçok yönünü bir arada kullanır:

class FormDogrulayici {
  constructor(formSelector) {
    this.form = document.querySelector(formSelector);
    this.hatalar = new Map();

    this.kurallar = {
      isim: (deger) => {
        if (!deger.trim()) return "İsim alanı zorunludur";
        if (deger.trim().length < 2) return "İsim en az 2 karakter olmalı";
        return null;
      },
      email: (deger) => {
        if (!deger.trim()) return "E-posta alanı zorunludur";
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(deger)) return "Geçerli bir e-posta girin";
        return null;
      },
      sifre: (deger) => {
        if (!deger) return "Şifre alanı zorunludur";
        if (deger.length < 8) return "Şifre en az 8 karakter olmalı";
        if (!/[A-Z]/.test(deger)) return "En az bir büyük harf gerekli";
        if (!/[0-9]/.test(deger)) return "En az bir rakam gerekli";
        return null;
      }
    };

    this.dinleyicileriKur();
  }

  dinleyicileriKur() {
    // Input event — debounce ile canlı doğrulama
    this.form.addEventListener("input", debounce((e) => {
      const input = e.target.closest("input, textarea");
      if (input && this.kurallar[input.name]) {
        this.alanDogrula(input);
      }
    }, 300));

    // Blur event — focus kaybedince anında doğrula
    this.form.addEventListener("blur", (e) => {
      const input = e.target.closest("input, textarea");
      if (input && this.kurallar[input.name]) {
        this.alanDogrula(input);
      }
    }, true); // Capturing — blur bubble etmez, capturing ile yakala

    // Submit event — event delegation ile
    this.form.addEventListener("submit", (e) => {
      e.preventDefault();
      if (this.tumunuDogrula()) {
        console.log("Form geçerli — gönderilebilir!");
        this.form.dispatchEvent(new CustomEvent("form:gecerli", {
          detail: Object.fromEntries(new FormData(this.form))
        }));
      }
    });
  }

  alanDogrula(input) {
    const kural = this.kurallar[input.name];
    const hata = kural(input.value);

    if (hata) {
      this.hatalar.set(input.name, hata);
      input.classList.remove("gecerli");
      input.classList.add("hatali");
      this.hataMesajiGoster(input, hata);
    } else {
      this.hatalar.delete(input.name);
      input.classList.remove("hatali");
      input.classList.add("gecerli");
      this.hataMesajiGizle(input);
    }
  }

  hataMesajiGoster(input, mesaj) {
    let hataSpan = input.parentElement.querySelector(".hata-mesaji");
    if (!hataSpan) {
      hataSpan = document.createElement("span");
      hataSpan.className = "hata-mesaji";
      input.parentElement.appendChild(hataSpan);
    }
    hataSpan.textContent = mesaj;
  }

  hataMesajiGizle(input) {
    const hataSpan = input.parentElement.querySelector(".hata-mesaji");
    if (hataSpan) hataSpan.remove();
  }

  tumunuDogrula() {
    const inputlar = this.form.querySelectorAll("input[name], textarea[name]");
    inputlar.forEach(input => {
      if (this.kurallar[input.name]) {
        this.alanDogrula(input);
      }
    });
    return this.hatalar.size === 0;
  }
}

// Debounce yardımcı fonksiyonu
function debounce(fn, bekleme) {
  let zamanlayici;
  return function(...args) {
    clearTimeout(zamanlayici);
    zamanlayici = setTimeout(() => fn.apply(this, args), bekleme);
  };
}

// Kullanım
const dogrulayici = new FormDogrulayici("#kayit-formu");

// Custom event ile form gönderimi
document.querySelector("#kayit-formu").addEventListener("form:gecerli", (e) => {
  console.log("Sunucuya gönderiliyor:", e.detail);
});

Bu örnekte kullanılan event kavramları:

  • addEventListener — click, input, blur, submit dinleme

  • Event delegation — form üzerinde tek listener, input'ları closest() ile ayırt etme

  • preventDefault — form submit'te sayfa yenilemesini engelleme

  • Capturing — blur event'i capturing fazında yakalama (blur bubble etmez)

  • Debounce — canlı input validasyonunda gereksiz kontrolü engelleme

  • Custom Event — form doğrulandığında diğer bileşenleri haberdar etme


Özet

  • 🔹 `addEventListener` modern event dinleme yöntemidir — birden fazla listener destekler, { once: true } ile tek seferlik, { capture: true } ile capturing fazında dinleme yapılabilir.

  • 🔹 Event object (e) olay hakkında her bilgiyi taşır: e.target (tıklanan), e.currentTarget (listener'ın sahibi), e.key (basılan tuş), e.preventDefault() (varsayılan davranışı engelle).

  • 🔹 Event'ler DOM ağacında capturing (aşağı) → targetbubbling (yukarı) sırasıyla yayılır. stopPropagation() yayılmayı durdurur.

  • 🔹 Event delegation: Üst elementte tek listener ile alt elementlerin event'lerini yakalama. e.target.closest() ile hedef ayırt edilir. Dinamik içerik ve performans için zorunlu teknik.

  • 🔹 Debounce — event durduğunda çalıştırır (arama, validasyon). Throttle — belirli aralıklarla çalıştırır (scroll, resize). İkisi de sık tetiklenen event'lerin performans maliyetini azaltır.

  • 🔹 Custom Event'ler (new CustomEvent) ile bileşenler arası iletişim sağlanır. detail property'si ile veri taşınır, dispatchEvent() ile tetiklenir.