المانهای مدرن و Web Components
المانهای مدرن و Web Components؛ آموزش HTML به زبان ساده، همراه با مثال و خروجی.
<dialog>
پنجرهی گفتگوی بومی با focus trap، Esc و ::backdrop:
<button id="open">باز کردن</button>
<dialog id="dlg" aria-labelledby="dlg-title">
<form method="dialog">
<h2 id="dlg-title">تأیید حذف</h2>
<p>مطمئنی؟</p>
<button value="cancel">انصراف</button>
<button value="ok">حذف</button>
</form>
</dialog>
open.addEventListener("click", () => dlg.showModal()); // مودال (با backdrop)
// dlg.show() → غیرمودال
dlg.addEventListener("close", () => console.log(dlg.returnValue)); // "ok" یا "cancel"
dlg.close("ok");
dialog::backdrop { background: rgb(0 0 0 / .5); backdrop-filter: blur(4px); }
closedby="any"(جدید): بستن با کلیک بیرون.
Popover API
منوی بازشو/تولتیپ بدون JS:
<button popovertarget="menu">منو</button>
<div id="menu" popover>
<a href="/a">گزینهی ۱</a>
<a href="/b">گزینهی ۲</a>
</div>
<button popovertarget="tip" popovertargetaction="toggle">؟</button>
<div id="tip" popover="manual">راهنما</div>
popover(auto): با کلیک بیرون یا Esc بسته میشه.popover="manual": فقط دستی.- روی top layer نمایش داده میشه (z-index نمیخواد).
[popover]:popover-open { /* وقتی باز است */ }
[popover]::backdrop { }
API: el.showPopover(), hidePopover(), togglePopover().
<details> اکاردئون
<details name="faq" open>
<summary>سوال ۱</summary>
<p>پاسخ</p>
</details>
<details name="faq"><summary>سوال ۲</summary><p>پاسخ</p></details>
name مشترک ⇒ فقط یکی باز میمونه. رویداد toggle. استایل: details[open] summary { }, summary::marker.
inert
غیرفعال کردن کل یک زیردرخت (کلیک، فوکوس، دیده نشدن برای صفحهخوان):
<main inert>…</main> <!-- مثلاً وقتی مودال سفارشی باز است -->
Lazy و Priority
<img loading="lazy" decoding="async" fetchpriority="low" />
<iframe loading="lazy"></iframe>
contenteditable
<div contenteditable="true" role="textbox" aria-multiline="true">متن قابل ویرایش</div>
<div contenteditable="plaintext-only">فقط متن خام</div>
hidden="until-found"
<div hidden="until-found">با Ctrl+F پیدا و باز میشه</div>
<search>
<search>
<form action="/search"><input type="search" name="q" /><button>جستجو</button></form>
</search>
Drag & Drop
<div draggable="true" id="box">بکش</div>
<div id="zone">بیانداز</div>
box.addEventListener("dragstart", (e) => e.dataTransfer.setData("text/plain", box.id));
zone.addEventListener("dragover", (e) => e.preventDefault()); // اجازهی drop
zone.addEventListener("drop", (e) => {
e.preventDefault();
zone.append(document.getElementById(e.dataTransfer.getData("text/plain")));
});
Microdata / RDFa (جایگزین JSON-LD)
<div itemscope itemtype="https://schema.org/Product">
<span itemprop="name">هدفون</span>
<span itemprop="offers" itemscope itemtype="https://schema.org/Offer">
<span itemprop="price" content="1500000">۱٬۵۰۰٬۰۰۰</span>
<meta itemprop="priceCurrency" content="IRR" />
</span>
</div>
Web Components
استاندارد مرورگر برای ساخت المانهای سفارشی و قابل استفادهی مجدد، بدون فریمورک. سه ستون:
- Custom Elements (تگ جدید با رفتار)
- Shadow DOM (کپسولهسازی استایل و ساختار)
- HTML Templates (
<template>و<slot>)
<template>
محتوای غیرفعال که تا clone نشه رندر نمیشه:
<template id="card-tpl">
<article class="card">
<h3></h3>
<p></p>
</article>
</template>
<script>
const t = document.getElementById("card-tpl");
const node = t.content.cloneNode(true);
node.querySelector("h3").textContent = "عنوان";
node.querySelector("p").textContent = "متن";
document.body.append(node);
</script>
Custom Element
نام حتماً شامل خط تیره باشه.
class HelloCard extends HTMLElement {
static observedAttributes = ["name"]; // ویژگیهای تحت نظر
constructor() {
super(); // اجباری
}
connectedCallback() { // وارد DOM شد
this.render();
}
disconnectedCallback() {} // از DOM خارج شد
adoptedCallback() {} // به سند دیگر منتقل شد
attributeChangedCallback(name, oldVal, newVal) {// تغییر ویژگی
if (this.isConnected) this.render();
}
get name() { return this.getAttribute("name") ?? "ناشناس"; }
set name(v) { this.setAttribute("name", v); }
render() {
this.textContent = `سلام ${this.name}!`;
}
}
customElements.define("hello-card", HelloCard);
<hello-card name="علی"></hello-card>
Shadow DOM
class UserBadge extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: "open" }); // open: root از بیرون قابل دسترسی
root.innerHTML = `
<style>
:host { display: inline-block; font-family: inherit; }
:host([vip]) .badge { background: gold; }
.badge { padding: .25em .6em; border-radius: 1em; background: #eee; }
::slotted(strong) { color: crimson; }
</style>
<span class="badge" part="badge"><slot name="icon"></slot><slot>کاربر</slot></span>
`;
}
}
customElements.define("user-badge", UserBadge);
<user-badge vip><span slot="icon">⭐</span><strong>علی</strong></user-badge>
مفاهیم:
- استایل کپسوله: CSS بیرون داخل shadow نمیره و بالعکس.
:host,:host(.x),:host-context(): استایل خود المان.<slot>: محل قرارگیری فرزندان نوشتهشده توسط کاربر؛slot="name"برای slot های نامدار؛::slotted()برای استایل محتوای slot شده.- CSS custom properties از شیلد رد میشن (راه تمدهی):
user-badge { --badge-bg: tomato; }
/* داخل shadow: background: var(--badge-bg, #eee); */
partو::part(): بیرون بتونه بخشهای مشخصی رو استایل کنه:
user-badge::part(badge) { font-weight: bold; }
- حالت
closed: root از بیرون در دسترس نیست. delegatesFocus: trueبرای انتقال فوکوس به داخل.
رویداد از Shadow DOM
this.dispatchEvent(new CustomEvent("selected", {
detail: { id: 5 },
bubbles: true,
composed: true, // از مرز shadow عبور کنه
}));
event.targetدر بیرون: خود host (retargeting)؛ مسیر کامل:event.composedPath().
فرم و Custom Element (ElementInternals)
class StarRating extends HTMLElement {
static formAssociated = true;
#internals = this.attachInternals();
#value = 0;
get value() { return this.#value; }
set value(v) {
this.#value = v;
this.#internals.setFormValue(String(v)); // مقدار در FormData
this.#internals.setValidity(v ? {} : { valueMissing: true }, "امتیاز بدهید");
}
}
customElements.define("star-rating", StarRating);
Customized Built-in (در Safari پشتیبانی نمیشه)
class FancyButton extends HTMLButtonElement {}
customElements.define("fancy-button", FancyButton, { extends: "button" });
// <button is="fancy-button">
Declarative Shadow DOM (SSR)
<user-badge>
<template shadowrootmode="open">
<style>span { color: green }</style>
<span><slot></slot></span>
</template>
علی
</user-badge>
Constructable Stylesheets
const sheet = new CSSStyleSheet();
sheet.replaceSync(".a { color: red }");
root.adoptedStyleSheets = [sheet]; // اشتراک یک stylesheet بین چند component
Lit (کتابخانهی سبک روی Web Components)
import { LitElement, html, css } from "lit";
class MyCounter extends LitElement {
static properties = { count: { type: Number } };
static styles = css`button { padding: .5em 1em }`;
count = 0;
render() { return html`<button @click=${() => this.count++}>${this.count}</button>`; }
}
customElements.define("my-counter", MyCounter);
مزایا / محدودیتها
- ✅ بدون فریمورک، در هر فریمورکی (React/Vue/Angular) قابل استفاده.
- ✅ کپسولهسازی واقعی استایل.
- ❌ SSR و فرمها نیاز به کار اضافه دارن.
- ❌ دسترسپذیری (ARIA) رو باید خودت مدیریت کنی (a11y).
- ❌ Shadow DOM با بعضی کتابخانههای CSS سراسری (مثل Tailwind) ناسازگاره مگر با adoptedStyleSheets.
مثال کامل: Accordion ساده
<my-accordion>
<h3 slot="title">عنوان</h3>
<p>محتوا…</p>
</my-accordion>
<script>
class MyAccordion extends HTMLElement {
#open = false;
constructor() {
super();
this.attachShadow({ mode: "open" }).innerHTML = `
<style>
.body { display: none }
:host([open]) .body { display: block }
button { all: unset; cursor: pointer; font-weight: bold }
</style>
<button aria-expanded="false"><slot name="title"></slot></button>
<div class="body"><slot></slot></div>`;
this.btn = this.shadowRoot.querySelector("button");
this.btn.addEventListener("click", () => this.toggleAttribute("open"));
}
static observedAttributes = ["open"];
attributeChangedCallback() {
this.btn.setAttribute("aria-expanded", String(this.hasAttribute("open")));
}
}
customElements.define("my-accordion", MyAccordion);
</script>
تمرین
۱. کامپوننت <toast-message> با slot و ویژگی type="success|error" بساز که بعد از ۳ ثانیه خودش حذف بشه. ۲. با <dialog> یک مودال تأیید بنویس که مقدار returnValue رو بخونه. ۳. با Popover API یک منوی کشویی بدون JS بساز.