DOM Projesi: Todo App
Giriş — Öğrenilenleri Birleştirmek
Bu bölümde DOM, element seçme/değiştirme ve event handling konularını öğrendik. Şimdi tüm bu bilgileri bir araya getirerek gerçek bir uygulama inşa edeceğiz: klasik Todo App. Basit gibi görünse de, bir Todo uygulaması CRUD operasyonlarının (Create, Read, Update, Delete) tamamını, localStorage ile veri kalıcılığını, filtreleme mantığını ve event delegation gibi ileri teknikleri kapsar.
Bu proje, sadece "öğrendiğini uygula" değil — profesyonel JavaScript geliştirmede kullanılan tasarım kalıplarını da gösterecek: veriyi DOM'dan ayırmak, tek kaynak doğruluğu (single source of truth), render döngüsü ve modüler yapı.
Analoji: Bir Todo App inşa etmek, araba kullanmayı öğrenen birinin ilk kez trafiğe çıkmasına benzer. Direksiyon, gaz, fren, ayna — hepsini ayrı ayrı biliyorsundur. Ama gerçek trafikte hepsini aynı anda koordine etmek başka bir beceridir. Bu proje, o koordinasyonu sağlayacak.
Proje Mimarisi
Başlamadan önce, uygulamamızın yapısını planlayalım. Profesyonel yaklaşım: veri modeli ve görünüm (DOM) birbirinden ayrılır.
┌──────────────────────────────────────────────────┐
│ Veri Katmanı │
│ todoListesi = [{ id, metin, tamamlandi, tarih }] │
│ localStorage ↔ JSON │
└────────────────────────┬─────────────────────────┘
│ render()
▼
┌──────────────────────────────────────────────────┐
│ Görünüm (DOM) │
│ <ul> → <li> kartları, butonlar, sayaçlar │
└────────────────────────┬─────────────────────────┘
│ event listener'lar
▼
┌──────────────────────────────────────────────────┐
│ Olay Yönetimi │
│ Ekle, Sil, Toggle, Düzenle, Filtrele │
│ → veri modelini güncelle → render() çağır │
└──────────────────────────────────────────────────┘Altın kural: Event handler'lar veriyi değiştirir, sonra render() fonksiyonu veriyi okuyarak DOM'u günceller. DOM'dan doğrudan veri okumayız — veri her zaman JavaScript'teki dizide yaşar.
HTML Yapısı
Önce uygulamamızın iskeletini oluşturalım:
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo Uygulaması</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f0f2f5;
color: #333;
min-height: 100vh;
display: flex;
justify-content: center;
padding-top: 40px;
}
.app { width: 100%; max-width: 520px; padding: 0 16px; }
h1 { text-align: center; margin-bottom: 24px; font-size: 28px; color: #1a1a2e; }
/* Form */
.todo-form {
display: flex; gap: 8px; margin-bottom: 20px;
}
.todo-form input {
flex: 1; padding: 12px 16px; border: 2px solid #ddd;
border-radius: 8px; font-size: 16px; outline: none;
transition: border-color 0.2s;
}
.todo-form input:focus { border-color: #4a90d9; }
.todo-form button {
padding: 12px 20px; background: #4a90d9; color: white;
border: none; border-radius: 8px; font-size: 16px;
cursor: pointer; transition: background 0.2s;
}
.todo-form button:hover { background: #357abd; }
/* Filtreler */
.filtreler {
display: flex; gap: 4px; margin-bottom: 16px;
background: white; padding: 4px; border-radius: 8px;
}
.filtre-btn {
flex: 1; padding: 8px; border: none; background: transparent;
border-radius: 6px; cursor: pointer; font-size: 14px;
transition: all 0.2s;
}
.filtre-btn.aktif { background: #4a90d9; color: white; }
/* Bilgi çubuğu */
.bilgi-cubugu {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 12px; font-size: 14px; color: #666;
}
.temizle-btn {
background: none; border: none; color: #e74c3c;
cursor: pointer; font-size: 14px;
}
.temizle-btn:hover { text-decoration: underline; }
/* Todo listesi */
.todo-listesi { list-style: none; }
.todo-item {
display: flex; align-items: center; gap: 12px;
background: white; padding: 14px 16px; margin-bottom: 8px;
border-radius: 8px; transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.todo-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.12); }
.todo-item.tamamlandi { opacity: 0.6; }
.todo-item.tamamlandi .todo-metin {
text-decoration: line-through; color: #999;
}
.todo-checkbox {
width: 22px; height: 22px; cursor: pointer; accent-color: #4a90d9;
}
.todo-metin {
flex: 1; font-size: 16px; word-break: break-word;
}
.todo-metin-input {
flex: 1; font-size: 16px; padding: 4px 8px;
border: 2px solid #4a90d9; border-radius: 4px; outline: none;
}
.todo-tarih { font-size: 12px; color: #aaa; }
.todo-sil-btn {
background: none; border: none; font-size: 18px;
cursor: pointer; opacity: 0.4; transition: opacity 0.2s;
}
.todo-sil-btn:hover { opacity: 1; }
.bos-mesaj {
text-align: center; padding: 40px; color: #aaa; font-size: 16px;
}
</style>
</head>
<body>
<div class="app">
<h1>📝 Yapılacaklar</h1>
<form class="todo-form" id="todoForm">
<input type="text" id="todoInput" placeholder="Yeni görev ekle..."
autocomplete="off" maxlength="200">
<button type="submit">Ekle</button>
</form>
<div class="filtreler" id="filtreler">
<button class="filtre-btn aktif" data-filtre="hepsi">Hepsi</button>
<button class="filtre-btn" data-filtre="aktif">Aktif</button>
<button class="filtre-btn" data-filtre="tamamlandi">Tamamlanan</button>
</div>
<div class="bilgi-cubugu">
<span id="sayac">0 görev</span>
<button class="temizle-btn" id="temizleBtn">Tamamlananları temizle</button>
</div>
<ul class="todo-listesi" id="todoListesi"></ul>
</div>
<script src="app.js"></script>
</body>
</html>Adım 1: Veri Modeli ve localStorage
Her şey veriyle başlar. Todo'larımızı bir dizide tutacağız ve localStorage ile kalıcı hale getireceğiz.
// ===== VERİ KATMANI =====
// localStorage'dan verileri yükle veya boş dizi başlat
function todoYukle() {
try {
const veri = localStorage.getItem("todolar");
return veri ? JSON.parse(veri) : [];
} catch (hata) {
console.error("localStorage okuma hatası:", hata);
return [];
}
}
// localStorage'a kaydet
function todoKaydet(todolar) {
try {
localStorage.setItem("todolar", JSON.stringify(todolar));
} catch (hata) {
console.error("localStorage yazma hatası:", hata);
// localStorage dolu olabilir (genellikle 5MB limit)
}
}
// Uygulama durumu (state)
let todolar = todoYukle();
let aktifFiltre = "hepsi";Her todo şu yapıda olacak:
// Tek bir todo'nun veri yapısı
// {
// id: 1709283600000, // Benzersiz ID (timestamp)
// metin: "JavaScript öğren", // Görev metni
// tamamlandi: false, // Tamamlandı mı?
// olusturma: "2025-03-01T10:00:00.000Z" // Oluşturma tarihi
// }💡 İpucu: ID olarak Date.now() kullanıyoruz — milisaniye cinsinden zaman damgası benzersiz bir tanımlayıcı olarak yeterli. Profesyonel uygulamalarda UUID veya sunucu tarafında oluşturulan ID'ler kullanılır.
Adım 2: CRUD Operasyonları
Veri katmanındaki temel işlemler:
// ===== CRUD İŞLEMLERİ =====
// CREATE — Yeni todo ekle
function todoEkle(metin) {
const temizMetin = metin.trim();
if (!temizMetin) return null; // Boş görev ekleme
const yeniTodo = {
id: Date.now(),
metin: temizMetin,
tamamlandi: false,
olusturma: new Date().toISOString()
};
todolar.unshift(yeniTodo); // Başa ekle (en yeni en üstte)
todoKaydet(todolar);
return yeniTodo;
}
// READ — Filtreye göre todo'ları getir
function todoFiltrele(filtre) {
switch (filtre) {
case "aktif":
return todolar.filter(t => !t.tamamlandi);
case "tamamlandi":
return todolar.filter(t => t.tamamlandi);
default:
return todolar;
}
}
// UPDATE — Todo durumunu toggle et
function todoToggle(id) {
const todo = todolar.find(t => t.id === id);
if (todo) {
todo.tamamlandi = !todo.tamamlandi;
todoKaydet(todolar);
}
}
// UPDATE — Todo metnini düzenle
function todoDuzenle(id, yeniMetin) {
const temizMetin = yeniMetin.trim();
if (!temizMetin) return false;
const todo = todolar.find(t => t.id === id);
if (todo) {
todo.metin = temizMetin;
todoKaydet(todolar);
return true;
}
return false;
}
// DELETE — Tek todo sil
function todoSil(id) {
const index = todolar.findIndex(t => t.id === id);
if (index !== -1) {
todolar.splice(index, 1);
todoKaydet(todolar);
}
}
// DELETE — Tamamlanmış todo'ları toplu sil
function tamamlananlariTemizle() {
todolar = todolar.filter(t => !t.tamamlandi);
todoKaydet(todolar);
}
// İstatistikler
function todoIstatistik() {
const toplam = todolar.length;
const tamamlanan = todolar.filter(t => t.tamamlandi).length;
const aktif = toplam - tamamlanan;
return { toplam, tamamlanan, aktif };
}Bu fonksiyonlar saf veri fonksiyonlarıdır — DOM ile hiçbir ilgileri yok. Bu ayrım çok önemlidir: veriyi yöneten kod ve DOM'u güncelleyen kod birbirinden bağımsız olmalıdır.
Adım 3: Render Fonksiyonu — Veriyi DOM'a Yansıtma
Render fonksiyonu, veri modelini okur ve DOM'u buna göre günceller. Bu, uygulamamızın tek güncelleme noktasıdır.
// ===== GÖRÜNÜM (RENDER) KATMANI =====
// DOM referansları — bir kere seç, tekrar tekrar kullan
const todoForm = document.querySelector("#todoForm");
const todoInput = document.querySelector("#todoInput");
const todoListesi = document.querySelector("#todoListesi");
const sayacSpan = document.querySelector("#sayac");
const temizleBtn = document.querySelector("#temizleBtn");
const filtreBtnleri = document.querySelectorAll(".filtre-btn");
// Tarih formatlama yardımcısı
function tarihFormatla(isoString) {
const tarih = new Date(isoString);
const simdi = new Date();
const farkMs = simdi - tarih;
const farkDakika = Math.floor(farkMs / 60000);
const farkSaat = Math.floor(farkMs / 3600000);
if (farkDakika < 1) return "Az önce";
if (farkDakika < 60) return `${farkDakika} dk önce`;
if (farkSaat < 24) return `${farkSaat} saat önce`;
return tarih.toLocaleDateString("tr-TR", {
day: "numeric",
month: "short"
});
}
// Tek bir todo'nun HTML'ini oluştur
function todoHTMLOlustur(todo) {
const li = document.createElement("li");
li.className = `todo-item${todo.tamamlandi ? " tamamlandi" : ""}`;
li.dataset.id = todo.id;
li.innerHTML = `
<input type="checkbox" class="todo-checkbox"
${todo.tamamlandi ? "checked" : ""}
aria-label="Görevi tamamla">
<span class="todo-metin">${escapeHTML(todo.metin)}</span>
<span class="todo-tarih">${tarihFormatla(todo.olusturma)}</span>
<button class="todo-sil-btn" aria-label="Görevi sil">✕</button>
`;
return li;
}
// XSS koruması — kullanıcı girdisini HTML'e güvenle ekleme
function escapeHTML(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
// Ana render fonksiyonu
function render() {
// 1. Filtrelenmiş listeyi al
const filtrelenmis = todoFiltrele(aktifFiltre);
// 2. Listeyi güncelle — DocumentFragment ile tek reflow
const fragment = document.createDocumentFragment();
if (filtrelenmis.length === 0) {
const bos = document.createElement("li");
bos.className = "bos-mesaj";
bos.textContent = aktifFiltre === "hepsi"
? "Henüz görev eklenmedi. Yukarıdan yeni bir görev ekleyin!"
: aktifFiltre === "aktif"
? "Tüm görevler tamamlanmış! 🎉"
: "Tamamlanmış görev bulunmuyor.";
fragment.appendChild(bos);
} else {
filtrelenmis.forEach(todo => {
fragment.appendChild(todoHTMLOlustur(todo));
});
}
todoListesi.replaceChildren(fragment);
// 3. Sayaç güncelle
const istatistik = todoIstatistik();
sayacSpan.textContent = `${istatistik.aktif} aktif görev`;
// 4. "Temizle" butonunu göster/gizle
temizleBtn.style.display = istatistik.tamamlanan > 0 ? "inline" : "none";
// 5. Aktif filtre butonunu güncelle
filtreBtnleri.forEach(btn => {
btn.classList.toggle("aktif", btn.dataset.filtre === aktifFiltre);
});
}⚠️ Dikkat: escapeHTML fonksiyonu, kullanıcının girdiği metni HTML olarak yorumlanmadan DOM'a eklememizi sağlar. Örneğin kullanıcı <script>alert(1)</script> yazarsa, bu metin olarak görünür, script olarak çalışmaz. Her zaman kullanıcı girdisini sanitize edin!
Adım 4: Event Handling — Kullanıcı Etkileşimleri
Şimdi tüm kullanıcı etkileşimlerini bağlayalım:
// ===== OLAY YÖNETİMİ =====
// 1. Form submit — Yeni todo ekleme
todoForm.addEventListener("submit", (e) => {
e.preventDefault();
const metin = todoInput.value;
const yeniTodo = todoEkle(metin);
if (yeniTodo) {
todoInput.value = ""; // Input'u temizle
todoInput.focus(); // Focus'u geri ver
// Eğer "tamamlandi" filtresindeyse, "hepsi"ne geç
if (aktifFiltre === "tamamlandi") {
aktifFiltre = "hepsi";
}
render();
}
});
// 2. Todo listesi etkileşimleri — EVENT DELEGATION
todoListesi.addEventListener("click", (e) => {
// Tıklanan öğenin en yakın todo-item'ını bul
const todoItem = e.target.closest(".todo-item");
if (!todoItem) return;
const todoId = parseInt(todoItem.dataset.id, 10);
// Checkbox tıklandıysa → toggle
if (e.target.classList.contains("todo-checkbox")) {
todoToggle(todoId);
render();
return;
}
// Sil butonu tıklandıysa → sil
if (e.target.classList.contains("todo-sil-btn")) {
// Kayma animasyonu
todoItem.style.transform = "translateX(100%)";
todoItem.style.opacity = "0";
todoItem.style.transition = "all 0.3s ease";
// Animasyon bitince sil ve render et
setTimeout(() => {
todoSil(todoId);
render();
}, 300);
return;
}
});
// 3. Çift tıklama ile düzenleme
todoListesi.addEventListener("dblclick", (e) => {
const todoMetin = e.target.closest(".todo-metin");
if (!todoMetin) return;
const todoItem = todoMetin.closest(".todo-item");
const todoId = parseInt(todoItem.dataset.id, 10);
const mevcutMetin = todolar.find(t => t.id === todoId)?.metin;
if (!mevcutMetin) return;
// Metin span'ını input'a dönüştür
const input = document.createElement("input");
input.type = "text";
input.className = "todo-metin-input";
input.value = mevcutMetin;
input.maxLength = 200;
todoMetin.replaceWith(input);
input.focus();
input.select(); // Metni seç
// Düzenlemeyi kaydet
function kaydet() {
const yeniMetin = input.value.trim();
if (yeniMetin && yeniMetin !== mevcutMetin) {
todoDuzenle(todoId, yeniMetin);
}
render(); // Her durumda render et (eski haline dön)
}
// Enter ile kaydet
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
kaydet();
}
if (e.key === "Escape") {
render(); // İptal — eski hale dön
}
});
// Focus kaybedince kaydet
input.addEventListener("blur", kaydet);
});
// 4. Filtre butonları — EVENT DELEGATION
document.querySelector("#filtreler").addEventListener("click", (e) => {
const btn = e.target.closest(".filtre-btn");
if (!btn) return;
aktifFiltre = btn.dataset.filtre;
render();
});
// 5. Tamamlananları temizle
temizleBtn.addEventListener("click", () => {
const tamamlananSayisi = todolar.filter(t => t.tamamlandi).length;
if (confirm(`${tamamlananSayisi} tamamlanmış görev silinecek. Emin misiniz?`)) {
tamamlananlariTemizle();
render();
}
});
// 6. Klavye kısayolları
document.addEventListener("keydown", (e) => {
// Ctrl+/ veya Cmd+/ ile input'a odaklan
if ((e.ctrlKey || e.metaKey) && e.key === "/") {
e.preventDefault();
todoInput.focus();
}
});Adım 5: İleri Özellikler
Sürükle-Bırak ile Sıralama (Basit Versiyon)
// ===== SÜRÜKLE-BIRAK =====
let suruklenenItem = null;
todoListesi.addEventListener("dragstart", (e) => {
const todoItem = e.target.closest(".todo-item");
if (!todoItem) return;
suruklenenItem = todoItem;
todoItem.style.opacity = "0.4";
e.dataTransfer.effectAllowed = "move";
});
todoListesi.addEventListener("dragend", (e) => {
const todoItem = e.target.closest(".todo-item");
if (todoItem) todoItem.style.opacity = "1";
suruklenenItem = null;
// Yeni sırayı kaydet
const yeniSira = [...todoListesi.querySelectorAll(".todo-item")]
.map(item => parseInt(item.dataset.id, 10));
todolar.sort((a, b) => yeniSira.indexOf(a.id) - yeniSira.indexOf(b.id));
todoKaydet(todolar);
});
todoListesi.addEventListener("dragover", (e) => {
e.preventDefault();
const todoItem = e.target.closest(".todo-item");
if (!todoItem || todoItem === suruklenenItem) return;
const rect = todoItem.getBoundingClientRect();
const ortaNokta = rect.top + rect.height / 2;
if (e.clientY < ortaNokta) {
todoItem.before(suruklenenItem);
} else {
todoItem.after(suruklenenItem);
}
});
// Todo kartlarına draggable attribute eklemeyi unutmayın:
// render fonksiyonunda li.draggable = true; ekleyinlocalStorage Değişiklik Senkronizasyonu
Birden fazla sekme açıksa, localStorage değişikliklerini senkronize edin:
// ===== SEKMELER ARASI SENKRONİZASYON =====
// Başka bir sekmede localStorage değiştiğinde
window.addEventListener("storage", (e) => {
if (e.key === "todolar") {
// Başka sekmede güncelleme yapılmış
todolar = e.newValue ? JSON.parse(e.newValue) : [];
render();
console.log("Başka sekmeden güncelleme alındı");
}
});Arama/Filtreleme
// ===== ARAMA =====
const aramaInput = document.createElement("input");
aramaInput.type = "search";
aramaInput.placeholder = "Görev ara...";
aramaInput.className = "arama-input";
// Bu elementi formun altına ekleyebilirsiniz
let aramaSorgusu = "";
aramaInput.addEventListener("input", debounce((e) => {
aramaSorgusu = e.target.value.trim().toLowerCase();
render();
}, 200));
// render() fonksiyonundaki filtrelemeyi güncelle:
function todoFiltreleVeAra(filtre) {
let sonuc = todoFiltrele(filtre);
if (aramaSorgusu) {
sonuc = sonuc.filter(t =>
t.metin.toLowerCase().includes(aramaSorgusu)
);
}
return sonuc;
}
function debounce(fn, bekleme) {
let zamanlayici;
return function(...args) {
clearTimeout(zamanlayici);
zamanlayici = setTimeout(() => fn.apply(this, args), bekleme);
};
}Tüm Kodun Birleştirilmiş Hali
İşte uygulamamızın tamamlanmış app.js dosyası:
// ╔══════════════════════════════════════════════════════════╗
// ║ Todo Uygulaması — app.js ║
// ╚══════════════════════════════════════════════════════════╝
// ===== YARDIMCI FONKSİYONLAR =====
function escapeHTML(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
function debounce(fn, bekleme) {
let zamanlayici;
return function(...args) {
clearTimeout(zamanlayici);
zamanlayici = setTimeout(() => fn.apply(this, args), bekleme);
};
}
function tarihFormatla(isoString) {
const tarih = new Date(isoString);
const simdi = new Date();
const farkMs = simdi - tarih;
const farkDakika = Math.floor(farkMs / 60000);
const farkSaat = Math.floor(farkMs / 3600000);
const farkGun = Math.floor(farkMs / 86400000);
if (farkDakika < 1) return "Az önce";
if (farkDakika < 60) return `${farkDakika} dk önce`;
if (farkSaat < 24) return `${farkSaat} saat önce`;
if (farkGun < 7) return `${farkGun} gün önce`;
return tarih.toLocaleDateString("tr-TR", {
day: "numeric",
month: "short"
});
}
// ===== VERİ KATMANI =====
function todoYukle() {
try {
const veri = localStorage.getItem("todolar");
return veri ? JSON.parse(veri) : [];
} catch {
return [];
}
}
function todoKaydet(todolar) {
try {
localStorage.setItem("todolar", JSON.stringify(todolar));
} catch (hata) {
console.error("Kaydetme hatası:", hata.message);
}
}
// Uygulama durumu
let todolar = todoYukle();
let aktifFiltre = "hepsi";
// CRUD
function todoEkle(metin) {
const temiz = metin.trim();
if (!temiz) return null;
const todo = {
id: Date.now(),
metin: temiz,
tamamlandi: false,
olusturma: new Date().toISOString()
};
todolar.unshift(todo);
todoKaydet(todolar);
return todo;
}
function todoToggle(id) {
const todo = todolar.find(t => t.id === id);
if (todo) {
todo.tamamlandi = !todo.tamamlandi;
todoKaydet(todolar);
}
}
function todoDuzenle(id, yeniMetin) {
const temiz = yeniMetin.trim();
if (!temiz) return false;
const todo = todolar.find(t => t.id === id);
if (todo) {
todo.metin = temiz;
todoKaydet(todolar);
return true;
}
return false;
}
function todoSil(id) {
todolar = todolar.filter(t => t.id !== id);
todoKaydet(todolar);
}
function tamamlananlariTemizle() {
todolar = todolar.filter(t => !t.tamamlandi);
todoKaydet(todolar);
}
function todoFiltrele(filtre) {
switch (filtre) {
case "aktif": return todolar.filter(t => !t.tamamlandi);
case "tamamlandi": return todolar.filter(t => t.tamamlandi);
default: return [...todolar];
}
}
function todoIstatistik() {
const toplam = todolar.length;
const tamamlanan = todolar.filter(t => t.tamamlandi).length;
return { toplam, tamamlanan, aktif: toplam - tamamlanan };
}
// ===== GÖRÜNÜM KATMANI =====
const todoForm = document.querySelector("#todoForm");
const todoInput = document.querySelector("#todoInput");
const todoListesi = document.querySelector("#todoListesi");
const sayacSpan = document.querySelector("#sayac");
const temizleBtn = document.querySelector("#temizleBtn");
const filtreBtnleri = document.querySelectorAll(".filtre-btn");
function todoHTMLOlustur(todo) {
const li = document.createElement("li");
li.className = `todo-item${todo.tamamlandi ? " tamamlandi" : ""}`;
li.dataset.id = todo.id;
li.draggable = true;
li.innerHTML = `
<input type="checkbox" class="todo-checkbox"
${todo.tamamlandi ? "checked" : ""}
aria-label="Görevi tamamla">
<span class="todo-metin">${escapeHTML(todo.metin)}</span>
<span class="todo-tarih">${tarihFormatla(todo.olusturma)}</span>
<button class="todo-sil-btn" aria-label="Görevi sil">✕</button>
`;
return li;
}
function render() {
const filtrelenmis = todoFiltrele(aktifFiltre);
const fragment = document.createDocumentFragment();
if (filtrelenmis.length === 0) {
const bos = document.createElement("li");
bos.className = "bos-mesaj";
const mesajlar = {
hepsi: "Henüz görev eklenmedi. Yukarıdan bir görev ekleyin!",
aktif: "Tüm görevler tamamlanmış! 🎉",
tamamlandi: "Tamamlanmış görev bulunmuyor."
};
bos.textContent = mesajlar[aktifFiltre];
fragment.appendChild(bos);
} else {
filtrelenmis.forEach(todo => {
fragment.appendChild(todoHTMLOlustur(todo));
});
}
todoListesi.replaceChildren(fragment);
const ist = todoIstatistik();
sayacSpan.textContent = `${ist.aktif} aktif görev`;
temizleBtn.style.display = ist.tamamlanan > 0 ? "inline" : "none";
filtreBtnleri.forEach(btn => {
btn.classList.toggle("aktif", btn.dataset.filtre === aktifFiltre);
});
}
// ===== OLAY YÖNETİMİ =====
// Form submit
todoForm.addEventListener("submit", (e) => {
e.preventDefault();
if (todoEkle(todoInput.value)) {
todoInput.value = "";
todoInput.focus();
if (aktifFiltre === "tamamlandi") aktifFiltre = "hepsi";
render();
}
});
// Todo listesi etkileşimleri — Event Delegation
todoListesi.addEventListener("click", (e) => {
const todoItem = e.target.closest(".todo-item");
if (!todoItem) return;
const id = parseInt(todoItem.dataset.id, 10);
if (e.target.classList.contains("todo-checkbox")) {
todoToggle(id);
render();
} else if (e.target.classList.contains("todo-sil-btn")) {
todoItem.style.transform = "translateX(100%)";
todoItem.style.opacity = "0";
todoItem.style.transition = "all 0.3s ease";
setTimeout(() => { todoSil(id); render(); }, 300);
}
});
// Çift tıklama ile düzenleme
todoListesi.addEventListener("dblclick", (e) => {
const metinSpan = e.target.closest(".todo-metin");
if (!metinSpan) return;
const todoItem = metinSpan.closest(".todo-item");
const id = parseInt(todoItem.dataset.id, 10);
const todo = todolar.find(t => t.id === id);
if (!todo) return;
const input = document.createElement("input");
input.type = "text";
input.className = "todo-metin-input";
input.value = todo.metin;
input.maxLength = 200;
metinSpan.replaceWith(input);
input.focus();
input.select();
function kaydet() {
todoDuzenle(id, input.value);
render();
}
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.preventDefault(); kaydet(); }
if (e.key === "Escape") render();
});
input.addEventListener("blur", kaydet);
});
// Filtre butonları
document.querySelector("#filtreler").addEventListener("click", (e) => {
const btn = e.target.closest(".filtre-btn");
if (!btn) return;
aktifFiltre = btn.dataset.filtre;
render();
});
// Tamamlananları temizle
temizleBtn.addEventListener("click", () => {
const sayi = todolar.filter(t => t.tamamlandi).length;
if (confirm(`${sayi} görev silinecek. Emin misiniz?`)) {
tamamlananlariTemizle();
render();
}
});
// Sekmeler arası senkronizasyon
window.addEventListener("storage", (e) => {
if (e.key === "todolar") {
todolar = e.newValue ? JSON.parse(e.newValue) : [];
render();
}
});
// ===== BAŞLANGIÇ =====
render();
todoInput.focus();Projeden Çıkarılacak Dersler
1. Veri ve DOM Ayrımı (Separation of Concerns)
// ❌ Kötü pratik — DOM'dan veri okumak
function todoSayisiniAl() {
return document.querySelectorAll(".todo-item").length;
}
// ✅ İyi pratik — veri modelinden okumak
function todoSayisiniAl() {
return todolar.length;
}Veri her zaman JavaScript'teki dizide yaşar. DOM sadece bu verinin görsel yansımasıdır. Veri değişir → render çağrılır → DOM güncellenir. Asla tersi yapılmaz.
2. Event Delegation
Bu projede todoListesi üzerinde tek bir click listener ile tüm checkbox ve silme butonlarını yönetiyoruz. 100 tane todo olsa bile tek listener yeterli — ve sonradan eklenen todo'lar da otomatik çalışır.
3. XSS Koruması
escapeHTML fonksiyonu, kullanıcı girdisini güvenle DOM'a eklememizi sağlar. innerHTML kullanırken bu koruma şarttır.
4. localStorage ile Kalıcılık
// Hata yönetimi ile localStorage kullanımı
// - JSON.parse/stringify ile veri dönüşümü
// - try/catch ile hata koruması
// - storage event ile sekmeler arası senkronizasyon⚠️ Dikkat: localStorage sınırlamaları:
~5MB limit (tarayıcıya göre değişir)
Sadece string saklar — JSON dönüşümü gerekir
Senkron API — büyük verilerde ana thread'i bloklar
Aynı origin'e özel — başka siteler erişemez
Private/incognito modda kalıcı değildir
5. render() Döngüsü
Tüm state değişikliklerinden sonra render() çağırmak, React ve Vue gibi modern framework'lerin temelini oluşturur. Bu projede bu kalıbı vanilya JavaScript ile öğreniyoruz.
💡 İpucu: Bu Todo App kalıbı, React'ın useState + JSX, Vue'nun reactive + template mantığının saf JavaScript karşılığıdır. Framework öğrenmeye başladığınızda, aslında burada yaptığınız şeyin daha otomatize edilmiş halini gördüğünüzü fark edeceksiniz.
Özet
🔹 Veri ve DOM ayrımı en temel mimari prensiptir. Veri JavaScript dizisinde yaşar, DOM sadece
render()fonksiyonu ile güncellenir.🔹 CRUD operasyonları (Create, Read, Update, Delete) veri katmanında yapılır, ardından
render()çağrılarak DOM güncellenir.🔹 Event delegation ile tek bir üst elementte (liste) dinleyip,
closest()ile hedef elementleri ayırt ediyoruz — performans ve dinamik içerik için zorunlu teknik.🔹 localStorage ile veriler kalıcı hale gelir.
JSON.stringify/parseile dönüşüm,storageevent'i ile sekmeler arası senkronizasyon sağlanır.🔹 XSS koruması: Kullanıcı girdisini
innerHTML'e yazmadan önceescapeHTMLile sanitize edin — güvenlik her şeyden önce gelir.🔹 Bu proje, modern framework'lerin (React, Vue) temel prensiplerinin vanilya JavaScript karşılığıdır: state → render → event → state döngüsü.
AI Asistan
Sorularını yanıtlamaya hazır