20-bo‘lim
Amaliy loyiha - vazifalar ilovasi
Butun kursda o'rganilgan bilimlarni birlashtirib, to'liq ishlaydigan vazifalar ro'yxati ilovasini yozamiz.
Ushbu bo‘lim mundarijasi
Yigirma bo'lim davomida o'rgangan hamma narsani bitta ilovada birlashtiramiz: holat boshqaruvi, DOM, hodisalar, saqlash va filtrlash.
Nima quramiz #
| Imkoniyat | Qaysi bo'limdan |
|---|---|
| Massiv metodlari | 7 - filter, map, reduce |
| Obyektlar va spread | 8 - holat yangilash |
| DOM yaratish | 13 - createElement, fragment |
| Delegatsiya | 14 - bitta ishlovchi |
| Forma va validatsiya | 15 - submit, tekshiruv |
localStorage | Bu bo'limda |
HTML tuzilmasi #
<div class="ilova">
<h1>Vazifalar</h1>
<form id="qoshishForma">
<input type="text" id="yangiVazifa" placeholder="Yangi vazifa"
required minlength="2" autocomplete="off">
<button type="submit">Qo'shish</button>
</form>
<div class="filtrlar" id="filtrlar">
<button data-filtr="hammasi" class="faol">Hammasi</button>
<button data-filtr="faol">Bajarilmagan</button>
<button data-filtr="bajarilgan">Bajarilgan</button>
</div>
<ul id="royxat"></ul>
<footer>
<span id="hisoblagich"></span>
<button id="tozalash">Bajarilganlarni o'chirish</button>
</footer>
</div>
1-qadam: holat va amallar #
function ilovaYaratish() {
let holat = {
vazifalar: [],
filtr: "hammasi",
keyingiId: 1
};
function qoshish(matn) {
const tozalangan = matn.trim();
if (tozalangan.length < 2) {
return { ok: false, sabab: "Kamida 2 belgi kerak" };
}
holat = {
...holat,
vazifalar: [
...holat.vazifalar,
{ id: holat.keyingiId, matn: tozalangan, bajarildi: false }
],
keyingiId: holat.keyingiId + 1
};
return { ok: true };
}
function almashtirish(id) {
holat = {
...holat,
vazifalar: holat.vazifalar.map((v) =>
v.id === id ? { ...v, bajarildi: !v.bajarildi } : v
)
};
}
function ochirish(id) {
holat = {
...holat,
vazifalar: holat.vazifalar.filter((v) => v.id !== id)
};
}
function bajarilganlarniTozalash() {
holat = {
...holat,
vazifalar: holat.vazifalar.filter((v) => !v.bajarildi)
};
}
function filtrOrnatish(filtr) {
holat = { ...holat, filtr };
}
function korinadiganlar() {
switch (holat.filtr) {
case "faol":
return holat.vazifalar.filter((v) => !v.bajarildi);
case "bajarilgan":
return holat.vazifalar.filter((v) => v.bajarildi);
default:
return holat.vazifalar;
}
}
function statistika() {
const jami = holat.vazifalar.length;
const bajarilgan = holat.vazifalar.filter((v) => v.bajarildi).length;
return { jami, bajarilgan, qolgan: jami - bajarilgan };
}
return {
qoshish, almashtirish, ochirish, bajarilganlarniTozalash,
filtrOrnatish, korinadiganlar, statistika,
holatniOlish: () => holat
};
}
// Sinab ko'ramiz
const ilova = ilovaYaratish();
console.log("Qisqa matn:", ilova.qoshish("a"));
console.log("To'g'ri :", ilova.qoshish("Non olish"));
ilova.qoshish("Kitob o'qish");
ilova.qoshish("Sport bilan shug'ullanish");
ilova.almashtirish(1);
console.log("Statistika:", ilova.statistika());
console.log("Hammasi :", ilova.korinadiganlar().length);
ilova.filtrOrnatish("faol");
console.log("Faqat faol:", ilova.korinadiganlar().map((v) => v.matn));
ilova.filtrOrnatish("bajarilgan");
console.log("Bajarilgan:", ilova.korinadiganlar().map((v) => v.matn));
Qisqa matn: { ok: false, sabab: 'Kamida 2 belgi kerak' }
To'g'ri : { ok: true }
Statistika: { jami: 3, bajarilgan: 1, qolgan: 2 }
Hammasi : 3
Faqat faol: [ "Kitob o'qish", "Sport bilan shug'ullanish" ]
Bajarilgan: [ 'Non olish' ]
E'tibor bering - hech bir amal massivni joyida o'zgartirmaydi:
// O'ZGARTIRADI - qochish kerak
holat.vazifalar.push(yangi);
vazifa.bajarildi = !vazifa.bajarildi;
// YANGI yaratadi - to'g'ri
holat = { ...holat, vazifalar: [...holat.vazifalar, yangi] };
Nima uchun bu muhim:
| Foyda | Izoh |
|---|---|
| Taqqoslash oson | eski !== yangi - o'zgardimi? |
| Tarix saqlash | Bekor qilish (undo) oson |
| Xatolarni topish | Kim o'zgartirdi degan savol yo'q |
| React/Vue moslik | Ular aynan shunga tayanadi |
Kamchiligi - juda katta massivda nusxalash qimmat. Lekin minglab element uchun bu sezilmaydi.
2-qadam: chizish #
function chizuvchiYaratish(hujjat) {
function vazifaElementi(vazifa) {
const li = hujjat.createElement("li");
li.className = vazifa.bajarildi ? "vazifa bajarilgan" : "vazifa";
li.dataset.id = String(vazifa.id);
const katakcha = hujjat.createElement("input");
katakcha.type = "checkbox";
katakcha.checked = vazifa.bajarildi;
katakcha.dataset.amal = "almashtirish";
const matn = hujjat.createElement("span");
matn.textContent = vazifa.matn; // textContent - XSS dan himoya
const ochirish = hujjat.createElement("button");
ochirish.textContent = "O'chirish";
ochirish.dataset.amal = "ochirish";
li.append(katakcha, matn, ochirish);
return li;
}
function chizish(idish, vazifalar) {
const fragment = hujjat.createDocumentFragment();
if (vazifalar.length === 0) {
const bosh = hujjat.createElement("li");
bosh.className = "bosh";
bosh.textContent = "Vazifa yo'q";
fragment.appendChild(bosh);
} else {
for (const v of vazifalar) {
fragment.appendChild(vazifaElementi(v));
}
}
idish.replaceChildren(fragment);
}
return { chizish, vazifaElementi };
}
// Node da DOM yo'q - hujjat o'rniga soddalashtirilgan taqlid beramiz
const soxtaHujjat = {
createElement(teg) {
return {
tagName: teg.toUpperCase(),
className: "", textContent: "", dataset: {},
children: [],
append(...bolalar) { this.children.push(...bolalar); }
};
},
createDocumentFragment() {
return { children: [], appendChild(b) { this.children.push(b); } };
}
};
const chizuvchi = chizuvchiYaratish(soxtaHujjat);
const element = chizuvchi.vazifaElementi({ id: 1, matn: "Non", bajarildi: true });
console.log("Teg :", element.tagName);
console.log("Klass :", element.className);
console.log("data-id :", element.dataset.id);
console.log("Bolalar :", element.children.length);
console.log("Matn :", element.children[1].textContent);
console.log("Katakcha :", element.children[0].checked);
Teg : LI
Klass : vazifa bajarilgan
data-id : 1
Bolalar : 3
Matn : Non
Katakcha : true
3-qadam: brauzerda bog'lash #
Endi hammasini haqiqiy DOM bilan bog'laymiz. Bu kod brauzerda ishlaydi:
const ilova = ilovaYaratish();
const royxat = document.getElementById("royxat");
const forma = document.getElementById("qoshishForma");
const maydon = document.getElementById("yangiVazifa");
const hisoblagich = document.getElementById("hisoblagich");
const filtrlar = document.getElementById("filtrlar");
function yangilash() {
chizish(royxat, ilova.korinadiganlar());
const { jami, qolgan } = ilova.statistika();
hisoblagich.textContent = `${qolgan} ta qoldi (jami ${jami})`;
saqlash();
}
// Qo'shish
forma.addEventListener("submit", (hodisa) => {
hodisa.preventDefault();
const natija = ilova.qoshish(maydon.value);
if (!natija.ok) {
maydon.setCustomValidity(natija.sabab);
maydon.reportValidity();
return;
}
maydon.setCustomValidity("");
maydon.value = "";
maydon.focus();
yangilash();
});
// Belgilash va o'chirish - BITTA ishlovchi (delegatsiya)
royxat.addEventListener("click", (hodisa) => {
const tugma = hodisa.target.closest("[data-amal]");
if (!tugma) {
return;
}
const id = Number(tugma.closest(".vazifa").dataset.id);
if (tugma.dataset.amal === "almashtirish") {
ilova.almashtirish(id);
} else if (tugma.dataset.amal === "ochirish") {
ilova.ochirish(id);
}
yangilash();
});
// Filtrlar
filtrlar.addEventListener("click", (hodisa) => {
const tugma = hodisa.target.closest("[data-filtr]");
if (!tugma) {
return;
}
for (const t of filtrlar.querySelectorAll("[data-filtr]")) {
t.classList.toggle("faol", t === tugma);
}
ilova.filtrOrnatish(tugma.dataset.filtr);
yangilash();
});
document.getElementById("tozalash").addEventListener("click", () => {
ilova.bajarilganlarniTozalash();
yangilash();
});
yuklash();
yangilash();
4-qadam: localStorage ga saqlash #
const KALIT = "vazifalar-ilovasi";
function saqlovchiYaratish(ombor) {
function saqlash(holat) {
try {
ombor.setItem(KALIT, JSON.stringify({
vazifalar: holat.vazifalar,
keyingiId: holat.keyingiId
}));
return true;
} catch (xato) {
// Maxfiy rejim yoki joy tugagan - ilova baribir ishlashi kerak
console.log("Saqlab bo'lmadi:", xato.constructor.name);
return false;
}
}
function yuklash() {
try {
const xom = ombor.getItem(KALIT);
if (!xom) {
return null;
}
const malumot = JSON.parse(xom);
// Ishonmang - tekshiring
if (!Array.isArray(malumot.vazifalar)) {
return null;
}
return {
vazifalar: malumot.vazifalar.filter(
(v) => typeof v.id === "number" && typeof v.matn === "string"
),
keyingiId: Number(malumot.keyingiId) || 1
};
} catch (xato) {
return null; // buzilgan JSON
}
}
return { saqlash, yuklash };
}
// Omborni taqlid qilamiz - haqiqiy ilovada bu localStorage bo'ladi
const soxtaOmbor = {
malumot: {},
setItem(k, q) { this.malumot[k] = q; },
getItem(k) { return this.malumot[k] ?? null; }
};
const saqlovchi = saqlovchiYaratish(soxtaOmbor);
console.log("Bo'sh omborda:", saqlovchi.yuklash());
saqlovchi.saqlash({
vazifalar: [{ id: 1, matn: "Non", bajarildi: false }],
keyingiId: 2
});
const yuklangan = saqlovchi.yuklash();
console.log("Yuklandi :", yuklangan.vazifalar.length, "ta vazifa");
console.log("Matn :", yuklangan.vazifalar[0].matn);
console.log("keyingiId :", yuklangan.keyingiId);
// Buzilgan ma'lumot
soxtaOmbor.malumot[KALIT] = "{buzuq json";
console.log("Buzilgan JSON:", saqlovchi.yuklash());
// Noto'g'ri tuzilma
soxtaOmbor.malumot[KALIT] = '{"vazifalar":"massiv emas"}';
console.log("Noto'g'ri tur:", saqlovchi.yuklash());
Bo'sh omborda: null
Yuklandi : 1 ta vazifa
Matn : Non
keyingiId : 2
Buzilgan JSON: null
Noto'g'ri tur: null
localStorage ga hech qachon ishonmangUchta muammo bor va uchalasini ham hal qilish kerak:
1. Yozish yiqilishi mumkin
Maxfiy rejimda yoki joy tugaganda setItem xato tashlaydi:
QuotaExceededError: Failed to execute 'setItem'
Har doim try/catch ga o'rang - saqlash yiqilsa ham ilova
ishlashda davom etsin.
2. Ma'lumot buzilgan bo'lishi mumkin
Foydalanuvchi F12 orqali qiymatni o'zgartirishi mumkin.
Boshqa versiyangiz boshqa tuzilma saqlagan bo'lishi ham
mumkin.
Shuning uchun JSON.parse dan keyin tuzilmani tekshiring:
if (!Array.isArray(malumot.vazifalar)) {
return null;
}
3. Hajmi cheklangan
Odatda 5-10 MB. Katta ma'lumot uchun IndexedDB ishlating.
Va eng muhimi: localStorage maxfiy emas. U shifrlanmagan
holda saqlanadi va JavaScript orqali o'qiladi. Parol, token yoki
shaxsiy ma'lumotni u yerda saqlamang.
To'liq ilova #
Yuqoridagi bo'laklarni birlashtirsak, ~150 qatorlik to'liq ishlaydigan ilova hosil bo'ladi:
// vazifalar.js - to'liq ilova (brauzerda ishlaydi)
const KALIT = "vazifalar-ilovasi";
let holat = { vazifalar: [], filtr: "hammasi", keyingiId: 1 };
const royxat = document.getElementById("royxat");
const forma = document.getElementById("qoshishForma");
const maydon = document.getElementById("yangiVazifa");
const hisoblagich = document.getElementById("hisoblagich");
const filtrlar = document.getElementById("filtrlar");
/* ---------- saqlash ---------- */
function saqlash() {
try {
localStorage.setItem(KALIT, JSON.stringify({
vazifalar: holat.vazifalar,
keyingiId: holat.keyingiId
}));
} catch {
/* saqlash imkonsiz - ilova baribir ishlaydi */
}
}
function yuklash() {
try {
const malumot = JSON.parse(localStorage.getItem(KALIT) ?? "null");
if (malumot && Array.isArray(malumot.vazifalar)) {
holat = { ...holat, ...malumot };
}
} catch {
/* buzilgan ma'lumot - bo'sh ro'yxatdan boshlaymiz */
}
}
/* ---------- chizish ---------- */
function korinadiganlar() {
if (holat.filtr === "faol") {
return holat.vazifalar.filter((v) => !v.bajarildi);
}
if (holat.filtr === "bajarilgan") {
return holat.vazifalar.filter((v) => v.bajarildi);
}
return holat.vazifalar;
}
function vazifaElementi(vazifa) {
const li = document.createElement("li");
li.className = vazifa.bajarildi ? "vazifa bajarilgan" : "vazifa";
li.dataset.id = String(vazifa.id);
const katakcha = document.createElement("input");
katakcha.type = "checkbox";
katakcha.checked = vazifa.bajarildi;
katakcha.dataset.amal = "almashtirish";
const matn = document.createElement("span");
matn.textContent = vazifa.matn;
const ochirish = document.createElement("button");
ochirish.textContent = "O'chirish";
ochirish.dataset.amal = "ochirish";
li.append(katakcha, matn, ochirish);
return li;
}
function yangilash() {
const korsatiladigan = korinadiganlar();
const fragment = document.createDocumentFragment();
if (korsatiladigan.length === 0) {
const bosh = document.createElement("li");
bosh.className = "bosh";
bosh.textContent = "Vazifa yo'q";
fragment.appendChild(bosh);
} else {
for (const v of korsatiladigan) {
fragment.appendChild(vazifaElementi(v));
}
}
royxat.replaceChildren(fragment);
const jami = holat.vazifalar.length;
const qolgan = holat.vazifalar.filter((v) => !v.bajarildi).length;
hisoblagich.textContent = `${qolgan} ta qoldi (jami ${jami})`;
saqlash();
}
/* ---------- hodisalar ---------- */
forma.addEventListener("submit", (hodisa) => {
hodisa.preventDefault();
const matn = maydon.value.trim();
if (matn.length < 2) {
maydon.setCustomValidity("Kamida 2 belgi kerak");
maydon.reportValidity();
return;
}
maydon.setCustomValidity("");
holat = {
...holat,
vazifalar: [...holat.vazifalar,
{ id: holat.keyingiId, matn, bajarildi: false }],
keyingiId: holat.keyingiId + 1
};
maydon.value = "";
maydon.focus();
yangilash();
});
royxat.addEventListener("click", (hodisa) => {
const boshqaruv = hodisa.target.closest("[data-amal]");
if (!boshqaruv) {
return;
}
const id = Number(boshqaruv.closest(".vazifa").dataset.id);
if (boshqaruv.dataset.amal === "almashtirish") {
holat = {
...holat,
vazifalar: holat.vazifalar.map((v) =>
v.id === id ? { ...v, bajarildi: !v.bajarildi } : v)
};
} else {
holat = {
...holat,
vazifalar: holat.vazifalar.filter((v) => v.id !== id)
};
}
yangilash();
});
filtrlar.addEventListener("click", (hodisa) => {
const tugma = hodisa.target.closest("[data-filtr]");
if (!tugma) {
return;
}
for (const t of filtrlar.querySelectorAll("[data-filtr]")) {
t.classList.toggle("faol", t === tugma);
}
holat = { ...holat, filtr: tugma.dataset.filtr };
yangilash();
});
document.getElementById("tozalash").addEventListener("click", () => {
holat = {
...holat,
vazifalar: holat.vazifalar.filter((v) => !v.bajarildi)
};
yangilash();
});
yuklash();
yangilash();
Uslublar #
.vazifa {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #e2e8f0;
}
.vazifa.bajarilgan span {
text-decoration: line-through;
color: #94a3b8;
}
.filtrlar button.faol {
background: #2563eb;
color: white;
}
.bosh {
padding: 20px;
text-align: center;
color: #94a3b8;
}
Keyingi qadamlar #
| Imkoniyat | Kerakli bilim |
|---|---|
| Vazifani tahrirlash | dblclick, contenteditable |
| Sudrab tartiblash | dragstart, drop hodisalari |
| Muddat va eslatma | 10-bo'lim, Notification API |
| Serverga saqlash | 18-bo'lim, fetch + POST |
| Bekor qilish (undo) | Holat tarixini massivda saqlash |
| Qidiruv | 15-bo'limdagi debounce |
| Klaviatura qisqartmalari | 14-bo'lim, key |
| Modullarga bo'lish | 19-bo'lim, import/export |
| Testlar | Vitest yoki Jest |
Eng foydali keyingi qadam - kodni modullarga bo'lish:
holat.js - holat va amallar
chizish.js - DOM yaratish
saqlash.js - localStorage
asosiy.js - hammasini bog'laydi
Har fayl bitta vazifa bajaradi va alohida sinaladi. Bu loyiha o'sganda o'zini oqlaydi.
- Holat obyektini va
qoshishamalini yozing. - Amallar massivni o'zgartirmasligini ta'minlang.
korinadiganlarbilan uch filtrni amalga oshiring.vazifaElementibilan DOM tugunini yasang.DocumentFragmentbilan ro'yxatni bir marta chizing.- Delegatsiya bilan belgilash va o'chirishni bog'lang.
submitda validatsiya qo'shing.localStoragega saqlang vatry/catchbilan o'rang.- Buzilgan JSON da ilova yiqilmasligini tekshiring.
- Ilovani to'rt modulga bo'ling.
Xulosa #
- Holat - haqiqat manbai, DOM esa uning aksi.
- Holatni o'zgartirmasdan yangilang - spread ishlating.
- Chizish funksiyasi holatni o'qiydi, o'zgartirmaydi.
- Bitta
yangilash()funksiyasi hamma narsani qayta chizadi. - Delegatsiya bilan bitta ishlovchi butun ro'yxatni boshqaradi.
- Foydalanuvchi matni uchun
textContent- XSS dan himoya. DocumentFragmentbilan bir marta joylashtiring.localStorageyiqilishi mumkin -try/catchmajburiy.- Saqlangan ma'lumot buzilgan bo'lishi mumkin - tekshiring.
localStoragemaxfiy emas - parol saqlamang.
Tabriklaymiz! Siz JavaScript ning asosiy imkoniyatlarini
o'rgandingiz: o'zgaruvchilar va turlar, funksiyalar va berkitmalar,
massiv va obyektlar, DOM va hodisalar, formalar, asinxron
dasturlash, Promise va async/await, fetch orqali server bilan
aloqa, modullar - va hammasini bitta ishlaydigan ilovada
birlashtirdingiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.