20-bo‘lim

Amaliy loyiha - vazifalar ilovasi

Butun kursda o'rganilgan bilimlarni birlashtirib, to'liq ishlaydigan vazifalar ro'yxati ilovasini yozamiz.

🕑 10 daqiqa o‘qish 📄 638 so‘z 👁 2 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Nima quramiz
  2. HTML tuzilmasi
  3. 1-qadam: holat va amallar
  4. 2-qadam: chizish
  5. 3-qadam: brauzerda bog'lash
  6. 4-qadam: localStorage ga saqlash
  7. To'liq ilova
  8. Uslublar
  9. Keyingi qadamlar
  10. Xulosa

Yigirma bo'lim davomida o'rgangan hamma narsani bitta ilovada birlashtiramiz: holat boshqaruvi, DOM, hodisalar, saqlash va filtrlash.

Nima quramiz #

Ma'lumot bir yo'nalishda oqadi Holat (state) vazifalar massivi joriy filtr chizish DOM ro'yxat, hisoblagich filtr tugmalari hodisa Amallar qo'shish, o'chirish belgilash, filtrlash holatni yangilaydi va qayta chizadi Asosiy qoida DOM ni hech qachon "haqiqat manbai" qilmang Holat - massivda, DOM esa uning aksi
Bu naqsh React va boshqa kutubxonalarning asosi
ImkoniyatQaysi bo'limdan
Massiv metodlari7 - filter, map, reduce
Obyektlar va spread8 - holat yangilash
DOM yaratish13 - createElement, fragment
Delegatsiya14 - bitta ishlovchi
Forma va validatsiya15 - submit, tekshiruv
localStorageBu bo'limda

HTML tuzilmasi #

HTML
<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 #

JavaScript
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));
Natija
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' ]
Holatni o'zgartirmasdan yangilash

E'tibor bering - hech bir amal massivni joyida o'zgartirmaydi:

JavaScript
// 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:

FoydaIzoh
Taqqoslash osoneski !== yangi - o'zgardimi?
Tarix saqlashBekor qilish (undo) oson
Xatolarni topishKim o'zgartirdi degan savol yo'q
React/Vue moslikUlar aynan shunga tayanadi

Kamchiligi - juda katta massivda nusxalash qimmat. Lekin minglab element uchun bu sezilmaydi.

2-qadam: chizish #

JavaScript
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);
Natija
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:

JavaScript
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 #

JavaScript
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());
Natija
Bo'sh omborda: null
Yuklandi     : 1 ta vazifa
Matn         : Non
keyingiId    : 2
Buzilgan JSON: null
Noto'g'ri tur: null
localStorage ga hech qachon ishonmang

Uchta muammo bor va uchalasini ham hal qilish kerak:

1. Yozish yiqilishi mumkin

Maxfiy rejimda yoki joy tugaganda setItem xato tashlaydi:

Natija
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:

JavaScript
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:

JavaScript
// 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 #

CSS
.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 #

Ilovani rivojlantirish yo'nalishlari
ImkoniyatKerakli bilim
Vazifani tahrirlashdblclick, contenteditable
Sudrab tartiblashdragstart, drop hodisalari
Muddat va eslatma10-bo'lim, Notification API
Serverga saqlash18-bo'lim, fetch + POST
Bekor qilish (undo)Holat tarixini massivda saqlash
Qidiruv15-bo'limdagi debounce
Klaviatura qisqartmalari14-bo'lim, key
Modullarga bo'lish19-bo'lim, import/export
TestlarVitest yoki Jest

Eng foydali keyingi qadam - kodni modullarga bo'lish:

Natija
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.

Amaliy topshiriq
  1. Holat obyektini va qoshish amalini yozing.
  2. Amallar massivni o'zgartirmasligini ta'minlang.
  3. korinadiganlar bilan uch filtrni amalga oshiring.
  4. vazifaElementi bilan DOM tugunini yasang.
  5. DocumentFragment bilan ro'yxatni bir marta chizing.
  6. Delegatsiya bilan belgilash va o'chirishni bog'lang.
  7. submit da validatsiya qo'shing.
  8. localStorage ga saqlang va try/catch bilan o'rang.
  9. Buzilgan JSON da ilova yiqilmasligini tekshiring.
  10. 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.
  • DocumentFragment bilan bir marta joylashtiring.
  • localStorage yiqilishi mumkin - try/catch majburiy.
  • Saqlangan ma'lumot buzilgan bo'lishi mumkin - tekshiring.
  • localStorage maxfiy 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.

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.