عناصر سفارشی

ویژگی‌های وب: عناصر سفارشی مستقل

عنصر سفارشی کلاسی است که ازطریق customElements.define() برای نام برچسب ثبت شده است. ثبت‌نام و چرخه حیات پایه به‌خوبی درک شده‌اند؛ قوانین زیر بخش‌هایی را هدف قرار می‌دهند که معمولاً به‌طور نادرست پیاده‌سازی می‌شوند.

چرخه حیات: بخش‌هایی که اشتباه پیاده‌سازی می‌شوند

خود فراخوانی‌های برگشتی به‌خوبی مستند شده‌اند؛ این‌ها تصورات اشتباهی هستند که باعث ایجاد اشکال می‌شوند:

  • connectedCallback می‌تواند بیش‌از یک‌بار اجرا شود. این رویداد با هر درج فعال می‌شود، بنابراین جابه‌جا کردن عنصر باعث اجرای مجدد آن می‌شود. راه‌اندازی را یکسان‌توان کنید — فرض نکنید که فقط یک‌بار اجرا می‌شود.
  • connectedMoveCallback جفت قطع/ وصل مجدد را در یک حرکت رد می‌کند. وقتی عنصری با Node.moveBefore() جابه‌جا می‌شود، این فراخوانی برگشتی به‌جای disconnectedCallback ← connectedCallback اجرا می‌شود، بنابراین وضعیت، تمرکز، و پخش iframe/رسانه پس‌از جابه‌جایی حفظ می‌شود. زمانی آن را تعریف کنید که راه‌اندازی یا جمع‌آوری شما پرهزینه باشد یا به‌طور قابل‌مشاهده‌ای دوباره اجرا شود.
  • در disconnectedCallback تخریب می‌شود. شنوندگان، زمان‌سنج‌ها، و ناظران سراسری را بردارید وگرنه نشت خواهید داشت؛ اما ممکن است عنصر دوباره درج شود، بنابراین جداسازی را دائمی درنظر نگیرید.
  • ابتدا super() را در سازنده فراخوانی کنید و به وجود DOM کامل وابسته نباشید. Calling super() after touching this throws a ReferenceError. مشخصه‌ها معمولاً (اما نه همیشه) وجود دارند؛ تجزیه فهرست‌های طولانی فرزندان ممکن است تمام نشده باشد. از connectedCallback برای مقداردهی اولیه تک‌باره، از slotchange برای نظارت بر فرزندان مستقیم، و از attributeChangedCallback برای نظارت بر مشخصه‌ها استفاده کنید. connectedCallback ممکن است همچنان قبل‌از تجزیه شدن همه فرزندان فعال شود، بنابراین تماس‌های slotchange / attributeChangedCallback بعدی را نادیده نگیرید.
  • attributeChangedCallback فقط برای observedAttributes فعال می‌شود و باید برای مشخصه‌هایی که بر برونداد تأثیر نمی‌گذارند ارزان بماند.
  • قلاب‌های چرخه حیات و observedAttributes در زمان ثبت‌نام عکس‌برداری می‌شوند. customElements.define() هر دو را یک‌بار می‌خواند؛ افزودن قلاب یا گسترش observedAttributes در کلاس پس‌از تماس بدون اطلاع نادیده گرفته می‌شود. به‌ویژه، attributeChangedCallback هرگز فعال نمی‌شود مگراینکه observedAttributes ازقبل در زمان define() وجود داشته باشد.

زمان‌بندی ارتقا و :defined

یک عنصر می‌تواند قبل‌از ثبت کلاس خود در DOM وجود داشته باشد (در HTML ارائه‌شده توسط سرور، یا زمانی که اسکریپت دیر بار می‌شود). Until defined it is an inert HTMLElement with none of your behavior; when customElements.define() runs, the browser upgrades every matching element already in the document, running its constructor and connectedCallback then.

دو پیامدی که مدل‌ها معمولاً ازدست می‌دهند:

  1. وقتی سازنده/connectedCallback شما در ارتقا اجرا می‌شود، ممکن است مشخصه‌ها و فرزندان ازقبل وجود داشته باشند. مشخصه‌های موجود را در connectedCallback بخوانید تا وضعیت اولیه همگام‌سازی شود و هرگز فرض نکنید که عنصر در ابتدا خالی بوده است.
  2. ممکن است فرزندان همچنان وجود نداشته باشند حتی در connectedCallback — تجزیه‌کننده HTML می‌تواند در اواسط تجزیه فهرست‌های طولانی فرزندان نتیجه دهد، بنابراین فراخوانی برگشتی گاهی قبل‌از پیوست شدن هر فرزند اجرا می‌شود. برای الگوی (slotchange + attributeChangedCallback)، به نکته سازنده در بخش چرخه حیات در بالا مراجعه کنید.

عناصر تعریف‌نشده را پنهان کنید تا از نمایش محتوای بدون سبک جلوگیری شود — اما fail open را فعال کنید تا اگر اسکریپت هرگز بار نشد، محتوا همچنان قابل‌دسترسی باشد. opacity را با تأخیر پویانمایی کنید، نه اینکه با visibility: hidden آن را کاملاً پنهان کنید، تا عنصر پس‌از اتمام مهلت نمایان شود، حتی اگر هرگز ارتقا داده نشود:

my-element:not(:defined) {
  opacity: 0;
  /* Reveal anyway after 2s, so a failed or slow script never hides content for good. */
  animation: defined-fallback 0.2s 2s forwards;
}
@keyframes defined-fallback { to { opacity: 1; } }

این محافظ‌ها را به‌جای کل body، به عناصری که به آن‌ها نیاز دارند — یا به یک زیردرخت خاص ازطریق :has(:not(:defined)) — محدود کنید. یک ابزاره کند و غیربحرانی (مثلاً دکمه هم‌رسانی) نباید رنگ‌آمیزی کل صفحه را به‌تأخیر بیندازد.

بازتاب مشخصه ⇄ ویژگی

مشخصه HTML و ویژگی جاوااسکریپت را همگام‌سازی کنید تا عنصر هم از نشانه‌گذاری و هم از اسکریپت قابل‌استفاده باشد. مقادیر مشخصه همیشه رشته هستند؛ آن‌ها را اجبار کنید.

class MyToggle extends HTMLElement {
  static observedAttributes = ['label', 'count', 'disabled'];

  // String attribute reflected via getter/setter.
  get label() { return this.getAttribute('label') ?? ''; }
  set label(v) {
    v ? this.setAttribute('label', v) : this.removeAttribute('label');
  }

  // Numeric attribute: coerce from string on read, serialize on write.
  get count() { return Number(this.getAttribute('count')) || 0; }
  set count(v) { this.setAttribute('count', String(v)); }

  // Boolean attribute: present = true, absent = false. Never set it to "false".
  get disabled() { return this.hasAttribute('disabled'); }
  set disabled(on) { this.toggleAttribute('disabled', !!on); }

  attributeChangedCallback(name, oldVal, newVal) {
    if (oldVal !== newVal) this.render();
  }
}

جایی که ارزش زندگی می‌کند

مثال بالا DOM را به‌عنوان منبع حقیقت نگه می‌دارد — هر گیرنده getAttribute را می‌خواند. این پیش‌فرض مناسبی برای چند مشخصه ساده رشته‌مانند است: فقط یک مکان وجود دارد که وضعیت می‌تواند در آن قرار بگیرد، بنابراین نمی‌تواند ناهماهنگ شود.

برای مؤلفه‌های غنی‌تر، منبع حقیقت را در فیلد JS نگه دارید و فقط زمانی‌که چیز دیگری باید آن را ببیند (هدف‌یابی CSS یا خواندن آن توسط مصرف‌کننده)، آن را به یک مشخصه منعکس کنید. خواندن فیلد بسیار ارزان‌تر از تجزیه و اجبار مشخصه در هر دسترسی است و مقادیر غیررشته‌ای هرگز ازطریق سریال‌سازی رفت‌وبرگشت نمی‌کنند. به‌همین دلیل است که کتابخانه‌هایی مانند Lit وضعیت را در JS نگه می‌دارند و به‌جای اینکه با مشخصه‌ها به‌عنوان فضای ذخیره‌سازی رفتار کنند، به‌صورت انتخابی منعکس می‌کنند: فقط آنچه را که دنیای خارج باید مشاهده کند منعکس می‌کنند، نه هر ویژگی را.

تله‌ها:

  • observedAttributes discipline: list only attributes whose change must re-render or trigger a side effect. مشاهده همه‌چیز باعث می‌شود که برای هر جهش یک فراخوانی برگشتی اضافه شود که هیچ فایده‌ای ندارد.
  • حلقه‌های بازتاب: تنظیم‌کننده‌ای که مشخصه‌ای را می‌نویسد، دوباره وارد attributeChangedCallback می‌شود. با بررسی oldVal !== newVal، همان‌طور که در بالا گفته شد، محافظت کنید.
  • حالت فقط سبک: برای حالت داخلی که فقط برای هدایت سبک (بارگیری، فعال، گسترش‌یافته) وجود دارد، حالت‌های سفارشی را به بازتاب یک ویژگی مصنوعی ترجیح دهید — به حالت‌های سفارشی در زیر مراجعه کنید. فقط زمانی به یک مشخصه منعکس کنید که مصرف‌کنندگان باید بتوانند وضعیت را از نشانه‌گذاری تنظیم کنند.

ایالت‌های سفارشی

حالت‌های سفارشی پرچم‌های بولی هستند که یک عنصر درباره خودش برای CSS نشان می‌دهد تا هدف قرار دهد، ازطریق ElementInternals تنظیم می‌شود، و با شبه‌کلاس :state() مطابقت داده می‌شود. از آن‌ها برای وضعیت میانای کاربری داخلی و زمان اجرا — busy،‏ expanded،‏ invalid — به‌جای بازتاب دادن مشخصه مصنوعی استفاده کنید: آن‌ها نمی‌توانند با مشخصه‌های مصرف‌کننده تداخل داشته باشند، هرگز در DOM ظاهر نمی‌شوند، و از بیرون فقط خواندنی هستند (مصرف‌کننده می‌تواند :state() را سبک‌بندی کند اما نمی‌تواند آن را جعل کند).

class AsyncButton extends HTMLElement {
  #internals;
  #action = async () => {};

  constructor() {
    super();
    this.#internals = this.attachInternals();
  }

  set action(fn) { this.#action = fn; } // host supplies the work to run

  async run() {
    if (this.#internals.states.has('busy')) return; // guard re-entry
    this.#internals.states.add('busy');
    this.#internals.states.delete('success');
    try {
      await this.#action();
      this.#internals.states.add('success');
    } catch {
      this.#internals.states.add('error');
    } finally {
      this.#internals.states.delete('busy');
    }
  }
}

‫CustomStateSet یک مجموعه است، بنابراین حالت‌های مستقل بدون ایجاد یک enum ترکیبی data-status، ترکیب می‌شوند (busy به‌علاوه error).

وضعیت سفارشی فقط زمان اجرا است. این ویژگی به DOM سریال‌سازی نمی‌شود، بنابراین درصورت بار کردن مجدد، بازآفرینی عنصر، یا پردازش سرور باقی نمی‌ماند. اگر وضعیت باید به‌صورت اعلانی در نشان‌گذاری تنظیم شود یا در بارگیری‌ها حفظ شود، این کار برای مشخصه است (به محل قرارگیری مقدار مراجعه کنید) — آن را در connectedCallback از منبع حقیقت خود بازیابی کنید. قاعده کلی: حالت سفارشی برای آنچه مؤلفه در زمان اجرا مشتق می‌کند و مالک آن است و فقط به سبک نیاز دارد؛ مشخصه‌ای برای آنچه باید از نشان‌گذاری یا سریال‌سازی تنظیم شود.

برای سبک‌بندی این حالت‌ها — هم در داخل مؤلفه و هم از برگه سبک نگارش مصرف‌کننده — به سبک‌بندی مؤلفه‌های وب مراجعه کنید.

قراردادهای نام‌گذاری

  • True-private state: native # fields (#count) for hard encapsulation; a _ prefix only when subclasses must still reach it. توجه: فیلدهای # به ارث برده نمی‌شوند — زیرکلاس نمی‌تواند فیلدهای خصوصی کلاس پایه را ببیند.
  • ویژگی‌های کلیدنمادی حد وسط هستند: از شمارش معمولی پنهان هستند، اما برای هر کدی که نماد را نگه می‌دارد قابل‌دسترسی هستند (آن را صادر کنید یا به‌عنوان فیلد static در کلاس نمایش دهید). چون هر نماد منحصربه‌فرد است، دو نماد با شرح مشترک هرگز باهم تداخل پیدا نمی‌کنند — این ویژگی زمانی مفید است که یک زیرکلاس یا کلاس هم‌سطح به دسترسی کنترل‌شده‌ای نیاز داشته باشد که فیلدهای # نمی‌توانند ارائه دهند.
  • مشخصه‌های سراسری (style، class، id، slot، part، title، lang، dir) را با ویژگی‌های خودتان سایه نزنید؛ این کار رفتار داخلی آن‌ها را مختل می‌کند. توجه داشته باشید که disabled فقط در کنترل‌های فرم سراسری است؛ در عناصر دیگر باید خودتان تأثیر آن را پیاده‌سازی کنید.

هنگام نام‌گذاری یک سطح API جدید، از قراردادهای خود پلاتفرم پیروی کنید: از kebab-case با حروف کوچک با حداقل یک خط تیره برای نام‌های برچسب عنصر سفارشی (برای مثال my-toggle)، lowerCamelCase برای ویژگی‌ها و روش‌ها، و نام‌های مشخصه با حروف کوچک همگام‌سازی‌شده با ویژگی‌های مربوطه استفاده کنید.