Blok 3 · Ders 23
Formlar, kullanıcı girdisi ve localStorage
Formdan veri almak, doğrulamak, listeye eklemek ve silmek; sayfa yenilense de kaybolmasın diye tarayıcıda saklamak.
Bu dersin sonunda
- Formun gönderilmesini
submitolayıyla yakalayıppreventDefaultile sayfanın yenilenmesini durdurabilirsin - Girdi kutusundaki değeri okuyup
Numberya davalueAsNumberile sayıya çevirebilirsin required,min,max,patternnitelikleri ve JavaScript ile girdiyi denetleyip anlaşılır hata mesajı gösterebilirsin- Bir diziden (durumdan) listeyi baştan çizerek ekleme, silme, düzenleme ve filtreleme yapabilirsin
- Veriyi
localStorage'a JSON olarak kaydedip sayfa yenilenince geri yükleyebilir, ilk açılışı ve bozuk kaydı güvenle karşılayabilirsin
Bu sayfada
Ders 22'de sayfaların tıklamalara ve tuşlara tepki verdi; bir kutuya yazılanı da harf harf okudun. Bu derste bir formu baştan sona işleyeceksin: kullanıcının yazdığını okuyacak, denetleyecek, bir listeye ekleyecek, silecek, düzenleyecek ve sayfa kapansa da kaybolmasın diye tarayıcıya kaydedeceksin.
Dersin sonunda elinde küçük ama gerçek bir uygulama olacak: maç günlüğü. Oynadığın her maçın şampiyonunu, KDA'sını, süresini ve sonucunu girersin; uygulama listeyi tutar, kazanma oranını hesaplar, tarayıcıyı kapatıp açtığında her şeyi hatırlar. Yapılacaklar listeleri, alışveriş sepetleri, yorum kutuları: web'deki formların çoğu bu dersteki kalıpla çalışır.
Proje klasörü ve form
C:\kod içinde mac-gunlugu adında yeni bir klasör oluştur ve VS Code'da File → Open Folder ile onu aç. Uygulama üç dosyadan oluşacak: iskelet index.html, görünüm stil.css, davranış app.js. Üçünü de oluştur; app.js şimdilik boş kalsın.
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Maç günlüğü</title> <link rel="stylesheet" href="stil.css"> <script src="app.js" defer></script></head><body> <main> <h1>Maç günlüğü</h1> <form id="macFormu"> <label>Şampiyon <input id="sampiyon" name="sampiyon" placeholder="Ahri"> </label> <label>KDA (kill/ölüm/asist) <input id="kda" name="kda" placeholder="8/2/11"> </label> <label>Süre (dakika) <input id="sure" name="sure" type="number" placeholder="31"> </label> <label>Sonuç <select id="sonuc" name="sonuc"> <option value="zafer">Zafer</option> <option value="bozgun">Bozgun</option> </select> </label> <p id="formHata" class="form-hata" role="alert"></p> <div class="dugmeler"> <button id="kaydetDugmesi" type="submit">Kaydet</button> <button id="vazgecDugmesi" class="ikincil" type="button" hidden>Vazgeç</button> </div> </form> <section> <div class="liste-bas"> <h2>Maçlar</h2> <select id="filtre" aria-label="Hangi maçlar gösterilsin"> <option value="hepsi">Hepsi</option> <option value="zafer">Zaferler</option> <option value="bozgun">Bozgunlar</option> </select> </div> <p id="ozet"></p> <ul id="liste"></ul> </section> </main></body></html>defer sayesinde app.js sayfa okunduktan sonra çalışır ve querySelector öğeleri hazır bulur (Ders 22). Geri kalanın çoğu Ders 19'dan tanıdık. Dört ayrıntıya dikkat et:
- Hem
idhemname.id'yi JavaScript'ten öğeyi bulmak için,name'i form için yazıyoruz: tarayıcı formu gönderirken verileri bu adlarla paketler (Ders 19). label'ın içindeinput. Kutuyu etiketin içine koymak,forile bağlamak kadar geçerli bir yol; etikete tıklayınca imleç kutuya gider.- İki düğme, iki tür. Kaydet
type="submit": formu gönderir. Vazgeçtype="button": kendi başına hiçbir şey yapmaz, formu da göndermez. Ona düzenleme bölümünde JavaScript ile iş vereceğiz. hiddenniteliği öğeyi gizler; Vazgeç şimdilik görünmüyor.#formHata,#ozetve#listede şimdilik boş; onları JavaScript dolduracak.
Görünüm bu dersin konusu değil; aşağıdaki CSS'in neredeyse tamamı Ders 20 ve 21'den. Yazarken grid'in ve flex'in nerede kullanıldığına bak. Yeni olan üç küçük ayrıntıyı hemen altında açıklıyoruz.
* { box-sizing: border-box;} body { margin: 0; font-family: system-ui, sans-serif; background: #f2f3f7; color: #1d1d2b;} main { max-width: 640px; margin: 0 auto; padding: 16px;} form { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 16px; background: #ffffff; border-radius: 12px;} label { display: grid; gap: 4px; font-weight: 600;} input, select, button { font: inherit; padding: 8px 10px; border: 1px solid #c4c7d4; border-radius: 8px;} button { background: #5b4bd6; color: #ffffff; border: 0; cursor: pointer;} .form-hata { grid-column: 1 / -1; margin: 0; color: #c0263b;} .dugmeler { grid-column: 1 / -1; display: flex; gap: 8px;} .liste-bas { display: flex; justify-content: space-between; align-items: center;} #liste { list-style: none; padding: 0;} #liste li { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; padding: 10px 12px; background: #ffffff; border-left: 6px solid #9a9db0; border-radius: 8px;} #liste li span { flex: 1;} #liste .zafer { border-left-color: #1f9d55;} #liste .bozgun { border-left-color: #d64545;} .ikincil,#liste button { background: #e9e8f7; color: #3a2fa3;} #liste button { padding: 4px 10px;} @media (max-width: 480px) { form { grid-template-columns: 1fr; }}grid-column: 1 / -1öğeyi grid'in ilk çizgisinden son çizgisine kadar uzatır. Hata paragrafı ve düğmeler, kaç sütun olursa olsun bütün satırı kaplar.border-leftyalnız sol kenara çizgi çeker;border-left-colorda yalnız o çizginin rengini değiştirir. Zafer ve bozgun sınıfları kalınlığa dokunmadan rengi boyar.system-ui, işletim sisteminin kendi yazı tipidir (Windows'ta Segoe UI).
Sayfayı yerel sunucuyla aç. Live Server kullanıyorsan adres 127.0.0.1:5500/index.html olur, npx serve kullanıyorsan localhost:3000. Ekranda şunu görmelisin:
Form gönderilince ne olur?
Ders 22'de öğrendiğin gibi bir olay dinleyicisi ekle: form gönderilince şampiyonun adı listeye eklensin. app.js'e yaz:
const form = document.querySelector("#macFormu")const sampiyonKutusu = document.querySelector("#sampiyon")const liste = document.querySelector("#liste") form.addEventListener("submit", () => { const satir = document.createElement("li") satir.textContent = sampiyonKutusu.value liste.append(satir) console.log("Listeye eklendi:", sampiyonKutusu.value)})submit, formun gönderildiği an çalışan olaydır. Kutuları doldur (Ahri, 8/2/11, 31) ve Kaydet'e bas. Ahri satırı ya hiç görünmez ya da bir an görünüp kaybolur. Kutular boşalır, konsoldaki mesaj silinir, adres çubuğu da değişir:
127.0.0.1:5500/index.html?sampiyon=Ahri&kda=8%2F2%2F11&sure=31&sonuc=zaferKodun çalıştı; sorun ondan sonra olanda. Bunu Ders 19'da görmüştün: form gönderilince tarayıcı kutuların name=değer çiftlerini adresin sonuna ekler ve sayfayı baştan yükler (/ adreste %2F diye yazılır). Bu, formun varsayılan davranışı; Ders 22'de bağlantıya tıklayınca başka sayfaya gidilmesi gibi. Yeni yüklenen sayfada ne eklediğin satır var ne de JavaScript değişkenlerin: Ders 22'deki benzetmeyle, maç baştan başladı.
Bu davranış, formları sunucunun işlediği sitelerden kalma ve hâlâ geçerli: birçok arama kutusu sayfayı böyle gönderir. Bizim işimizi ise JavaScript yapacak; tarayıcıya "bu sefer gönderme" demeliyiz. Ders 22'de bağlantının varsayılan davranışını preventDefault() ile durdurmuştun; formda da aynı metot çalışır.
Dinleyici olay nesnesini Ders 22'deki gibi olay adıyla alsın ve ilk iş olarak olay.preventDefault() çağırsın:
const form = document.querySelector("#macFormu")const sampiyonKutusu = document.querySelector("#sampiyon")const liste = document.querySelector("#liste") form.addEventListener("submit", (olay) => { olay.preventDefault() const satir = document.createElement("li") satir.textContent = sampiyonKutusu.value liste.append(satir) console.log("Listeye eklendi:", sampiyonKutusu.value)})Listeye eklendi: AhriYeniden dene: satır listede kalır, adres değişmez, mesaj konsolda durur. Şampiyon kutusundayken Enter'a basmak da formu gönderir; tarayıcı bunu Kaydet'e tıklanmış gibi işler ve yine submit çalışır.
Neden düğmenin click'i değil, formun submit'i?
Dinleyiciyi Kaydet düğmesinin click olayına da bağlayabilirdin; Enter'a basınca bile çalışırdı. Asıl fark doğrulamada. Birazdan forma "bu kutu boş geçilemez" gibi kurallar ekleyeceğiz. click bu kurallara bakılmadan önce çalışır, form bomboşken bile. submit ise yalnız bütün kurallar geçince çalışır. Veriyi submit'te almak, bozuk verinin kapıdan girmemesi demek.
Ne zaman çıkar? submit dinleyicisinde olay.preventDefault() yoksa. Hata mesajı çıkmaz; kutular boşalır, liste sıfırlanır ve adres çubuğuna ?sampiyon=Ahri&… gibi bir ek gelir.
Nasıl düzelir? Dinleyici olay parametresini alsın ve ilk satırı olay.preventDefault() olsun. Adres çubuğunda ? ile başlayan beklenmedik bir ek görürsen ilk şüphelin budur.
ReferenceError konsolda bir an görünüp kayboluyorNe zaman çıkar? preventDefault dinleyicinin sonunda dururken ondan önceki bir satır hata verirse. Hata fonksiyonu yarıda keser, preventDefault'a sıra hiç gelmez; tarayıcı formu gönderir, sayfa yenilenir ve konsol temizlenir. Preserve log açıkken gerçek mesajı görürsün:
const form = document.querySelector("#macFormu")const sampiyonKutusu = document.querySelector("#sampiyon")const liste = document.querySelector("#liste") form.addEventListener("submit", (olay) => { const satir = document.createElement("li") satir.textContent = sampiyonKutsu.value liste.append(satir) olay.preventDefault()})Uncaught ReferenceError: sampiyonKutsu is not defined at HTMLFormElement.<anonymous> (app.js:7:25)Nasıl düzelir? Önce yazım hatasını düzelt: sampiyonKutusu. Sonra alışkanlık edin: olay.preventDefault() dinleyicinin ilk satırı olsun. Ondan sonra bir hata çıksa bile sayfa yerinde kalır ve mesaj konsolda durur.
Girdiyi okumak: kutudan gelen her şey metin
Şimdi bütün kutuları okuyalım. Her girdi öğesinin value özelliği içine yazılanı verir; <select>'te ise seçili seçeneğin value'sunu. Aşağıdaki ikinci console.log'da yeni bir sözcük var: typeof (okunuşu "tayp ov"), yanındaki değerin türünü metin olarak söyler. typeof 31 sonucu "number", typeof "31" sonucu "string". Bir değerin sayı mı metin mi olduğundan şüphelendiğinde ilk başvuracağın araç bu.
Listeye ekleme işini birazdan daha iyi bir yolla yazacağız; deneme için app.js'in içeriğini şununla değiştir:
const form = document.querySelector("#macFormu")const sampiyonKutusu = document.querySelector("#sampiyon")const kdaKutusu = document.querySelector("#kda")const sureKutusu = document.querySelector("#sure")const sonucKutusu = document.querySelector("#sonuc") form.addEventListener("submit", (olay) => { olay.preventDefault() console.log(sampiyonKutusu.value, kdaKutusu.value, sureKutusu.value, sonucKutusu.value) console.log(typeof sureKutusu.value)})Ahri 8/2/11 31 zaferstringSüre kutusunun türü number olduğu hâlde typeof string diyor. Kural şu: Girdi kutusundan okunan değer her zaman metindir. type="number" kutunun neyi kabul ettiğini ve telefonda hangi klavyenin açılacağını değiştirir, value'nun türünü değiştirmez.
Bunun neden önemli olduğunu görmek için maçların toplam süresini tutmayı dene. Bu bölümdeki her denemede app.js'in içeriğini yenisiyle değiştir:
const form = document.querySelector("#macFormu")const sureKutusu = document.querySelector("#sure") let toplamSure = 0 form.addEventListener("submit", (olay) => { olay.preventDefault() toplamSure = toplamSure + sureKutusu.value console.log("Toplam süre:", toplamSure)})Süreye önce 31 yazıp Kaydet'e bas, sonra 28 yazıp yine bas:
Toplam süre: 031Toplam süre: 0312859 beklerken 03128 çıktı. 0 + "31" işleminde bir taraf metin olduğu için + toplamadı, yapıştırdı (Ders 1'deki metin + sayı kuralı). Sonuç artık bir metin ve her yeni süre onun sonuna eklendi.
Çözüm, değeri hesaba sokmadan önce sayıya çevirmek. İki yolun var:
Number(sureKutusu.value): Ders 9'daki çevirme.sureKutusu.valueAsNumber: yalnıztype="number"gibi sayısal kutularda çalışan, değeri doğrudan sayı olarak veren özellik.
Farkları boş kutuda ortaya çıkar. Şunu yazıp önce süre 31 iken, sonra süre kutusu boşken Kaydet'e bas:
const form = document.querySelector("#macFormu")const kdaKutusu = document.querySelector("#kda")const sureKutusu = document.querySelector("#sure") form.addEventListener("submit", (olay) => { olay.preventDefault() console.log(Number(sureKutusu.value), sureKutusu.valueAsNumber) console.log(kdaKutusu.valueAsNumber)})31 31NaN0 NaNNaN| Kutuda yazan | value | Number(value) | valueAsNumber |
|---|---|---|---|
31 (sayı kutusu) | "31" | 31 | 31 |
| hiçbir şey (sayı kutusu) | "" | 0 | NaN |
8/2/11 (metin kutusu) | "8/2/11" | NaN | NaN |
Boş kutuyu Number sessizce 0 yapar: 0 dakikalık bir maç kaydedersin ve kimse fark etmez. valueAsNumber ise NaN verir, yani "burada sayı yok" der. Sayı kutularında valueAsNumber'ı tercih et; birazdan required ile boş kutuyu kökünden engelleyeceğiz. Metin kutusunda valueAsNumber hep NaN'dır, çünkü o kutu sayı kutusu değil.
KDA bir metin kutusu ve içinde üç sayı var. Onu Ders 9'daki gibi parçalara ayırıp her parçayı çevirirsin. Bu kısım sayfaya dokunmadığı için Node'da da deneyebilirsin:
const kda = "8/2/11"const parcalar = kda.split("/")console.log(parcalar)console.log(parcalar.map((parca) => Number(parca)))[ '8', '2', '11' ][ 8, 2, 11 ]Uygulamada bunu tek satırda, dizi parçalamayla (Ders 12) yazacağız: const [kill, olum, asist] = kdaKutusu.value.split("/").map((parca) => Number(parca)).
"5" + 1 sonucu "51": girdiyi sayıya çevirmemekNe zaman çıkar? Kutudan okuduğun değeri çevirmeden + ile bir sayıya eklediğinde. Hata mesajı çıkmaz, sonuç sessizce yanlıştır.
const form = document.querySelector("#macFormu")const sureKutusu = document.querySelector("#sure") form.addEventListener("submit", (olay) => { olay.preventDefault() console.log(sureKutusu.value + 1) console.log(Number(sureKutusu.value) + 1)})516Nasıl düzelir? Kutudan gelen değeri hesaba sokmadan önce Number(...) ya da valueAsNumber ile çevir. -, * ve / metni kendiliğinden sayıya çevirdiği için hata yalnız +'da görünür; bu da onu sinsi yapar.
Doğrulama: önce tarayıcı, sonra sen
Kullanıcı her şeyi yazabilir: kutuyu boş bırakabilir, süreye 500 yazabilir, KDA'yı 8-2-11 diye girebilir. Bozuk veri listeye girerse kazanma oranı da KDA da bozulur. Bu yüzden veriyi kabul etmeden önce doğrularız (validation). Ders 13'te fonksiyona gelen değerleri kapıdaki bir bekçiyle denetlemiştin; şimdi bekçiyi kullanıcının yazdığının önüne koyuyoruz. İlk savunma hattı HTML'in kendisi: kutulara birkaç nitelik eklemen yeter. index.html'deki üç input'u şöyle değiştir:
<form id="macFormu"> <label>Şampiyon <input id="sampiyon" name="sampiyon" required maxlength="20" placeholder="Ahri"> </label> <label>KDA (kill/ölüm/asist) <input id="kda" name="kda" required pattern="[0-9]{1,2}/[0-9]{1,2}/[0-9]{1,2}" placeholder="8/2/11" title="Üç sayıyı eğik çizgiyle ayır, ör. 8/2/11"> </label> <label>Süre (dakika) <input id="sure" name="sure" type="number" required min="1" max="120" placeholder="31"> </label>Uzun satırı ikiye böldük; bir etiketin nitelikleri birkaç satıra yayılabilir. Kaydet'e basınca tarayıcı önce bu kuralları denetler. Kurala uymayan ilk kutunun yanında bir uyarı balonu açılır ve submit olayı hiç çalışmaz: dinleyicin bozuk veriyi görmez bile. maxlength="20" uyarı vermez, 20 karakterden fazlasını yazdırmaz.
| Nitelik | Ne ister? | Türkçe Chrome'da mesaj | İngilizce Chrome'da mesaj |
|---|---|---|---|
required | Kutu boş geçilemez | Lütfen bu alanı doldurun. | Please fill out this field. |
min="1" | Sayı en az 1 | Değer 1 veya daha büyük olmalıdır. | Value must be greater than or equal to 1. |
max="120" | Sayı en çok 120 | Değer 120 veya daha küçük olmalıdır. | Value must be less than or equal to 120. |
pattern="…" | Metin bu kalıba uymalı | Lütfen istenen biçimi eşleştirin. | Please match the requested format. |
Mesajları tarayıcı yazar, tarayıcının diliyle. Chrome'un dili Türkçeyse soldaki, İngilizceyse sağdaki sütunu görürsün.
pattern'a yazılan şey bir düzenli ifade (regular expression), yani bir metin kalıbı.
Bütün dilini öğrenmen gerekmiyor; bu kalıbı okumak için üç parça yeter:
[0-9]: 0'dan 9'a kadar bir rakam.{1,2}: hemen önündeki parça bir ya da iki kez./: eğik çizginin kendisi.
Yani [0-9]{1,2}/[0-9]{1,2}/[0-9]{1,2} "bir ya da iki basamaklı sayı, eğik çizgi, yine öyle, yine öyle" demek. Kalıp metnin tamamına uymalı: 8/2/11 ve 08/02/11 geçer; 8-2-11, 8/2, 123/1/1 ve başında boşluk olan 8/2/11 geçmez. Boş kutuya pattern bakmaz; boş kutuyu required yakalar. Kalıbı okumak zor olduğu için ne istediğini title'a insan diliyle yaz; tarayıcı bu açıklamayı uyarıyla birlikte gösterebilir.
Aynı denetimi JavaScript'ten yapmak
Her girdi öğesinin checkValidity() metodu, kurallara uyuyorsa true, uymuyorsa false döndürür. validationMessage özelliği de tarayıcının göstereceği mesajı verir. Formun da bir checkValidity()'si vardır: bütün kutular geçerliyse true. value'ya JavaScript'ten değer de atayabilirsin; kutunun içi değişir. Denemek için app.js'in içeriğini geçici olarak şununla değiştir ve sayfayı yenile:
const form = document.querySelector("#macFormu")const sampiyonKutusu = document.querySelector("#sampiyon")const kdaKutusu = document.querySelector("#kda")const sureKutusu = document.querySelector("#sure") sureKutusu.value = "200"console.log(sureKutusu.checkValidity())console.log(sureKutusu.validationMessage) kdaKutusu.value = "8-2-11"console.log(kdaKutusu.validationMessage) sampiyonKutusu.value = " "console.log(sampiyonKutusu.checkValidity())console.log(form.checkValidity())falseDeğer 120 veya daha küçük olmalıdır.Lütfen istenen biçimi eşleştirin.truefalseİngilizce Chrome'da ikinci ve üçüncü satır Value must be less than or equal to 120. ve Please match the requested format. olur. Dördüncü satıra dikkat: yalnızca üç boşluktan oluşan şampiyon adı required'ı geçti. Tarayıcı için boşluk da bir karakterdir. İşte HTML'in yakalayamadığı ilk kural; onu sen yazacaksın.
Kendi kuralın ve kendi mesajın
Tarayıcının denetimini geçen veri submit dinleyicisine gelir. Kendi kurallarını orada, preventDefault'un hemen altında yazarsın. Kural bozuksa formdaki #formHata paragrafına mesajı yaz, imleci o kutuya götür ve return ile dinleyiciden çık (erken çıkış). app.js'teki geçici denemeyi sil, yerine şunu yaz:
const form = document.querySelector("#macFormu")const sampiyonKutusu = document.querySelector("#sampiyon")const formHata = document.querySelector("#formHata") form.addEventListener("submit", (olay) => { olay.preventDefault() const sampiyon = sampiyonKutusu.value.trim() if (sampiyon === "") { formHata.textContent = "Şampiyon adı yalnız boşluktan oluşamaz." sampiyonKutusu.focus() return } formHata.textContent = "" console.log("Geçerli:", sampiyon)})trim() baştaki ve sondaki boşlukları atar (Ders 9); geriye bir şey kalmadıysa ad boştur. focus() imleci o kutuya götürür. #formHata'nın role="alert" niteliği, mesaj değişince ekran okuyucunun onu sesli okumasını sağlar. Şampiyon kutusuna yalnızca boşluk yaz, ötekileri doldur ve Kaydet'e bas:

Veriden arayüz çizmek
İlk denemede satırı doğrudan listeye ekledik. Ekleme için yeterli; ama silme, düzenleme ve filtreleme gelince bu yol çıkmaza girer: listedeki bir maçın KDA'sını öğrenmek için li'nin metnini ("Zafer · Ahri · 8/2/11 …") yeniden parçalaman gerekir. Sayfa, veriyi saklamak için kötü bir yerdir.
Bunun yerine bütün veriyi bir dizide tutar, ekrandaki listeyi her seferinde o diziden baştan çizeriz. Ders 22'deki dükkânda kural "önce durumu değiştir, sonra ekranı güncelle" idi; ama orada kartların kendisi de durumun bir parçasıydı, bir kartın fiyatını onun dataset'inden okuyordun. Burada bir adım ileri gidiyoruz: sayfa yalnız bir görüntü, bilgi her zaman dizide. Kural basit: veriyi değiştiren her olayın sonunda ekranı yeniden çiz.
Üçüncü adımı (kaydetmeyi) dersin sonunda ekleyeceğiz. Şimdi app.js'i baştan yaz. Önce bütün öğeleri bul ve durumu kur:
const SONUC_ADI = { zafer: "Zafer", bozgun: "Bozgun" } const form = document.querySelector("#macFormu")const sampiyonKutusu = document.querySelector("#sampiyon")const kdaKutusu = document.querySelector("#kda")const sureKutusu = document.querySelector("#sure")const sonucKutusu = document.querySelector("#sonuc")const formHata = document.querySelector("#formHata")const kaydetDugmesi = document.querySelector("#kaydetDugmesi")const vazgecDugmesi = document.querySelector("#vazgecDugmesi")const filtreKutusu = document.querySelector("#filtre")const ozet = document.querySelector("#ozet")const liste = document.querySelector("#liste") let maclar = []SONUC_ADI, "zafer" gibi değerleri ekranda yazılacak hâllerine çevirir: SONUC_ADI[mac.sonuc] köşeli parantezle okuma (Ders 4). Adının büyük harfle yazılması Ders 17'deki DUSMANLAR gibi: "bu hiç değişmeyen bir ayar" demek. maclar'ı let ile açtık, çünkü birazdan ona yeni diziler atayacağız. Her maç bir obje olacak; alanları id, sampiyon, kill, olum, asist, sure ve sonuc.
Durumun altına, bir boş satır bırakıp iki hesap fonksiyonu yaz. kdaMetni, Ders 9'daki fonksiyonun üç sayı yerine bir maç objesi alan hâli. kazanmaOrani zaferleri filter ile sayıp yüzdeye çevirir:
function kdaMetni(mac) { if (mac.olum === 0) { return "Kusursuz" } return ((mac.kill + mac.asist) / mac.olum).toFixed(2)} function kazanmaOrani(tumMaclar) { if (tumMaclar.length === 0) { return "—" } const zaferSayisi = tumMaclar.filter((mac) => mac.sonuc === "zafer").length return "%" + Math.round((zaferSayisi / tumMaclar.length) * 100)}Bu iki fonksiyon hiçbir sayfa öğesine dokunmuyor; aldıklarından bir sonuç üretiyorlar (saf fonksiyon). Böyle fonksiyonları tarayıcı açmadan Node ile deneyebilirsin. Klasörde deneme.js aç, iki fonksiyonu oraya da yaz ve sınır değerleriyle çağır:
function kdaMetni(mac) { if (mac.olum === 0) { return "Kusursuz" } return ((mac.kill + mac.asist) / mac.olum).toFixed(2)} function kazanmaOrani(tumMaclar) { if (tumMaclar.length === 0) { return "—" } const zaferSayisi = tumMaclar.filter((mac) => mac.sonuc === "zafer").length return "%" + Math.round((zaferSayisi / tumMaclar.length) * 100)} console.log(kdaMetni({ kill: 8, olum: 2, asist: 11 }))console.log(kdaMetni({ kill: 3, olum: 0, asist: 17 }))console.log(kazanmaOrani([]))console.log(kazanmaOrani([{ sonuc: "zafer" }, { sonuc: "bozgun" }, { sonuc: "zafer" }]))PS C:\kod\mac-gunlugu> node deneme.js9.50Kusursuz—%67Sayfaya hiç dokunmayan kodu Node'da denemek, tarayıcıda form doldurmaktan çok daha hızlıdır.
if satırları neden var? Onlar olmasaydı hiç maç yokken ve hiç ölmeyen oyuncuda ne çıkacağını Node hemen gösterir:
console.log("%" + Math.round((0 / 0) * 100))console.log(((3 + 17) / 0).toFixed(2))%NaNInfinityBoş liste ve sıfır ölüm: Ders 9'da da karşına çıkan sınır değerleri burada da tuzak kuruyor.
Şimdi çizim fonksiyonu. app.js'e, hesap fonksiyonlarının altına ekle:
function ciz() { liste.replaceChildren() for (const mac of maclar) { const satir = document.createElement("li") satir.classList.add(mac.sonuc) satir.textContent = `${SONUC_ADI[mac.sonuc]} · ${mac.sampiyon} · ${mac.kill}/${mac.olum}/${mac.asist} · KDA ${kdaMetni(mac)} · ${mac.sure} dk` liste.append(satir) } if (maclar.length === 0) { const bos = document.createElement("li") bos.textContent = "Henüz maç yok. İlk maçını yukarıdan ekle." liste.append(bos) } const zaferSayisi = maclar.filter((mac) => mac.sonuc === "zafer").length ozet.textContent = `${maclar.length} maç · ${zaferSayisi} zafer · kazanma oranı ${kazanmaOrani(maclar)}`}liste.replaceChildren()listenin bütün çocuklarını söker; Ders 22'de dükkânı da böyle boşaltmıştın. Her çizim temiz başlar, eski satırlar iki kez görünmez.classList.add(mac.sonuc)satırazaferya dabozgunsınıfını verir; CSS sol kenarı ona göre boyar.- Liste boşsa "Henüz maç yok" yazılır; boş durumu unutmak kullanıcıya bomboş bir ekran göstermek demektir. Özet de her çizimde baştan hesaplanır.
Son olarak submit dinleyicisi maçı diziye ekler ve çizer:
form.addEventListener("submit", (olay) => { olay.preventDefault() const sampiyon = sampiyonKutusu.value.trim() if (sampiyon === "") { formHata.textContent = "Şampiyon adı yalnız boşluktan oluşamaz." sampiyonKutusu.focus() return } const [kill, olum, asist] = kdaKutusu.value.split("/").map((parca) => Number(parca)) maclar.push({ id: Date.now(), sampiyon: sampiyon, kill: kill, olum: olum, asist: asist, sure: sureKutusu.valueAsNumber, sonuc: sonucKutusu.value }) formHata.textContent = "" form.reset() ciz()}) ciz()Dinleyici sayfaya tek bir satır bile eklemiyor: yalnız durumu değiştiriyor (push) ve ciz()'i çağırıyor. form.reset() formu ilk hâline döndürür, Ders 19'daki type="reset" düğmesi gibi: kutular boşalır, Sonuç yeniden Zafer olur. En alttaki ciz() sayfa açılırken bir kez çalışır. Date.now() o anın zamanını büyük bir sayı olarak verir; id: Date.now() bu sayıyı maçın kimlik numarası yapar. Ne işe yaradığını hemen göreceksin. Birkaç maç ekle ve listenin her seferinde baştan kurulduğunu izle.
Ne zaman çıkar? ciz()'in ilk satırını, yani liste.replaceChildren()'ı unutunca. ciz her seferinde bütün diziyi yeniden yazar ama eski satırlar da yerinde kalır. Sayfayı açıp Ahri'yi, sonra Jinx'i ekleyince liste şu dört satır olur:
Henüz maç yok. İlk maçını yukarıdan ekle.Zafer · Ahri · 8/2/11 · KDA 9.50 · 31 dkZafer · Ahri · 8/2/11 · KDA 9.50 · 31 dkBozgun · Jinx · 4/7/9 · KDA 1.86 · 38 dkÜstündeki özet ise doğru: 2 maç · 1 zafer · kazanma oranı %50. Veri sağlam, yalnız görüntü bozuk; çünkü özet her seferinde baştan yazılıyor, liste ise üst üste ekleniyor. Konsolda hata çıkmaz.
Nasıl düzelir? Diziden çizen fonksiyon önce eskisini söker: ciz()'in ilk satırı liste.replaceChildren() olsun.
Silme, düzenleme ve filtreleme
Silme: her maçın bir kimliği olsun
Silmek için hangi maçın silineceğini bilmen gerekir. İlk akla gelen dizideki sıra numarası; ama birazdan listeyi filtreleyeceğiz ve "Bozgunlar" seçiliyken ekrandaki ilk satır dizinin üçüncü elemanı olabilir. İki sırayı karıştırırsan yanlış maçı silersin. Bu yüzden her maça değişmeyen bir kimlik verdik. Oyunun kendisi de böyle yapar: maç geçmişindeki her maçın, liste nasıl sıralanırsa sıralansın değişmeyen bir maç kimliği vardır. Bizim kimliğimiz Date.now(): 1 Ocak 1970'ten bu yana geçen milisaniye sayısı. Elle eklenen iki maçın aynı milisaniyeye düşmesi pratikte imkânsızdır.
Satıra düğme ekleyeceğimiz için metni ayrı bir span'a koyuyoruz. ciz()'deki döngüyü şöyle değiştir:
for (const mac of maclar) { const satir = document.createElement("li") satir.classList.add(mac.sonuc) const bilgi = document.createElement("span") bilgi.textContent = `${SONUC_ADI[mac.sonuc]} · ${mac.sampiyon} · ${mac.kill}/${mac.olum}/${mac.asist} · KDA ${kdaMetni(mac)} · ${mac.sure} dk` const silDugmesi = document.createElement("button") silDugmesi.textContent = "Sil" silDugmesi.addEventListener("click", () => sil(mac.id)) satir.append(bilgi, silDugmesi) liste.append(satir) }Silme fonksiyonunu ciz fonksiyonunun bittiği yerin altına, submit dinleyicisinin üstüne yaz:
function sil(id) { maclar = maclar.filter((mac) => mac.id !== id) ciz()}Her Sil düğmesinin dinleyicisi, döngünün o turundaki mac'i hatırlar (kapanış, Ders 10); bu yüzden her düğme kendi maçının kimliğini gönderir. filter, o kimlik dışındaki maçlardan yeni bir dizi kurar, biz de maclar'ı onunla değiştiririz. Sonra yine ciz(). Eski düğmeler, dinleyicileriyle birlikte replaceChildren() ile sökülür. Ders 22'deki olay yetkilendirmesiyle listeye tek dinleyici koyup kimliği dataset'ten okumak da olurdu; okuması daha kolay olduğu için burada her düğmeye kendi dinleyicisini veriyoruz.
Düzenleme: aynı form, iki iş
Düzenlemek için ayrı bir form kurmayacağız. Düzenle'ye basınca maçın bilgileri forma dolsun ve düğmenin yazısı Güncelle olsun; gönderilince yeni maç eklenmesin, var olan değişsin. Formun hangi işte olduğunu bilmek için durumu bir değişkenle genişlet, let maclar = []'ın altına yaz:
let duzenlenenId = nullnull "şu an düzenlenen bir maç yok" demek (null). Döngüde Sil'in önüne bir Düzenle düğmesi ekle ve append satırını güncelle:
const duzenleDugmesi = document.createElement("button") duzenleDugmesi.textContent = "Düzenle" duzenleDugmesi.addEventListener("click", () => duzenlemeyeBasla(mac)) satir.append(bilgi, duzenleDugmesi, silDugmesi)Düzenlemeyi başlatan ve formu ilk hâline döndüren iki fonksiyonu sil'in üstüne yaz:
function duzenlemeyeBasla(mac) { duzenlenenId = mac.id sampiyonKutusu.value = mac.sampiyon kdaKutusu.value = `${mac.kill}/${mac.olum}/${mac.asist}` sureKutusu.value = mac.sure sonucKutusu.value = mac.sonuc kaydetDugmesi.textContent = "Güncelle" vazgecDugmesi.hidden = false sampiyonKutusu.focus()} function formuTemizle() { duzenlenenId = null form.reset() formHata.textContent = "" kaydetDugmesi.textContent = "Kaydet" vazgecDugmesi.hidden = true}hidden niteliği JavaScript'te aynı adlı bir özellik olarak durur: false yapınca Vazgeç görünür. Şimdi submit dinleyicisinde const [kill, olum, asist] satırından dinleyicinin sonuna kadar olan kısmı şununla değiştir; dosyanın en altındaki ciz() yerinde kalsın. Ekleme de güncelleme de aynı bilgileri kullandığı için onları önce bir bilgiler objesinde topluyoruz:
const [kill, olum, asist] = kdaKutusu.value.split("/").map((parca) => Number(parca)) const bilgiler = { sampiyon: sampiyon, kill: kill, olum: olum, asist: asist, sure: sureKutusu.valueAsNumber, sonuc: sonucKutusu.value } if (duzenlenenId === null) { maclar.push({ id: Date.now(), ...bilgiler }) } else { maclar = maclar.map((mac) => { if (mac.id === duzenlenenId) { return { ...mac, ...bilgiler } } return mac }) } formuTemizle() ciz()}) vazgecDugmesi.addEventListener("click", formuTemizle)map her maçı olduğu gibi geri verir; yalnız kimliği eşleşeni { ...mac, ...bilgiler } ile değiştirir. Yaymada (Ders 12) sonra gelen kazanır: önce eski maçın bütün alanları (kimlik dahil) kopyalanır, sonra bilgiler'deki yeni değerler onların üstüne yazılır. Vazgeç, hiçbir şeyi değiştirmeden formu temizler.
Bir sınır durumu daha: düzenlediğin maçı silersen ne olur? Form Güncelle modunda kalır, Güncelle'ye basınca map silinmiş maçı bulamaz ve yazdıkların sessizce kaybolur. sil'e bir denetim ekle:
function sil(id) { maclar = maclar.filter((mac) => mac.id !== id) if (id === duzenlenenId) { formuTemizle() } ciz()}Jinx maçında Düzenle'ye basıp KDA'yı değiştirince form şöyle görünür:

Listede Jinx'in eski KDA'sı duruyor: Güncelle'ye basana kadar durum değişmedi, ekran da değişmedi.
Filtreleme: diziyi değil görünümü süz
Filtre maclar dizisini değiştirmez; yalnız neyin gösterileceğini seçer. Bu yüzden maclar'ı filtreleyip üstüne yazmayız; yazarsak gizlenen maçlar silinmiş olur.
ciz() içinde ayrı bir gorunenler dizisi kurar, döngüyü onun üzerinde döndürürüz. ciz()'in başını ve boş liste kısmını şöyle değiştir:
function ciz() { liste.replaceChildren() const gorunenler = maclar.filter((mac) => filtreKutusu.value === "hepsi" || mac.sonuc === filtreKutusu.value) for (const mac of gorunenler) { if (gorunenler.length === 0) { const bos = document.createElement("li") if (maclar.length === 0) { bos.textContent = "Henüz maç yok. İlk maçını yukarıdan ekle." } else { bos.textContent = "Bu filtreye uyan maç yok." } liste.append(bos) }Seçim değişince yeniden çiz; bu satırı dosyanın en sonundaki ciz() çağrısının üstüne ekle. change olayı, bir <select>'te seçim değiştiğinde çalışır:
filtreKutusu.addEventListener("change", ciz)Özet satırı bilerek bütün maclar'dan hesaplanıyor: "Bozgunlar" seçiliyken kazanma oranının %0 görünmesini istemeyiz.

localStorage: sayfa kapansa da unutmasın
Birkaç maç ekle ve F5'e bas. Liste boş; Ders 22'deki dükkânda yenileyince altının sıfırlanması gibi. maclar dizisi programın belleğinde yaşıyordu ve sayfa yenilenince program baştan başladı. Verinin kalıcı olması için onu bir yere yazmamız gerekir. Sunucu ve veritabanı Blok 4 ve 5'in konusu; şimdilik tarayıcının kendi küçük deposu yeter.
Bu derste "depo" dediğimizde bu dolabı kastediyoruz; Ders 7'deki Git deposuyla ilgisi yok. Türkçe geliştirici araçları da ona Yerel depolama der.
Üç metot
Denemeleri maç günlüğünü bozmadan yapmak için klasöre iki küçük dosya aç. depo.html yalnız betiği yükler:
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <title>Depo denemeleri</title> <script src="depo.js" defer></script></head><body> <p>Sonuçlar konsolda.</p></body></html>depo.js'e şunu yaz ve 127.0.0.1:5500/depo.html'i açıp konsola bak:
localStorage.setItem("sihirdarAdi", "Ahri Main")console.log(localStorage.getItem("sihirdarAdi"))console.log(localStorage.getItem("yokBoyleBirAnahtar"))localStorage.removeItem("sihirdarAdi")console.log(localStorage.getItem("sihirdarAdi"))Ahri MainnullnullsetItem(anahtar, değer): değeri o anahtarın altına yazar. Anahtar zaten varsa eski değerin üstüne yazar.getItem(anahtar): değeri okur. Öyle bir anahtar yoksanulldöndürür.removeItem(anahtar): anahtarı ve değerini siler.
Sonraki denemelerde depo.js'in içeriğini her seferinde yenisiyle değiştir ve sayfayı yenile. depo.html ile maç günlüğü aynı adresten açıldığı için aynı depoyu kullanır; denemelerde maç günlüğünün kullanmayacağı anahtar adları seçtik.
Yalnız metin
Depoya bir sayı ve bir dizi koy, geri oku:
localStorage.setItem("seviye", 18)const seviye = localStorage.getItem("seviye")console.log(seviye, typeof seviye)console.log(seviye + 1) localStorage.setItem("favoriler", ["Ahri", "Lux"])console.log(localStorage.getItem("favoriler"))18 string181Ahri,LuxsetItem her değeri metne çevirerek saklar; sayı koysan da metin geri gelir. Girdi kutularındaki tuzağın aynısı. Bir dizinin metin hâli de elemanlarının virgülle yan yana yazılmasıdır: köşeli parantezler ve tırnaklar gitti, elinde artık bir dizi değil "Ahri,Lux" metni var. Bir diziyi ya da objeyi saklayacaksan önce JSON metnine çevirmelisin:
const mac = { sampiyon: "Ahri", kill: 8 }localStorage.setItem("sonMac", JSON.stringify(mac))console.log(localStorage.getItem("sonMac"))const geriGelen = JSON.parse(localStorage.getItem("sonMac"))console.log(geriGelen.sampiyon, geriGelen.kill + 1){"sampiyon":"Ahri","kill":8}Ahri 9Saklanan şey artık JSON metni; JSON.parse ondan yeniden bir obje kurdu ve kill yine sayı. Maç günlüğünde bu işi iki fonksiyon yapacak:
[object Object]: objeyi JSON.stringify olmadan saklamakNe zaman çıkar? setItem'a objeyi doğrudan verince. setItem onu metne çevirir; bir objenin metin hâli de [object Object]'tir (Ders 4'te metinle + yaparken görmüştün). Asıl veri kaybolmuştur; geri okumaya kalkınca JSON.parse patlar.
const mac = { sampiyon: "Ahri", kill: 8 }localStorage.setItem("sonMac", mac)console.log(localStorage.getItem("sonMac"))const okunan = JSON.parse(localStorage.getItem("sonMac"))[object Object]Uncaught SyntaxError: "[object Object]" is not valid JSON at JSON.parse (<anonymous>) at depo.js:4:21Nasıl düzelir? Yazarken JSON.stringify(mac), okurken JSON.parse(...). Depoda [object Object] gördüğün bir kayıt kurtarılamaz; anahtarı sil ve doğru kodla yeniden kaydet. Dizide hata mesajı bile çıkmaz: "Ahri,Lux" sessizce saklanır, sorunu ancak okurken fark edersin.
Maç günlüğüne kaydetmek ve yüklemek
Şimdi app.js'e dön. Dosyanın en üstüne bir anahtar adı ekle; let duzenlenenId = null satırının altına da bir boş satır bırakıp kaydet fonksiyonunu yaz:
const ANAHTAR = "macGunlugu.maclar"function kaydet() { localStorage.setItem(ANAHTAR, JSON.stringify(maclar))}Anahtarın adına uygulamanın adını koyduk; nedenini birazdan göreceksin. Durumu değiştiren her yerin sonunda, ciz()'den önce kaydet() çağır: submit dinleyicisinde formuTemizle()'nin üstüne ve sil fonksiyonunda ciz()'in üstüne. Yükleme için ilk akla gelen, let maclar = [] satırını şöyle yazmak:
let maclar = JSON.parse(localStorage.getItem(ANAHTAR))Sayfayı yenile. Konsol kırmızı:
TypeError: Cannot read properties of null (reading 'filter')Ne zaman çıkar? Uygulama ilk kez açıldığında. O anahtarla hiç kayıt yapılmadığı için getItem null döndürür, JSON.parse(null) da hata vermeden null verir. maclar artık bir dizi değil null'dır ve onu dizi gibi kullanan ilk satır patlar.
let maclar = JSON.parse(localStorage.getItem(ANAHTAR))Uncaught TypeError: Cannot read properties of null (reading 'filter') at ciz (app.js:40:31) at app.js:142:1Uncaught TypeError: Cannot read properties of null (reading 'push') at HTMLFormElement.<anonymous> (app.js:124:16)İlk hata sayfa açılınca ciz()'deki filter satırında, ikincisi Kaydet'e basınca push satırında çıktı. Boş satırların bizimkinden farklıysa satır numaraları sende biraz kayar. Mesaj hatanın çıktığı yeri gösterir; sebebi 16. satırdaki okuma. Ders 13'teki kuralı hatırla: patladığı yer, sebebin olduğu yer değildir. null ile ilgili bir hatada "bu değer nereden geldi?" diye geriye doğru iz sür.
Bu hatayı görmüyorsan büyük ihtimalle arada Kaydet'e basıp bir kayıt oluşturdun; o zaman getItem metin döndürür ve sorun saklanır. Kaydı birazdan göreceğin Application panelinden silip yeniden dene.
Nasıl düzelir? Okuduğun değer null'sa boş bir diziyle başla. Doğru yukle fonksiyonu az aşağıda.
İlk açılışta kayıt olmaması normal bir durum, hata değil. Okumayı bir fonksiyona taşı ve null'ı ayrıca karşıla. 16. satırı şöyle değiştir, yukle fonksiyonunu da kaydet'in üstüne yaz:
let maclar = yukle()function yukle() { const kayit = localStorage.getItem(ANAHTAR) if (kayit === null) { return [] } return JSON.parse(kayit)}yukle bir fonksiyon bildirimi olduğu için tanımından önceki satırda çağrılabilir (hoisting, Ders 10). Bu hâliyle ilk açılış sorunsuz. Ama bir sorun daha var: kayıt bozulursa?
SyntaxError: "undefined" is not valid JSONNe zaman çıkar? Depodaki metin JSON değilse. Mesajı Ders 12'den tanıyorsun. Sık yaşanan bir yol: kaydet'i parametreli yazıp (function kaydet(liste)) bir yerde argümanı unutmak. JSON.stringify(undefined) metin değil undefined döndürür, setItem da onu "undefined" metni olarak saklar. Bir sonraki açılışta:
function yukle() { const kayit = localStorage.getItem(ANAHTAR) if (kayit === null) { return [] } return JSON.parse(kayit)}Uncaught SyntaxError: "undefined" is not valid JSON at JSON.parse (<anonymous>) at yukle (app.js:24:17) at app.js:16:14Daha kötüsü, hata 16. satırdaki yukle() çağrısından yükseldiği için dosyanın geri kalanı hiç çalışmaz: submit dinleyicisi de eklenmez, Kaydet'e basınca sayfa yenilenir. Bozuk kayıt yerinde durduğu için hata her açılışta tekrar eder; kullanıcın için uygulama tamamen bozulmuştur.
Nasıl düzelir? Önce bozuk kaydı Application panelinden sil ve kaydet'i doğru çağır. Sonra yukle'yi bozuk kayda dayanıklı yap: JSON.parse'ı try/catch içine al (Ders 13).
yukle'nin son hâli:
function yukle() { const kayit = localStorage.getItem(ANAHTAR) if (kayit === null) { return [] } try { return JSON.parse(kayit) } catch (hata) { console.warn("Kayıt okunamadı, boş listeyle başlıyorum:", hata.message) return [] }}Kayıt okunamadı, boş listeyle başlıyorum: "undefined" is not valid JSONconsole.warn konsola sarı bir uyarı yazar. Uygulama boş listeyle açılır ve çalışır; ilk kaydette bozuk metnin üstüne doğru veri yazılır. Bozuk veri kaybolur ama uygulama kurtulur. Depodan gelen veriye de, dışarıdan gelen her veri gibi, körü körüne güvenme.
Depoya bakmak: Application paneli
F12 → Application sekmesi (Türkçe arayüzde Uygulama) → solda Storage (Depolama) altında Local storage (Yerel depolama) → http://127.0.0.1:5500. Sekme görünmüyorsa sekme satırının sonundaki » işaretine tıkla; yer kalmayan sekmeler orada durur.
Bu sitenin bütün anahtarları ve değerleri bir tabloda durur. Bir satırı seçince değeri alt bölmede görünür: geçerli bir JSON dizisi ya da objesiyse açılır bir ağaç olarak, Ahri,Lux ya da [object Object] gibi JSON olmayan bir metinse düz yazı olarak. Depoya doğru yazıp yazmadığını anlamanın en hızlı yolu bu.
Bir hücreye çift tıklayıp değeri düzenleyebilir, bir satırı seçip Delete ile silebilir, araç çubuğundaki düğmeyle hepsini temizleyebilirsin. Depo denemelerinden kalan seviye, favoriler ve sonMac da orada; işin bitince onları sil. Bozuk kayıt denemesi için de en hızlı araç bu: macGunlugu.maclar'ın değerini çift tıklayıp undefined yap, sayfayı yenile ve yukle'nin sarı uyarısını gör. DevTools sürümden sürüme biraz değişir; düğmelerin yeri farklıysa aradığın adlar aynıdır.
Ne zaman çıkar? Her kaydı aynı anahtarla tek tek saklarsan. setItem anahtar varsa sormadan üstüne yazar; elinde hep yalnız son kayıt kalır.
const form = document.querySelector("#macFormu")const sampiyonKutusu = document.querySelector("#sampiyon")const sonucKutusu = document.querySelector("#sonuc") form.addEventListener("submit", (olay) => { olay.preventDefault() const mac = { sampiyon: sampiyonKutusu.value, sonuc: sonucKutusu.value } localStorage.setItem("mac", JSON.stringify(mac)) console.log("Kayıtlı:", localStorage.getItem("mac"))})Kayıtlı: {"sampiyon":"Ahri","sonuc":"zafer"}Kayıtlı: {"sampiyon":"Jinx","sonuc":"bozgun"}Kayıtlı: {"sampiyon":"Lux","sonuc":"zafer"}Application panelinde tek bir mac anahtarı ve içinde yalnız Lux görürsün.
Nasıl düzelir? Bütün maçları bir dizide tut ve diziyi tek bir anahtarla kaydet: setItem(ANAHTAR, JSON.stringify(maclar)). Her kayıtta bütün dizi yeniden yazılır, hiçbir maç kaybolmaz.
Uygulamanın tamamı
Bütün parçalar yerine oturunca app.js şöyle olmalı. Parçaları anlatılan yerlere yazdıysan dosyan bununla satırı satırına aynıdır; hata izlerindeki satır numaraları da buna göre. Kendi dosyanla karşılaştır.
Fonksiyonların sırası farklıysa da çalışır; ama ANAHTAR, let maclar = yukle() satırından önce durmalı. Sonra tanımlarsan sayfa Uncaught ReferenceError: Cannot access 'ANAHTAR' before initialization hatasıyla açılır: const ile açılan bir kutu, tanımlandığı satırdan önce kullanılamaz.
const ANAHTAR = "macGunlugu.maclar"const SONUC_ADI = { zafer: "Zafer", bozgun: "Bozgun" } const form = document.querySelector("#macFormu")const sampiyonKutusu = document.querySelector("#sampiyon")const kdaKutusu = document.querySelector("#kda")const sureKutusu = document.querySelector("#sure")const sonucKutusu = document.querySelector("#sonuc")const formHata = document.querySelector("#formHata")const kaydetDugmesi = document.querySelector("#kaydetDugmesi")const vazgecDugmesi = document.querySelector("#vazgecDugmesi")const filtreKutusu = document.querySelector("#filtre")const ozet = document.querySelector("#ozet")const liste = document.querySelector("#liste") let maclar = yukle()let duzenlenenId = null function yukle() { const kayit = localStorage.getItem(ANAHTAR) if (kayit === null) { return [] } try { return JSON.parse(kayit) } catch (hata) { console.warn("Kayıt okunamadı, boş listeyle başlıyorum:", hata.message) return [] }} function kaydet() { localStorage.setItem(ANAHTAR, JSON.stringify(maclar))} function kdaMetni(mac) { if (mac.olum === 0) { return "Kusursuz" } return ((mac.kill + mac.asist) / mac.olum).toFixed(2)} function kazanmaOrani(tumMaclar) { if (tumMaclar.length === 0) { return "—" } const zaferSayisi = tumMaclar.filter((mac) => mac.sonuc === "zafer").length return "%" + Math.round((zaferSayisi / tumMaclar.length) * 100)} function ciz() { liste.replaceChildren() const gorunenler = maclar.filter((mac) => filtreKutusu.value === "hepsi" || mac.sonuc === filtreKutusu.value) for (const mac of gorunenler) { const satir = document.createElement("li") satir.classList.add(mac.sonuc) const bilgi = document.createElement("span") bilgi.textContent = `${SONUC_ADI[mac.sonuc]} · ${mac.sampiyon} · ${mac.kill}/${mac.olum}/${mac.asist} · KDA ${kdaMetni(mac)} · ${mac.sure} dk` const duzenleDugmesi = document.createElement("button") duzenleDugmesi.textContent = "Düzenle" duzenleDugmesi.addEventListener("click", () => duzenlemeyeBasla(mac)) const silDugmesi = document.createElement("button") silDugmesi.textContent = "Sil" silDugmesi.addEventListener("click", () => sil(mac.id)) satir.append(bilgi, duzenleDugmesi, silDugmesi) liste.append(satir) } if (gorunenler.length === 0) { const bos = document.createElement("li") if (maclar.length === 0) { bos.textContent = "Henüz maç yok. İlk maçını yukarıdan ekle." } else { bos.textContent = "Bu filtreye uyan maç yok." } liste.append(bos) } const zaferSayisi = maclar.filter((mac) => mac.sonuc === "zafer").length ozet.textContent = `${maclar.length} maç · ${zaferSayisi} zafer · kazanma oranı ${kazanmaOrani(maclar)}`} function duzenlemeyeBasla(mac) { duzenlenenId = mac.id sampiyonKutusu.value = mac.sampiyon kdaKutusu.value = `${mac.kill}/${mac.olum}/${mac.asist}` sureKutusu.value = mac.sure sonucKutusu.value = mac.sonuc kaydetDugmesi.textContent = "Güncelle" vazgecDugmesi.hidden = false sampiyonKutusu.focus()} function formuTemizle() { duzenlenenId = null form.reset() formHata.textContent = "" kaydetDugmesi.textContent = "Kaydet" vazgecDugmesi.hidden = true} function sil(id) { maclar = maclar.filter((mac) => mac.id !== id) if (id === duzenlenenId) { formuTemizle() } kaydet() ciz()} form.addEventListener("submit", (olay) => { olay.preventDefault() const sampiyon = sampiyonKutusu.value.trim() if (sampiyon === "") { formHata.textContent = "Şampiyon adı yalnız boşluktan oluşamaz." sampiyonKutusu.focus() return } const [kill, olum, asist] = kdaKutusu.value.split("/").map((parca) => Number(parca)) const bilgiler = { sampiyon: sampiyon, kill: kill, olum: olum, asist: asist, sure: sureKutusu.valueAsNumber, sonuc: sonucKutusu.value } if (duzenlenenId === null) { maclar.push({ id: Date.now(), ...bilgiler }) } else { maclar = maclar.map((mac) => { if (mac.id === duzenlenenId) { return { ...mac, ...bilgiler } } return mac }) } kaydet() formuTemizle() ciz()}) vazgecDugmesi.addEventListener("click", formuTemizle)filtreKutusu.addEventListener("change", ciz) ciz()Beş maç ekledikten sonra uygulama şöyle görünür:

Sayfayı yenile, tarayıcıyı kapatıp aç: maçlar yerinde. Application panelinde macGunlugu.maclar anahtarının değeri şöyle başlar:
[{"id":1791724265534,"sampiyon":"Ahri","kill":8,"olum":2,"asist":11,"sure":31,"sonuc":"zafer"},{"id":1791724265582,"sampiyon":"Jinx","kill":4,"olum":7,"asist":9,"sure":38,"sonuc":"bozgun"},…id'ler, eklediğin anın zamanı olduğu için sende farklı olur.
Görevler
Görevleri C:\kod\mac-gunlugu klasöründe yap. Şampiyon araması maç günlüğünün kendisine eklenir; ötekiler için görevde adı geçen yeni .html ve .js dosyalarını aç. Onlar 127.0.0.1:5500/altin.html gibi adreslerde açılır. Hepsi aynı kökende olduğu için anahtar adlarının başına ders23. koyuyoruz. Önce kendin dene, takılırsan ipuçlarını sırayla aç.
Altın kesesi
altin.html ve altin.js aç. Sayfada bir sayı kutusu ve Ekle düğmesi olsun; her gönderimde kutudaki miktar keseye eklensin ve sayfadaki yazı güncellensin. Kutu boş geçilemesin; miktar en az 1, en çok 5000 olsun. Sayfa Kesede 0 altın var. ile açılır; 300 ve 150 ekledikten sonra şunu göstermeli:
Kesede 450 altın var.Kesede 0300150 altın var. görürsen bir şeyi çevirmeyi unuttun.
İpucu 1
Formun submit olayını dinle; ilk satır olay.preventDefault(). Kurallar için kutuya required, min ve max yaz.
İpucu 2
Toplamı dinleyicinin dışında let toplamAltin = 0 ile tut. Kutunun değerini valueAsNumber ile oku; gönderimden sonra altinFormu.reset() kutuyu boşaltır.
Çözümü göster
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <title>Altın kesesi</title> <script src="altin.js" defer></script></head><body> <h1>Altın kesesi</h1> <form id="altinFormu"> <label>Kazandığın altın <input id="miktar" type="number" required min="1" max="5000"> </label> <button type="submit">Ekle</button> </form> <p id="kese">Kesede 0 altın var.</p></body></html>const altinFormu = document.querySelector("#altinFormu")const miktarKutusu = document.querySelector("#miktar")const kese = document.querySelector("#kese") let toplamAltin = 0 altinFormu.addEventListener("submit", (olay) => { olay.preventDefault() toplamAltin = toplamAltin + miktarKutusu.valueAsNumber kese.textContent = `Kesede ${toplamAltin} altın var.` altinFormu.reset()})Sınır denemesi: 0, -5, 5001 ve boş kutu gönderilmez; tarayıcı balonla uyarır (Türkçe Chrome'da 0 için Değer 1 veya daha büyük olmalıdır.). 5000 kabul edilir ve kese 5450 olur.
Sihirdar adını hatırla
sihirdar.html ve sihirdar.js aç. Bir kutuya sihirdar adını yazıp Hatırla'ya basınca ad ders23.sihirdarAdi anahtarıyla kaydedilsin ve sayfanın üstündeki yazı şöyle olsun:
Tekrar hoş geldin, Ahri Main!Sayfa yenilenince de aynı selam görünsün. Hiç ad kaydedilmemişse Merhaba, yabancı. yazsın. Ayrı bir Unut düğmesi kaydı silip selamı Merhaba, yabancı. yapsın. Baştaki ve sondaki boşluklar kaydedilmesin; yalnız boşluktan oluşan ad kaydedilmesin, onun yerine Ad yalnız boşluktan oluşamaz. yazsın.
İpucu 1
Tek bir metin sakladığın için JSON gerekmez: setItem(ANAHTAR, ad) ve getItem(ANAHTAR) yeter.
İpucu 2
Selamı yazan bir selamla() fonksiyonu kur. Onu sayfa açılınca, kaydettikten sonra ve Unut'tan sonra çağır. getItem null döndürüyorsa kullanıcı yabancıdır.
İpucu 3
Unut için removeItem. Boşluk denetimi için adKutusu.value.trim() ve if (ad === ""), sonra return.
Çözümü göster
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <title>Sihirdar</title> <script src="sihirdar.js" defer></script></head><body> <p id="selam">Merhaba, yabancı.</p> <form id="adFormu"> <label>Sihirdar adın <input id="ad" required maxlength="16"> </label> <button type="submit">Hatırla</button> </form> <button id="unutDugmesi" type="button">Unut</button></body></html>const ANAHTAR = "ders23.sihirdarAdi"const selam = document.querySelector("#selam")const adFormu = document.querySelector("#adFormu")const adKutusu = document.querySelector("#ad")const unutDugmesi = document.querySelector("#unutDugmesi") function selamla() { const ad = localStorage.getItem(ANAHTAR) if (ad === null) { selam.textContent = "Merhaba, yabancı." } else { selam.textContent = `Tekrar hoş geldin, ${ad}!` }} adFormu.addEventListener("submit", (olay) => { olay.preventDefault() const ad = adKutusu.value.trim() if (ad === "") { selam.textContent = "Ad yalnız boşluktan oluşamaz." return } localStorage.setItem(ANAHTAR, ad) adFormu.reset() selamla()}) unutDugmesi.addEventListener("click", () => { localStorage.removeItem(ANAHTAR) selamla()}) selamla()Unut düğmesi formun dışında, yani formu zaten gönderemez; type="button" yine de iyi bir alışkanlık: düğmeyi bir gün forma taşırsan sürpriz yaşamazsın. Sınır denemesi: üç boşluk yazınca mesaj çıkar ve depoya bir şey yazılmaz (getItem hâlâ null); Ahri Main depoya Ahri Main olarak girer; Unut'tan sonra anahtar silinir ve yenileyince yine yabancısın.
Şampiyon araması
Maç günlüğüne bir arama kutusu ekle: yazdıkça liste yalnız şampiyon adında aranan metin geçen maçları göstersin.
- Büyük-küçük harf fark etmesin:
AHRyazınca Ahri'nin maçları çıksın. - Baştaki ve sondaki boşluklar yok sayılsın:
luxLux'u bulsun. - Arama, sonuç filtresiyle birlikte çalışsın:
ahriyazıp "Bozgunlar"ı seçince yalnız Ahri'nin bozgunları görünsün. - Eşleşme yoksa
Bu filtreye uyan maç yok.görünsün; arama kutusu boşken bütün maçlar.
İpucu 1
index.html'de <h2>Maçlar</h2>'nin altına <input id="arama" type="search" placeholder="Şampiyon ara" aria-label="Şampiyon ara"> ekle. Yazdıkça tepki vermek için input olayını dinle (Ders 22): aramaKutusu.addEventListener("input", ciz).
İpucu 2
Durumu değiştirmiyorsun; yalnız ciz()'deki gorunenler hesabını genişletiyorsun. Bir maç ancak iki koşula birden uyarsa görünür: sonuç filtresi ve ad araması.
İpucu 3
mac.sampiyon.toLowerCase().includes(aranan). aranan da önce trim(), sonra toLowerCase() görmüş olmalı. Boş metin her metnin içinde "geçer": "Ahri".includes("") true verir; arama boşken bütün maçların görünmesi bu sayede kendiliğinden olur.
Çözümü göster
index.html'de .liste-bas'ın içi:
<h2>Maçlar</h2> <input id="arama" type="search" placeholder="Şampiyon ara" aria-label="Şampiyon ara"> <select id="filtre" aria-label="Hangi maçlar gösterilsin">app.js'te üç değişiklik. Öğelerin arasına:
const aramaKutusu = document.querySelector("#arama")ciz()'deki gorunenler satırının yerine:
const aranan = aramaKutusu.value.trim().toLowerCase() const gorunenler = maclar.filter((mac) => { const sonucUyuyor = filtreKutusu.value === "hepsi" || mac.sonuc === filtreKutusu.value const adUyuyor = mac.sampiyon.toLowerCase().includes(aranan) return sonucUyuyor && adUyuyor })Dosyanın sonuna, filtreKutusu'nun dinleyicisinin altına:
aramaKutusu.addEventListener("input", ciz)Sınır denemesi (Ahri, Jinx, Lux, Irelia ve ikinci bir Ahri maçıyla): boş arama beş maçı da gösterir; AHR iki Ahri maçını; lux Lux'u; irelia ve IRELIA Irelia'yı; zzz "Bu filtreye uyan maç yok." mesajını. ahri + "Bozgunlar" yalnız Ahri'nin bozgununu gösterir, özet ise hep beş maçı sayar.
Neden Ders 9'daki toLocaleLowerCase("tr") değil? Şampiyon adları İngilizce. Türkçe kuralla "Irelia".toLocaleLowerCase("tr") sonucu "ırelia" olur ve irelia araması onu bulamaz. Hangi harf kuralını kullanacağın, metnin hangi dilde olduğuna bağlı.
Riot ID kutusu
riot.html ve riot.js aç. Riot ID'nin biçimi ad#etiket: ad 3–16 harf, rakam ya da boşluk; etiket 3–5 harf ya da rakam (Ahri#TR1, Ahri Main#TR1). Riot'un gerçek kuralları daha ayrıntılı; görevde sade bir hâlini uyguluyoruz.
- Kural HTML'de dursun:
required,pattern,maxlength. - Kullanıcı yazdıkça kutunun altında senin Türkçe mesajın görünsün: biçim doğruysa
Biçim doğru., değilse ne beklendiğini anlatan bir cümle, kutu boşsa hiçbir şey. - Gönderilince
Kaydedildi: Ahri#TR1yazsın; baştaki ve sondaki boşluklar atılsın.
Ah#TR1, Ahri#TR, AhriTR1 ve 17 harfli bir ad geçersiz; Ahri#TR1, Ahri Main#TR1 ve 16 harfli bir ad geçerli olmalı.
İpucu 1
Kalıp: [A-Za-z0-9 ]{3,16}#[A-Za-z0-9]{3,5}. Köşeli parantezin içindeki boşluk "boşluk da olur" demek. En uzun Riot ID 16 + 1 + 5 = 22 karakter: maxlength="22".
İpucu 2
Canlı mesaj için kutunun input olayını dinle ve riotKutusu.checkValidity()'ye bak. Önce kutunun boş olup olmadığını denetle; boş kutu da required yüzünden geçersiz sayılır ama o anda hata mesajı göstermek kullanıcıyı boşuna azarlamak olur.
Çözümü göster
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <title>Riot ID</title> <script src="riot.js" defer></script></head><body> <form id="riotFormu"> <label>Riot ID <input id="riotId" required maxlength="22" pattern="[A-Za-z0-9 ]{3,16}#[A-Za-z0-9]{3,5}" placeholder="Ahri#TR1" title="Ad, # ve etiket. Ör. Ahri#TR1"> </label> <button type="submit">Kaydet</button> </form> <p id="durum"></p></body></html>const riotFormu = document.querySelector("#riotFormu")const riotKutusu = document.querySelector("#riotId")const durum = document.querySelector("#durum") riotKutusu.addEventListener("input", () => { if (riotKutusu.value === "") { durum.textContent = "" } else if (riotKutusu.checkValidity()) { durum.textContent = "Biçim doğru." } else { durum.textContent = "Biçim: ad#etiket. Ad 3–16, etiket 3–5 harf ya da rakam (ör. Ahri#TR1)." }}) riotFormu.addEventListener("submit", (olay) => { olay.preventDefault() durum.textContent = `Kaydedildi: ${riotKutusu.value.trim()}` riotFormu.reset()})Sınır denemesi: Ahri#TR123 (5 karakterlik etiket) ve Abcdefghijklmnop#TR1 (16 harf) geçerli; Ahri#TR1234, Ahri##TR1 ve Abcdefghijklmnopq#TR1 (17 harf) geçersiz. Ağrı#TR1 de geçersiz: [A-Za-z] yalnız İngilizce harfleri kapsar. Ahri#TR1 (başta boşluk) kalıba uyar, çünkü adda boşluk serbest; trim() onu Kaydedildi: Ahri#TR1'e çevirir. Ahri#TR1 (sonda boşluk) uymaz, çünkü etikette boşluk yok. Geçersiz ID'yle Kaydet'e basınca tarayıcının balonu çıkar (Lütfen istenen biçimi eşleştirin.) ve submit çalışmaz.
Favori şampiyonlar
favori.html ve favori.js ile bir favori şampiyon listesi kur:
- Formdan ad eklenir. Liste
ders23.favorileranahtarıyla JSON olarak saklanır; sayfa yenilense de kalır. - Her satırda Yıldızla ve Sil düğmeleri var. Yıldızla, adın başına
★koyar ve düğmenin yazısınıYıldızı kaldıryapar; tekrar basınca geri alır. - Aynı ad, büyük-küçük harf farkı gözetmeden ikinci kez eklenemez: Ahri listedeyken
ahriyazıncaahri zaten listede.mesajı çıkar. - Yalnız boşluktan oluşan ad eklenmez. Liste boşken
Henüz favorin yok.yazar.
İpucu 1
Maç günlüğünün iskeletini izle: favoriler dizisi (durum), yukle(), kaydet(), ciz(). Her favori bir obje: { ad: "Lux", yildizli: false }.
İpucu 2
Adlar tekrarsız olacağı için kimlik yerine adı kullanabilirsin. Var mı denetimi için some (Ders 11): favoriler.some((f) => f.ad.toLowerCase() === ad.toLowerCase()).
İpucu 3
Yıldızlamak bir düzenleme: map ile o favorinin kopyasını { ...f, yildizli: !f.yildizli } olarak kur. ! doğruyu yanlışa, yanlışı doğruya çevirir. Her değişiklikten sonra kaydet() ve ciz().
Çözümü göster
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <title>Favori şampiyonlar</title> <script src="favori.js" defer></script></head><body> <h1>Favori şampiyonlar</h1> <form id="favoriFormu"> <label>Şampiyon <input id="favoriAd" required maxlength="20"> </label> <button type="submit">Ekle</button> </form> <p id="favoriHata"></p> <ul id="favoriListe"></ul></body></html>const ANAHTAR = "ders23.favoriler"const favoriFormu = document.querySelector("#favoriFormu")const adKutusu = document.querySelector("#favoriAd")const favoriHata = document.querySelector("#favoriHata")const favoriListe = document.querySelector("#favoriListe") let favoriler = yukle() function yukle() { const kayit = localStorage.getItem(ANAHTAR) if (kayit === null) { return [] } try { return JSON.parse(kayit) } catch (hata) { return [] }} function kaydet() { localStorage.setItem(ANAHTAR, JSON.stringify(favoriler))} function ciz() { favoriListe.replaceChildren() if (favoriler.length === 0) { const bos = document.createElement("li") bos.textContent = "Henüz favorin yok." favoriListe.append(bos) return } for (const favori of favoriler) { const satir = document.createElement("li") const ad = document.createElement("span") const yildizDugmesi = document.createElement("button") if (favori.yildizli) { ad.textContent = `★ ${favori.ad}` yildizDugmesi.textContent = "Yıldızı kaldır" } else { ad.textContent = favori.ad yildizDugmesi.textContent = "Yıldızla" } yildizDugmesi.addEventListener("click", () => { favoriler = favoriler.map((f) => { if (f.ad === favori.ad) { return { ...f, yildizli: !f.yildizli } } return f }) kaydet() ciz() }) const silDugmesi = document.createElement("button") silDugmesi.textContent = "Sil" silDugmesi.addEventListener("click", () => { favoriler = favoriler.filter((f) => f.ad !== favori.ad) kaydet() ciz() }) satir.append(ad, " ", yildizDugmesi, " ", silDugmesi) favoriListe.append(satir) }} favoriFormu.addEventListener("submit", (olay) => { olay.preventDefault() favoriHata.textContent = "" const ad = adKutusu.value.trim() if (ad === "") { favoriHata.textContent = "Ad yalnız boşluktan oluşamaz." return } const zatenVar = favoriler.some((f) => f.ad.toLowerCase() === ad.toLowerCase()) if (zatenVar) { favoriHata.textContent = `${ad} zaten listede.` return } favoriler.push({ ad: ad, yildizli: false }) kaydet() favoriFormu.reset() ciz()}) ciz()append'e metin de verebilirsin: " " düğmelerin arasına boşluk koyar. Ahri, Lux ve Jinx'i ekleyip ahri'yi bir daha eklemeye çalıştıktan ve Lux'u yıldızladıktan sonra sayfa şöyle görünür (CSS yazmadığımız için tarayıcının varsayılan görünümü):
Sınır denemesi: ahri ve LUX "zaten listede" der; üç boşluk eklenmez; jinx boşlukları atılarak eklenir. Yenileyince yıldız yerinde durur; depoda [{"ad":"Ahri","yildizli":false},{"ad":"Lux","yildizli":true},{"ad":"jinx","yildizli":false}] gibi bir değer olur. Hepsini silince depoda [] kalır ve yenileyince yine Henüz favorin yok. görünür.
Kendini sına
Soru 1
Süre kutusunda 7 yazıyor. Bu satır ne yazar?
console.log(sureKutusu.value * 2, sureKutusu.value + 2)Cevabı göster
14 72. value metindir: "7". * metni kendiliğinden sayıya çevirir, "7" * 2 sonucu 14 olur. + ise bir taraf metin olunca yapıştırır: "72". Kutudan gelen değeri her zaman önce çevir.
Soru 2
Bu satırlar ne yazar?
localStorage.setItem("altin", 300)const altin = localStorage.getItem("altin")console.log(altin + 50, altin - 50)Cevabı göster
30050 250. setItem sayıyı metin olarak sakladı, getItem de "300" metnini döndürdü. "300" + 50 yapıştırır, "300" - 50 hesaplar. Doğrusu Number(localStorage.getItem("altin")) ya da veriyi JSON olarak saklamak.
Soru 3
hicYok anahtarıyla hiç kayıt yapılmadı. Hangi satır hata verir, mesajı ne olur?
const kayit = JSON.parse(localStorage.getItem("hicYok"))console.log(kayit.length)Cevabı göster
İlk satır hata vermez: getItem null döndürür, JSON.parse(null) da null verir. İkinci satır TypeError: Cannot read properties of null (reading 'length') ile patlar. Hatanın çıktığı yer 2. satır, sebebi 1. satırdaki karşılanmamış null. Çözüm yukle'deki gibi null'ı ayrıca karşılamak.
Soru 4
Dinleyiciyi neden formun submit olayına bağlıyoruz da Kaydet düğmesinin click olayına bağlamıyoruz?
Cevabı göster
Çünkü submit yalnız required, min, max, pattern gibi kurallar geçince çalışır; click ise doğrulamadan önce, form bomboşken bile çalışır. Enter'a basmak iki olayı da tetikler; asıl fark doğrulamada.
Soru 5
required olan şampiyon kutusuna yalnız üç boşluk yazıp (öteki kutular doğru) Kaydet'e basarsan submit olayı çalışır mı?
Cevabı göster
Çalışır. Tarayıcı için boşluk da bir karakterdir; required yalnız tamamen boş kutuyu durdurur (checkValidity() bu durumda true verir). Bu kuralı kendin yazarsın: value.trim() boşsa mesaj göster ve return ile çık.
Soru 6
Filtre "Bozgunlar"dayken ekrandaki ilk satırın Sil düğmesine basıldı. Silmeyi ekrandaki sıraya göre yapsaydık (ekranın ilk satırı = dizinin ilk elemanı) ne olurdu?
Cevabı göster
Yanlış maç silinirdi. Filtre zaferleri gizlediği için ekranın ilk satırı dizinin ilk elemanı olmayabilir; dizinin başında bir zafer duruyorsa o silinirdi. Bu yüzden her maçın ekranın düzeninden bağımsız, değişmeyen bir id'si var ve silme filter((mac) => mac.id !== id) ile yapılıyor.
Soru 7
Dün 127.0.0.1:5500/index.html'den açıp maç eklemiştin. Bugün localhost:5500/index.html'den açtın ve liste boş. Maçların silindi mi?
Cevabı göster
Hayır. localStorage kökene bağlıdır ve 127.0.0.1:5500 ile localhost:5500 iki ayrı köken sayılır. Sayfayı 127.0.0.1:5500'den aç, maçların orada.
Kayıt noktası
Bu derste yeni bir proje klasörü açtın; Git'in onu tanıması için orada bir kez git init gerekiyor.
Bir sonraki derste veriyi kendin girmek yerine internetten çekeceksin: fetch ile bir API'den bütün şampiyon listesini alıp sayfada kartlara dökecek, yüklenme ve hata durumlarını göstereceksin.