IlmHamroh
JavaScript Full-stack/9-qism. JavaScript brauzerda va debugging15/29-dars21 daqiqa
Mundarija (33)

JavaScript event delegation: bitta tinglovchi, closest va data-amal naqshi

Qisqacha: Event delegation — har tugmaga alohida tinglovchi qo'yish o'rniga, ularning umumiy otasiga bitta tinglovchi qo'yish. Hodisa yuqoriga ko'tarilgani uchun ota har bosishni eshitadi. Qaysi tugma bosilganini event.target.closest("button") topadi. Afzalligi: tinglovchi bitta, keyin qo'shilgan elementlar ham avtomatik ishlaydi. Bir nechta amalni data-amal="ochir" kabi atribut bilan ajratish — keng tarqalgan naqsh.

Bu darsda

  • Ota elementga bitta tinglovchi qo'yib, ichidagi istalgan tugmani taniy olasiz.
  • closest va contains bilan bosilgan elementni ishonchli topasiz.
  • Sahifa ochilgandan keyin qo'shilgan elementlar ham ishlashini ta'minlaysiz.
  • data-amal naqshi bilan bitta tinglovchida bir nechta amalni boshqarasiz.
  • Delegation qachon ishlamasligini va uning o'rniga nima qilishni bilasiz.

Oldin bilishingiz kerak: Hodisa tarqalishi: capture va bubble, Atributlar, xususiyatlar va dataset, Holatdan UI chizish.

1. Nega bu kerak?

addEventListener asoslari darsida sikl bilan har "Savatga" tugmasiga tinglovchi qo'ydik va ikki muammoga duch keldik:

  • Kartalar ko'p bo'lsa — tinglovchilar ham ko'p.
  • «Yangi taom qo'shish» bilan qo'shilgan kartaning tugmasi ishlamadi: sikl bir marta ishlagan, yangi tugmani "ko'rmagan".

O'tgan darsda esa hodisa bosilgan elementdan yuqoriga ko'tarilishini ko'rdik. Ikkalasini birlashtiramiz: tinglovchini tugmalarga emas, ularning otasiga qo'yamiz. Qaysi tugma bosilgan bo'lmasin, hodisa otaga yetib keladi.

Hayotdan o'xshatish: «Bahor»da 20 ta stol bor. Har stolga alohida ofitsiant qo'yish shart emas. Otabek zalda bitta turadi: qaysi stoldan qo'l ko'tarilsa, u ko'radi va stol raqamiga qarab boradi. Yangi stol qo'shilsa ham — Otabek uni ham ko'radi, yangi ofitsiant kerak emas. Bu — event delegation (hodisani topshirish): ish otaga topshiriladi.

flowchart LR
  subgraph A["Har tugmaga tinglovchi"]
    T1["Osh: tinglovchi"]
    T2["Manti: tinglovchi"]
    T3["Yangi taom: yo'q!"]
  end
  subgraph B["Delegation"]
    U["ul: bitta tinglovchi"] --- L1["Osh"]
    U --- L2["Manti"]
    U --- L3["Yangi taom"]
  end

Chap tomonda har tugma o'z tinglovchisini kutadi, yangisi esa bo'sh qoladi. O'ng tomonda bitta tinglovchi hammasini, jumladan yangisini ham qamraydi.

2. Birinchi delegation

2.1 Tinglovchi otada

html
<ul id="menyu">
  <li class="taom-karta">
    Osh <button type="button">Savatga</button>
  </li>
  <li class="taom-karta">
    Manti <button type="button">Savatga</button>
  </li>
  <li class="taom-karta">
    Lag'mon <button type="button">Savatga</button>
  </li>
</ul>
<script>
  const menyu = document.getElementById("menyu");

  menyu.addEventListener("click", (event) => {
    const tugma = event.target.closest("button");
    if (tugma === null) {
      return;
    }
    const karta = tugma.closest(".taom-karta");
    const nomi = karta.firstChild.textContent.trim();
    console.log(`Savatga qo'shildi: ${nomi}`);
  });
</script>

Manti tugmasini bosing — konsolda "Savatga qo'shildi: Manti". Lag'mon tugmasini — "Savatga qo'shildi: Lag'mon". Kartaning bo'sh joyini yoki "Osh" so'zini bosing — konsol jim.

Tinglovchi bitta, u ul da. Kodni qadamma-qadam o'qiymiz:

  1. Hodisa keldi. Mehmon qayergadir bosdi, hodisa ul ga ko'tarildi.
  2. Nishonni topish. event.target — bosilgan eng ichki element. closest("button") undan yuqoriga qarab eng yaqin tugmani qidiradi (Elementlarni tanlash).
  3. "Menga emas" tekshiruvi. Tugma topilmasa (null), bosish tugmaga tushmagan — handler erta qaytadi.
  4. Ish. Tugmadan kartaga chiqamiz va taom nomini olamiz.

Bu to'rt qadam — har delegation'ning skeleti. Keyingi misollarda faqat 4-qadam o'zgaradi.

2.2 Nega target emas, closest?

Tugmaning ichida ikonka bo'lsa, event.target — ikonka, tugma emas (Event obyekti darsidagi span muammosi). Tekshiramiz:

html
<ul id="menyu">
  <li class="taom-karta">
    Osh
    <button type="button">
      <span class="ikonka">+</span> Savatga
    </button>
  </li>
</ul>
<script>
  const menyu = document.getElementById("menyu");

  menyu.addEventListener("click", (event) => {
    console.log("target:", event.target.nodeName);
    const tugma = event.target.closest("button");
    console.log("closest:", tugma?.nodeName);
  });
</script>

+ ni bosing. Konsolda target: SPAN va closest: BUTTON. "Savatga" so'zini bosing — target: BUTTON, closest: BUTTON. "Osh" ni bosing — target: LI, closest: undefined (tugma topilmadi; ?. xato o'rniga undefined berdi).

event.target.matches("button") deb tekshirsangiz, + ni bosgan mehmonning bosishi "yo'qoladi". Telefonda barmoq aynan ikonkaga tegishi ehtimoli katta — xato faqat ba'zan chiqadi va topilishi qiyin bo'ladi. closest har ikki holatni ham to'g'ri tutadi.

Tekshirib ko'ring: Delegation handlerida if (tugma === null) return; qatori o'chirildi. Mehmon kartaning bo'sh joyini bosdi. Nima bo'ladi?

Javob

Xato: tugma — null, keyingi qatordagi tugma.closest(...) TypeError: Cannot read properties of null (reading 'closest') beradi. Delegation tinglovchisi otaning har bosishini eshitadi — tugmaga tegmaganlarini ham. Shuning uchun "bu menga tegishlimi?" tekshiruvi har doim kerak.

3. Chegarani tekshirish: contains

closest yuqoriga qarab qidiradi — va ba'zan otadan ham chiqib ketadi. Quyidagi sahifada menyu bo'limi kattaroq ro'yxatning ichida joylashgan. Tinglovchi ichki ro'yxatda, biz li ni qidiramiz:

html
<style>
  #ichki { padding: 0.5rem 0.5rem 0.5rem 3rem; outline: 1px dashed; }
</style>
<ul>
  <li id="bolim">
    Taomlar bo'limi
    <ul id="ichki">
      <li>Osh</li>
      <li>Manti</li>
    </ul>
  </li>
</ul>
<script>
  const ichki = document.getElementById("ichki");

  ichki.addEventListener("click", (event) => {
    const band = event.target.closest("li");
    console.log("closest:", band.firstChild.textContent.trim());
    console.log("ichidami:", ichki.contains(band));
  });
</script>

"Manti" ni bosing — closest: Manti, ichidami: true. Endi punktir ramkaning chap bo'sh qismini bosing (ro'yxat bandlaridan tashqari, lekin ramka ichida). Konsolda closest: Taomlar bo'limi va ichidami: false.

Nima bo'ldi? Bosish ul#ichki ning o'ziga tushdi. closest("li") undan yuqoriga chiqdi va tashqi li ni topdi — tinglovchi turgan elementdan tashqaridagi elementni. Real loyihada bu "noto'g'ri band o'chirildi" kabi xatoga aylanadi.

Tuzatish: topilgan element shu otaning ichidami — ota.contains(element) bilan tekshiring. Bu metodni DOM bo'ylab harakatlanish darsidan bilasiz: element otaning o'zi yoki uning istalgan chuqurlikdagi avlodi bo'lsa — true:

js
const band = event.target.closest("li");
if (band === null || !ichki.contains(band)) {
  return;
}

Qachon kerak? Siz qidirayotgan selektor (masalan li, .karta) otadan yuqorida ham uchrashi mumkin bo'lsa. Tugmani qidirayotganda va ota tugma ichida bo'lmasa — null tekshiruvi yetarli. Shubha bo'lsa — ikkalasini yozing, bir qator xolos.

4. Keyin qo'shilgan elementlar

Delegation'ning eng katta yutug'i. addEventListener asoslari darsidagi ishlamagan misolni delegation bilan qayta yozamiz:

html
<div id="menyu">
  <article class="taom-karta">
    <h2 class="taom-nomi">Osh</h2>
    <button type="button">Savatga</button>
  </article>
</div>
<button id="yangi" type="button">Yangi taom qo'shish</button>
<script>
  const menyu = document.getElementById("menyu");

  menyu.addEventListener("click", (event) => {
    const tugma = event.target.closest("button");
    if (tugma === null) {
      return;
    }
    const karta = tugma.closest(".taom-karta");
    const nomi = karta.querySelector(".taom-nomi").textContent;
    console.log(`Savatga qo'shildi: ${nomi}`);
  });

  document.getElementById("yangi").addEventListener("click", () => {
    const karta = document.createElement("article");
    karta.className = "taom-karta";
    karta.innerHTML = `<h2 class="taom-nomi">Manti</h2>
      <button type="button">Savatga</button>`;
    menyu.append(karta);
  });
</script>

«Yangi taom qo'shish» ni bosing, keyin yangi Manti kartasidagi «Savatga» ni bosing — konsolda "Savatga qo'shildi: Manti". Yangi tugmaga hech kim tinglovchi qo'ymadi, lekin u ishlaydi: bosish #menyu ga ko'tarildi, u yerda tinglovchi bor.

«Yangi taom qo'shish» tugmasi #menyu ning tashqarisida — shuning uchun delegation tinglovchisi uni eshitmaydi va ikki tinglovchi bir-biriga xalaqit bermaydi. Agar u #menyu ichida bo'lganida edi, closest("button") uni ham "Savatga" deb o'ylardi. Bunday holatda tugmalarni farqlash kerak — buni «data-amal naqshi» bo'limida ko'ramiz.

4.1 State → render bilan birga

Holatdan UI chizish darsida ro'yxatni har o'zgarishda massivdan qaytadan chizgan edik. Qaytadan chizilganda eski elementlar o'chadi, o'rniga yangilari yaratiladi. Eski elementlarga qo'yilgan tinglovchilar ham ular bilan birga yo'qoladi. Delegation bu muammoni umuman yo'qotadi: tinglovchi ro'yxatning o'zida, ro'yxat esa o'chmaydi.

html
<ul id="savat"></ul>
<script>
  const savat = document.getElementById("savat");
  const taomlar = ["Osh", "Manti", "Ko'k choy"];

  function chiz() {
    savat.replaceChildren();
    for (const [i, nomi] of taomlar.entries()) {
      const band = document.createElement("li");
      band.textContent = `${nomi} `;
      const tugma = document.createElement("button");
      tugma.type = "button";
      tugma.textContent = "O'chirish";
      tugma.dataset.indeks = i;
      band.append(tugma);
      savat.append(band);
    }
    console.log(`Savatda: ${taomlar.length} ta`);
  }

  savat.addEventListener("click", (event) => {
    const tugma = event.target.closest("button");
    if (tugma === null) {
      return;
    }
    taomlar.splice(Number(tugma.dataset.indeks), 1);
    chiz();
  });

  chiz();
</script>

Konsolda:

text
Savatda: 3 ta

"Manti" yonidagi «O'chirish» ni bosing — u yo'qoladi, konsolda "Savatda: 2 ta". Qolgan tugmalar ham ishlaydi, garchi chiz() ularni yangidan yaratgan bo'lsa ham.

Kodda uchta tanish narsa bor:

Agar tinglovchini har tugmaga chiz() ichida qo'ysak ham ishlardi — lekin har chizishda tinglovchilar qayta yaratilardi. Delegation bilan tinglovchi bitta va u faqat bir marta qo'shiladi.

4.2 Tinglovchilar soni muhimmi?

Har tinglovchi — xotirada kichik yozuv va funksiya. 10 ta tugma uchun farq sezilmaydi. Lekin 1 000 qatorli jadvalda har qatorda uchtadan tugma bo'lsa — 3 000 ta tinglovchi. Ularni qo'shish ham vaqt oladi, sahifa ochilishi sekinlashadi. Qayta chizishda eski tinglovchilarni unutib qoldirish esa xotira sizishiga olib boradi.

Delegation bilan son o'zgarmaydi: qator 10 ta bo'lsin yoki 10 000 ta — tinglovchi bitta. Uning narxi — har bosishda closest chaqiruvi. Bu millisekundning juda kichik bo'lagi, odam sezmaydi. Shuning uchun ko'p elementli ro'yxatda delegation — standart tanlov. Ikki-uchta alohida tugma uchun esa oddiy tinglovchi ham bemalol: maqsad — kod aniq va tushunarli bo'lishi.

Tekshirib ko'ring: Nega taom indeksini tugmaning data-indeks atributida saqladik? closest bilan li ni topib, uning matnidan taom nomini olsa bo'lmasmidi?

Javob

Bo'lardi, lekin ishonchsiz: ikkita bir xil nomli taom bo'lsa (ikki portsiya osh), matn qaysi biri ekanini aytmaydi. Indeks — aniq manzil. Real loyihada esa indeks o'rniga takrorlanmas id saqlanadi (masalan data-id="17"), chunki o'chirishdan keyin indekslar siljiydi. Bizda bu xavfsiz, chunki har o'chirishdan keyin hammasi qayta chiziladi va indekslar yangilanadi.

5. data-amal naqshi

5.1 Bitta kartada bir nechta tugma

Real kartada tugma bitta emas: «+», «−», «O'chirish». Bitta tinglovchi ularni qanday farqlaydi? Har tugmaga u nima qilishini atribut bilan yozamiz: data-amal="qosh", data-amal="ayir". Tinglovchi tugmani topadi va uning dataset.amal iga qarab ish tanlaydi:

html
<ul id="savat">
  <li data-taom="Osh">
    Osh: <output>1</output>
    <button type="button" data-amal="ayir">−</button>
    <button type="button" data-amal="qosh">+</button>
  </li>
  <li data-taom="Manti">
    Manti: <output>1</output>
    <button type="button" data-amal="ayir">−</button>
    <button type="button" data-amal="qosh">+</button>
  </li>
</ul>
<script>
  const savat = document.getElementById("savat");

  savat.addEventListener("click", (event) => {
    const tugma = event.target.closest("[data-amal]");
    if (tugma === null) {
      return;
    }
    const band = tugma.closest("[data-taom]");
    const soni = band.querySelector("output");
    let son = Number(soni.textContent);

    switch (tugma.dataset.amal) {
      case "qosh":
        son++;
        break;
      case "ayir":
        son = Math.max(1, son - 1);
        break;
    }
    soni.textContent = son;
    console.log(`${band.dataset.taom}: ${son}`);
  });
</script>

Osh qatoridagi + ni ikki marta bosing — konsolda Osh: 2, Osh: 3. Manti qatoridagi − ni bosing — Manti: 1 (1 dan kam bo'lmaydi). Tinglovchi bitta, tugmalar to'rtta, amallar ikkita.

Naqshning uchta qismi:

  • Nima qilinadi — tugmaning data-amal i. closest("[data-amal]") — atribut selektori (Atribut selektorlari): "data-amal atributi bor eng yaqin element".
  • Nimaga qilinadi — bandning data-taom i (real loyihada — data-id).
  • Qanday qilinadi — switch (switch) bitta joyda hamma amallarni ro'yxat qiladi.

5.2 Nega data-*, class emas?

class="qosh-tugma" bilan ham farqlasa bo'lardi. Lekin class — ko'rinish uchun. Nodira dizaynni yangilab, class nomlarini o'zgartirishi mumkin — va JavaScript jimgina buziladi. Atribut data-amal esa faqat dastur uchun: uning nomi "bu tugma nima qiladi" deb o'qiladi va CSS uni ishlatmaydi. Elementlarni tanlash darsida "JavaScript uchun maxsus atributlar" deb va'da qilgan narsa shu.

Bonus: yangi amal qo'shish oson. «O'chirish» tugmasi kerakmi? HTML'ga data-amal="ochir" li tugma, switch ga bitta case — tinglovchiga tegmaysiz. Buni «Savat — jami summa bilan» mashqida qilasiz.

Tekshirib ko'ring: Tugmada data-amal="qosh" yozilgan, kodda esa switch (tugma.dataset.amal) ichida case "qo'sh": (apostrof bilan). Nima bo'ladi?

Javob

+ hech narsa qilmaydi: "qosh" va "qo'sh" — ikki xil satr, birorta case mos kelmadi. Xato xabari yo'q, son o'zgarmaydi. Shuning uchun amal nomlarini qisqa va apostrofsiz tanlang (qosh, ochir), va switch oxiriga default: console.warn("Noma'lum amal:", amal) qo'shish foydali — xato darhol ko'rinadi (console API).

6. Delegation qachon ishlamaydi?

6.1 Ko'tarilmaydigan hodisalar

Delegation bubble'ga tayanadi. O'tgan darsdagi ko'tarilmaydigan hodisalar — focus, blur, mouseenter, mouseleave — otaga yetmaydi. Ularning o'rniga juftlarini ishlating: focusin, focusout, mouseover, mouseout. Yoki tinglovchini { capture: true } bilan qo'ying.

6.2 Ichkarida kimdir stopPropagation qilsa

Kartaning ichidagi biror kod stopPropagation() chaqirsa, hodisa otaga yetmaydi va delegation "ko'r" bo'lib qoladi. O'tgan darsda aynan shu sabab bilan stopPropagation ni kamdan-kam ishlatishni maslahat bergan edik.

6.3 Juda baland ota

Hamma narsani document ga topshirish mumkin — texnik jihatdan ishlaydi. Lekin unda sahifadagi har bosish sizning handleringizdan o'tadi va kodda "bu tugma qaysi qismga tegishli?" degan savol paydo bo'ladi. Qoida: tinglovchini kerakli elementlarni o'rab turgan eng yaqin umumiy otaga qo'ying — #menyu, #savat, form. U sahifa yashagancha yashashi kerak (qayta chizilmasin).

6.4 Bosiladigan narsa — tugma bo'lsin

Delegation bilan li ning o'ziga bosishni ushlash ham oson: closest("li"). Lekin li ga Tab bilan borib bo'lmaydi va Enter uni "bosmaydi" — Bobur va Malika uni ishlata olmaydi. Bosiladigan har narsa ichida haqiqiy <button> yoki <a> bo'lsin, delegation esa ularni qidirsin. Bunda click klaviaturadan ham keladi (addEventListener asoslari darsida tekshirgan edik).

6.5 disabled tugma — hodisa umuman yo'q

Kartadagi taom tugagan bo'lsa, uning «Savatga» tugmasiga disabled qo'yiladi. Delegation tinglovchisida buni alohida tekshirish kerakmi? Chrome'da sinadik: disabled tugmani (va hatto uning ichidagi ikonkani) bosganda click hodisasi otaga ham yetmaydi — tinglovchi umuman chaqirilmaydi. Shuning uchun disabled tugmalar uchun qo'shimcha kod kerak emas.

aria-disabled="true" esa boshqa: u faqat ekran o'quvchiga holatni aytadi, bosish odatdagidek otaga ko'tariladi. Delegation'da uni addEventListener asoslari darsidagi qorovul kabi o'zingiz tekshirasiz: if (tugma.getAttribute("aria-disabled") === "true") return;.

6.6 Formada: bitta tinglovchi — hamma maydon

Delegation faqat click uchun emas. Ko'tariladigan istalgan hodisa bilan ishlaydi. Masalan, input — maydonga har harf yozilganda keladi va formaga ko'tariladi. Bron formasidagi hamma maydonni bitta tinglovchi kuzatadi, qaysi maydon ekanini esa name aytadi:

html
<form id="bron" action="https://example.com/bron" method="post">
  <label for="ism">Ism</label>
  <input id="ism" name="ism">
  <label for="odam">Odam</label>
  <input id="odam" name="odam" type="number" min="1" max="20">
</form>
<script>
  const forma = document.getElementById("bron");

  forma.addEventListener("input", (event) => {
    const maydon = event.target;
    console.log(`${maydon.name}: ${maydon.value}`);
  });
</script>

Ism maydoniga "Ali" deb yozing — konsolda ism: A, ism: Al, ism: Ali. Odam soniga 4 yozing — odam: 4. Bu yerda closest kerak emas: input hodisasining nishoni har doim maydonning o'zi, uning ichida boshqa element bo'lmaydi.

Bu qolip forma darslarining asosi bo'ladi: input va change darsida aynan shunday bitta tinglovchi bilan butun formani jonli tekshiramiz.

7. Ko'p uchraydigan xatolar

7.1 closest o'rniga target

event.target.dataset.amal — ikonka ustidan bosilganda undefined. Doim closest("[data-amal]").

7.2 null tekshiruvisiz

html
<ul id="savat">
  <li data-taom="Osh">
    Osh <button type="button" data-amal="qosh">+</button>
  </li>
</ul>
<script>
  const savat = document.getElementById("savat");

  savat.addEventListener("click", (event) => {
    const tugma = event.target.closest("[data-amal]");
    console.log(tugma.dataset.amal);
  });

  document.querySelector("li").click();
</script>

Konsolda:

text
Uncaught TypeError: Cannot read properties of null (reading 'dataset')

Tarjimasi: "null ning xususiyatini o'qib bo'lmaydi (dataset ni o'qishda)". Oxirgi qator li ni kod bilan "bosdi" (click()) — mehmon bandning bo'sh joyini bosgandek. closest tugma topmadi va null qaytardi. Xabar dataset ni ko'rsatadi, sabab esa bir qator yuqorida: tekshiruv yo'q. Tuzatish: if (tugma === null) return;.

7.3 dataset nomlari

data-taom-id="7" → dataset.taomId (chiziqcha olib tashlanadi, keyingi harf katta). dataset["taom-id"] — undefined. Qiymat doim satr: "7", son kerak bo'lsa Number(). Ikkalasi ham Atributlar va dataset darsidan.

7.4 Tinglovchi qayta chiziladigan elementda

chiz() har safar #savat ni emas, uning otasini qayta yaratsa (ota.innerHTML = ...), #savat dagi tinglovchi ham u bilan birga yo'qoladi. Delegation tinglovchisi doim o'zgarmaydigan konteynerda turishi kerak.

7.5 this va currentTarget — bu ota, tugma emas

Delegation tinglovchisida event.currentTarget (va oddiy function dagi this) — tinglovchi turgan ota: #savat, #menyu. Bosilgan tugma emas! addEventListener asoslari darsida sikl bilan ishlaganda this tugma edi, chunki tinglovchi tugmaning o'zida turardi. Delegation'ga o'tganda this.closest(...) yozilgan eski kod jimgina noto'g'ri elementni topadi. Bosilgan narsa — doim event.target.closest(...) orqali.

Tekshirib ko'ring: addEventListener asoslari darsidagi «Kupon» mashqini delegation'ga o'tkazdingiz: tinglovchi endi ul da, ichida esa this.closest(".taom-karta") qoldi. Nima bo'ladi?

Javob

Endi this — ul. Uning o'zi ham, ota-bobolari ham .taom-karta emas — closest null qaytaradi va keyingi qatorda Cannot read properties of null xatosi chiqadi. Tuzatish: event.target.closest(".taom-karta").

8. Mashqlar

1-mashq (oson): Qaysi kun?

Ish kunlari tugmalari bor. Bitta tinglovchi (#kunlar da) bilan: bosilgan tugmaning matnini konsolga Tanlandi: Juma kabi chiqaring.

html
<div id="kunlar">
  <button type="button">Dushanba</button>
  <button type="button">Chorshanba</button>
  <button type="button">Juma</button>
</div>
<script>
  // shu yerga yozing
</script>
Yechim
html
<div id="kunlar">
  <button type="button">Dushanba</button>
  <button type="button">Chorshanba</button>
  <button type="button">Juma</button>
</div>
<script>
  const kunlar = document.getElementById("kunlar");

  kunlar.addEventListener("click", (event) => {
    const tugma = event.target.closest("button");
    if (tugma === null) {
      return;
    }
    console.log(`Tanlandi: ${tugma.textContent}`);
  });
</script>

"Juma" ni bosing — "Tanlandi: Juma". Tugmalar orasidagi bo'sh joyni bosing — konsol jim: closest null qaytardi.

2-mashq (o'rta): Kichik vazifalar ro'yxati

Forma orqali vazifa qo'shiladi, har vazifa yonida «Bajarildi» tugmasi. Tugma bosilganda vazifa matni chizilsin (s elementiga o'raladi yoki style.textDecoration — o'zingiz tanlang) va tugma o'chsin. Talablar:

  • forma submit da preventDefault (Event obyekti), bo'sh matn qo'shilmasin;
  • «Bajarildi» uchun bitta delegation tinglovchisi #royxat da;
  • sahifa ochilganda ro'yxatda bitta vazifa bor — u ham, yangilari ham ishlashi kerak.
html
<form id="qosh" action="https://example.com/vazifa" method="post">
  <label for="matn">Vazifa</label>
  <input id="matn" name="matn">
  <button>Qo'shish</button>
</form>
<ul id="royxat">
  <li>Guruch olish <button type="button">Bajarildi</button></li>
</ul>
<script>
  // shu yerga yozing
</script>
Yechim
html
<form id="qosh" action="https://example.com/vazifa" method="post">
  <label for="matn">Vazifa</label>
  <input id="matn" name="matn">
  <button>Qo'shish</button>
</form>
<ul id="royxat">
  <li>Guruch olish <button type="button">Bajarildi</button></li>
</ul>
<script>
  const forma = document.getElementById("qosh");
  const maydon = document.getElementById("matn");
  const royxat = document.getElementById("royxat");

  forma.addEventListener("submit", (event) => {
    event.preventDefault();
    const matn = maydon.value.trim();
    if (matn === "") {
      return;
    }
    const band = document.createElement("li");
    band.textContent = `${matn} `;
    const tugma = document.createElement("button");
    tugma.type = "button";
    tugma.textContent = "Bajarildi";
    band.append(tugma);
    royxat.append(band);
    maydon.value = "";
  });

  royxat.addEventListener("click", (event) => {
    const tugma = event.target.closest("button");
    if (tugma === null) {
      return;
    }
    const band = tugma.closest("li");
    band.style.textDecoration = "line-through";
    tugma.remove();
    console.log(`Bajarildi: ${band.textContent.trim()}`);
  });
</script>

"Guruch olish" yonidagi tugmani bosing — matn chiziladi, konsolda "Bajarildi: Guruch olish". "Sabzi tozalash" ni yozib Enter bosing, keyin uning tugmasini bosing — u ham ishlaydi.

Matnni textContent bilan qo'ydik, innerHTML bilan emas. Mehmon <img src=x onerror=...> yozsa ham, u matn bo'lib qoladi (Kontentni o'zgartirish va XSS). Bu kichik ro'yxat — qism oxiridagi vazifalar ilovasining kurtagi.

3-mashq (qiyin): Savat — jami summa bilan

«data-amal naqshi» bo'limidagi savatni kengaytiring:

  1. Holat massivda turadi: { nom, narx, soni } obyektlari. Ekran undan chiz() bilan chiziladi (state → render).
  2. Uch amal: qosh, ayir (soni 1 dan kam bo'lmasin), ochir (taom savatdan olib tashlanadi).
  3. Pastda jami summa: Jami: 98000 so'm.
  4. Tinglovchi bitta va #savat da; bandni data-indeks bilan toping.

Boshlang'ich holat: osh 35 000 × 2, manti 30 000 × 1, ko'k choy 5 000 × 2. Ochilganda konsolda Jami: 110000 so'm chiqsin.

html
<h2>Savat</h2>
<ul id="savat"></ul>
<p id="jami"></p>
<script>
  const holat = [
    { nom: "Osh", narx: 35000, soni: 2 },
    { nom: "Manti", narx: 30000, soni: 1 },
    { nom: "Ko'k choy", narx: 5000, soni: 2 },
  ];
  // shu yerga yozing
</script>
Yechim
html
<h2>Savat</h2>
<ul id="savat"></ul>
<p id="jami"></p>
<script>
  const holat = [
    { nom: "Osh", narx: 35000, soni: 2 },
    { nom: "Manti", narx: 30000, soni: 1 },
    { nom: "Ko'k choy", narx: 5000, soni: 2 },
  ];
  const savat = document.getElementById("savat");
  const jamiQator = document.getElementById("jami");

  function tugmaYasa(amal, matn) {
    const tugma = document.createElement("button");
    tugma.type = "button";
    tugma.dataset.amal = amal;
    tugma.textContent = matn;
    return tugma;
  }

  function chiz() {
    savat.replaceChildren();
    let jami = 0;
    for (const [i, taom] of holat.entries()) {
      const band = document.createElement("li");
      band.dataset.indeks = i;
      band.append(
        `${taom.nom}: ${taom.soni} ta `,
        tugmaYasa("ayir", "−"),
        tugmaYasa("qosh", "+"),
        tugmaYasa("ochir", "O'chirish"),
      );
      savat.append(band);
      jami += taom.narx * taom.soni;
    }
    const matn = `Jami: ${jami} so'm`;
    jamiQator.textContent = matn;
    console.log(matn);
  }

  savat.addEventListener("click", (event) => {
    const tugma = event.target.closest("[data-amal]");
    if (tugma === null) {
      return;
    }
    const i = Number(tugma.closest("li").dataset.indeks);
    switch (tugma.dataset.amal) {
      case "qosh":
        holat[i].soni++;
        break;
      case "ayir":
        holat[i].soni = Math.max(1, holat[i].soni - 1);
        break;
      case "ochir":
        holat.splice(i, 1);
        break;
    }
    chiz();
  });

  chiz();
</script>

Konsolda:

text
Jami: 110000 so'm

Mantining + ini bosing — "Jami: 140000 so'm". Ko'k choyni o'chiring — "Jami: 130000 so'm". Biz buni Chrome'da tekshirdik.

E'tibor bering: tinglovchi ekranni o'zi o'zgartirmaydi. U faqat holatni o'zgartiradi va chiz() ni chaqiradi. Ekran doim holatdan chiziladi — shuning uchun ular hech qachon bir-biridan ajralib qolmaydi. append ga bir nechta argument berish mumkin — matn ham, element ham (Element yaratish). Sonni 110 000 ko'rinishida yozish uchun 08-qismdagi somYoz funksiyasini ishlating. options obyekti darsidagi toLocaleString("uz-UZ") ga brauzerda tayanmang: Node 110 000 beradi, Chrome esa 110,000 — biz ikkalasida sinadik. Sababi va yechimi — Intl.NumberFormat darsida.

4-mashq: Portfolio qadami — menyu va filtrga bitta tinglovchi

addEventListener asoslari darsida telefon menyusidagi har havolaga sikl bilan tinglovchi qo'ydingiz, Event obyekti darsida esa unga joriy sahifa tekshiruvini qo'shdingiz. O'tgan darsda esa havola bosilganda hodisa #asosiy-menyu gacha ko'tarilishini DevTools'da ko'rdingiz. Siklni bitta delegation tinglovchisi bilan almashtiring:

  • tinglovchi — #asosiy-menyu ning o'zida;
  • bosish havolaga (a) tushgan bo'lsa — panel yopilsin, havola joriy sahifaniki bo'lsa — standart harakat bekor qilinsin (avvalgidek);
  • menyuga keyinroq yangi havola qo'shsangiz (masalan "Blog"), JavaScript'ga tegmasdan u ham ishlashi kerak;
  • Holatdan UI chizish darsidagi loyihalar filtrida ham sikl o'rniga bitta tinglovchi bo'lsin (Yechimning 2-qismi).
Yechim

1-qism: menyu. asosiy.js dagi menyu qismi (sikl o'rniga):

js
// Telefon menyusi: havola bosilganda panel yopilsin,
// joriy sahifa havolasi sahifani qayta yuklamasin
const menyu = document.getElementById("asosiy-menyu");

menyu?.addEventListener("click", (event) => {
  const havola = event.target.closest("a");
  if (havola === null) {
    return;
  }
  if (havola.getAttribute("aria-current") === "page") {
    event.preventDefault();
  }
  menyu.hidePopover();
});

Natijani menyuga o'xshash parchada tekshiring — havolalar bu yerda boshqa sahifaga o'tmasin deb, faqat shu sinov uchun hamma havola to'xtatiladi:

html
<button type="button" popovertarget="asosiy-menyu">Menyu</button>
<nav id="asosiy-menyu" aria-label="Asosiy menyu" popover>
  <ul>
    <li><a href="https://example.com/">Bosh sahifa</a></li>
    <li>
      <a href="https://example.com/aloqa/" aria-current="page">
        Aloqa
      </a>
    </li>
  </ul>
</nav>
<script>
  const menyu = document.getElementById("asosiy-menyu");

  menyu?.addEventListener("click", (event) => {
    const havola = event.target.closest("a");
    if (havola === null) {
      console.log("Havola emas — panel ochiq qoladi");
      return;
    }
    if (havola.getAttribute("aria-current") === "page") {
      event.preventDefault();
    }
    console.log("Bekor qilindi:", event.defaultPrevented);
    event.preventDefault(); // faqat sinov uchun
    menyu.hidePopover();
  });
</script>

«Menyu» → "Aloqa": panel yopiladi, konsolda "Bekor qilindi: true" (joriy sahifa). «Menyu» → "Bosh sahifa": "Bekor qilindi: false". Panelning bo'sh joyini bosing — closest("a") null, konsolda "Havola emas — panel ochiq qoladi".

Avvalgi if (menyu !== null) + sikl o'rniga — ?. va bitta tinglovchi. Joriy sahifa tekshiruvi endi event.currentTarget dan emas, closest topgan havoladan: delegation'da currentTarget — nav («this va currentTarget» xatosi). contains tekshiruvi shart emas: nav ning tepasida havola yo'q, demak closest("a") menyudan tashqariga chiqib havola topa olmaydi. Kod qisqardi, yangi havola qo'shilsa ham o'zgarmaydi.

2-qism: loyihalar filtri. Holatdan UI chizish darsidagi filtrniUlash ham har [data-teg] tugmaga sikl bilan tinglovchi qo'yadi. Teglar tugmalari bitta panelda (p) turibdi — demak bu ham delegation uchun tayyor joy. Siklni panelning o'zidagi bitta tinglovchi bilan almashtiring. Tugma data-teg atributi bilan topiladi — bu «data-amal naqshi» bo'limidagi g'oyaning o'zi.

filtrniUlash ning yangi ko'rinishi (holat, mosmi, render — o'zgarishsiz):

js
function filtrniUlash() {
  if (document.querySelector(".loyihalar") === null) {
    return;
  }
  const panel = document.querySelector("[data-teg]").parentElement;
  panel.addEventListener("click", (event) => {
    const tugma = event.target.closest("[data-teg]");
    if (tugma === null) {
      return;
    }
    holat.teg = tugma.dataset.teg;
    render();
  });
  render();
}

Panelni qanday topdik? teglarPaneliniChiz uni id siz yaratgan, lekin uning ichidagi tugmalarda data-teg bor: birinchisini topib, parentElement bilan otasiga chiqdik (DOM bo'ylab harakatlanish). .loyihalar bor sahifada panel ham albatta bor — teglarPaneliniChiz uni shu shart bilan yaratadi.

Sinov — Holatdan UI chizish darsidagi parcha, faqat filtrniUlash yangilangan:

html
<main>
  <p>
    <button type="button" data-teg="hammasi">Hammasi</button>
    <button type="button" data-teg="HTML">HTML</button>
    <button type="button" data-teg="CSS">CSS</button>
  </p>
  <p id="loyihalar-soni">2 ta loyiha</p>
  <div class="loyihalar">
    <article data-texnologiyalar="HTML CSS">
      <h2>Choyxona «Bahor» sayti</h2>
    </article>
    <article data-texnologiyalar="HTML">
      <h2>Semantik blog maqolasi</h2>
    </article>
  </div>
</main>
<script>
  const holat = { teg: "hammasi" };

  function mosmi(karta) {
    if (holat.teg === "hammasi") {
      return true;
    }
    const teglar = karta.dataset.texnologiyalar.split(" ");
    return teglar.includes(holat.teg);
  }

  function render() {
    let soni = 0;
    const kartalar =
      document.querySelectorAll(".loyihalar > article");
    for (const karta of kartalar) {
      karta.hidden = !mosmi(karta);
      if (!karta.hidden) {
        soni++;
      }
    }
    for (const tugma of document.querySelectorAll("[data-teg]")) {
      const tanlangan = tugma.dataset.teg === holat.teg;
      tugma.setAttribute("aria-pressed", String(tanlangan));
    }
    const yozuv = document.getElementById("loyihalar-soni");
    yozuv.textContent = `${soni} ta loyiha ko'rsatilmoqda`;
    console.log(yozuv.textContent);
  }

  function filtrniUlash() {
    if (document.querySelector(".loyihalar") === null) {
      return;
    }
    const panel = document.querySelector("[data-teg]").parentElement;
    panel.addEventListener("click", (event) => {
      const tugma = event.target.closest("[data-teg]");
      if (tugma === null) {
        return;
      }
      holat.teg = tugma.dataset.teg;
      render();
    });
    render();
  }

  filtrniUlash();
</script>

Konsolda:

text
2 ta loyiha ko'rsatilmoqda

CSS ni bosing — 1 ta loyiha ko'rsatilmoqda, Hammasi — yana 2 ta. Tugmalar orasidagi bo'sh joyni bosing — konsol jim. Natija avvalgidek, lekin tinglovchi bitta. Endi panelga yangi teg qo'shilsa ham (yangi loyiha bilan), kod o'zgarmaydi.

Tekshiring: telefon o'lchamida «Menyu» → "Aloqa" → "Orqaga" — menyu yopiq; loyihalar sahifasida filtr avvalgidek ishlaydi. npm run check, commit: JS: menyu va filtrni delegation'ga o'tkaz.

9. Real ishda

  • Jadvallar va ro'yxatlar. Admin panellardagi yuzlab qatorli jadvallar (har qatorda "Tahrirlash", "O'chirish") delegation bilan ishlaydi. Telegram Web'da xabarlar ro'yxati ham shunday: yangi xabar kelganda unga tinglovchi qo'shilmaydi.
  • Freymvorklar. React (kursda alohida o'rganamiz) siz har elementga onClick yozsangiz ham, ichida hodisalarni ildizdagi tinglovchilar orqali topshiradi. Delegation g'oyasi uning poydevorida.
  • data-action naqshi — ingliz tilidagi loyihalarda shunday nom bilan uchraydi (data-action="delete"). Kursda o'zbekcha nom — data-amal; 12-qismdan keyin inglizcha nomlarga o'tamiz.
  • Kod sharhida (code review) "bu ro'yxatga nega har bandga alohida tinglovchi qo'ydingiz?" degan izoh tez-tez uchraydi. Delegation'ni bilish — jamoada ishlashning kichik, lekin ko'rinadigan belgisi.
  • Intervyuda deyarli har doim: "Event delegation nima va nega kerak?", "dinamik qo'shilgan elementga qanday tinglovchi qo'yasiz?", "target va closest nega kerak?".

Xulosa

  • Delegation — ko'p elementning umumiy otasiga bitta tinglovchi; bubble tufayli ota hamma bosishni eshitadi.
  • Skelet: closest(selektor) → null bo'lsa qayt → kerak bo'lsa ota.contains(...) → ish.
  • target emas, closest: ichki ikonka ustidan bosish ham to'g'ri tutiladi.
  • Keyin qo'shilgan va qayta chizilgan elementlar avtomatik ishlaydi — state → render bilan juda mos.
  • data-amal — tugma nima qilishini aytadi, switch amallarni bitta joyda yig'adi.
  • Ko'tarilmaydigan hodisalar va ichkaridagi stopPropagation delegation'ni buzadi; bosiladigan narsa — haqiqiy tugma yoki havola.

Keyingi dars: Listener opsiyalari: once, passive, signal — uchinchi argument bilan tinglovchini bir martalik qilamiz, surishni tezlashtiramiz va ko'p tinglovchini bitta buyruq bilan olib tashlaymiz.

Manbalar

  • MDN: "Event delegation" (Introduction to events bo'limi) — developer.mozilla.org
  • MDN: "Element: closest() method", "Node: contains() method" — developer.mozilla.org
  • MDN: "HTMLElement: dataset property" — developer.mozilla.org
  • javascript.info: "Event delegation" — javascript.info
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript event delegation: bitta tinglovchi, closest va data-amal naqshi — IlmHamroh