العناصر المخصصة

العنصر المخصّص هو فئة مسجّلة مقابل اسم علامة من خلال customElements.define(). يتم فهم التسجيل ومراحل النشاط الأساسية جيدًا، وتستهدف القواعد أدناه الأجزاء التي يتم تنفيذها بشكل غير صحيح بشكل روتيني.

مراحل النشاط: الأجزاء التي يتم تنفيذها بشكل خاطئ

يتم توثيق عمليات الاستدعاء نفسها بشكل جيد، ولكن هناك بعض المفاهيم الخاطئة التي تسبب الأخطاء:

  • يمكن تشغيل connectedCallback أكثر من مرة. يتم تشغيلها عند كل عملية إدراج، لذا يؤدي نقل العنصر إلى إعادة تشغيلها. يجب أن يكون الإعداد متكررًا، ولا تفترض أنّه سيتم تشغيله مرة واحدة فقط.
  • يتخطّى connectedMoveCallback زوج قطع الاتصال/إعادة الاتصال عند النقل. عند نقل عنصر باستخدام Node.moveBefore()، يتم تنشيط دالة الاستدعاء هذه بدلاً من disconnectedCallback → connectedCallback، وبالتالي تظل الحالة والتركيز وتشغيل iframe/الوسائط كما هي بعد النقل. حدِّدها عندما يكون الإعداد أو الإزالة مكلفًا أو تتم إعادة تشغيله بشكل ملحوظ.
  • سيتم إيقاف الميزة بعد disconnectedCallback. أزِل المستمعين والموقّتات والمراقبين العامين أو تسرَّب، ولكن قد تتم إعادة إدراج العنصر، لذا لا تتعامل مع عملية الإزالة على أنّها دائمة.
  • يجب استدعاء super() أولاً في الدالة الإنشائية، وعدم الاعتماد على توفّر نموذج العناصر في المستند (DOM) بالكامل. يؤدي استدعاء super() بعد لمس this إلى ظهور ReferenceError. تكون السمات موجودة عادةً (ولكن ليس دائمًا)، وقد لا تكون القوائم الطويلة من العناصر الفرعية قد انتهت من التحليل. استخدِم connectedCallback للتهيئة لمرة واحدة، وslotchange لمراقبة العناصر الثانوية المباشرة، وattributeChangedCallback لمراقبة السمات. قد يستمر تشغيل connectedCallback قبل تحليل جميع العناصر الثانوية، لذا لا تتجاهل طلبات slotchange / attributeChangedCallback اللاحقة.
  • يتم تشغيل attributeChangedCallback فقط لـ observedAttributes، ويجب أن تظل منخفضة التكلفة للسمات التي لا تؤثر في الناتج.
  • يتم التقاط لقطات من دوال lifecycle hooks و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).

إخفاء العناصر غير المحدّدة لتجنُّب ظهور محتوى غير منسَّق، ولكن يجب أن يظل المحتوى متاحًا في حال عدم تحميل النص البرمجي. يمكنك إضافة تأثيرات حركية إلى 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; } }

يمكنك حصر نطاق هذه الحواجز على العناصر التي تحتاج إليها، أو على شجرة فرعية معيّنة من خلال :has(:not(:defined))، بدلاً من body بأكملها. يجب ألا يؤدي تطبيق مصغّر واحد بطيء وغير مهم (مثل زر مشاركة) إلى تأخير عرض الصفحة بأكملها.

انعكاس السمة ⇄ الخاصية

يجب الحفاظ على مزامنة سمة HTML وسمة JS لكي يكون العنصر قابلاً للاستخدام من كل من الترميز والنص البرمجي. تكون قيم السمات سلاسل دائمًا، لذا يجب فرضها.

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 كمصدر موثوق، حيث يقرأ كل دالة getter السمة getAttribute. هذا هو الإعداد التلقائي المناسب لعدد قليل من السمات البسيطة التي تشبه السلسلة: هناك مكان واحد فقط يمكن أن تتوفّر فيه الحالة، لذا لا يمكن إلغاء مزامنتها.

بالنسبة إلى المكوّنات الأكثر ثراءً، احتفظ بمصدر المعلومات في حقل JS واعرضه خارجيًا في سمة فقط عندما يحتاج شيء آخر إلى رؤيته، مثل استهداف CSS أو قراءة المستهلك له. إنّ قراءة حقل أقل تكلفة بكثير من تحليل سمة وفرضها في كل عملية وصول، ولا يتم مطلقًا نقل القيم غير النصية ذهابًا وإيابًا من خلال التسلسل. لهذا السبب، تحتفظ مكتبات مثل Lit بالحالة في JS وتعكسها بشكل انتقائي بدلاً من التعامل مع السمات كمساحة تخزين: تعكس فقط ما يجب أن يلاحظه العالم الخارجي، وليس كل سمة.

الصعاب:

  • observedAttributes discipline: list only attributes whose change must re-render or trigger a side effect. تؤدي مراقبة كل شيء إلى إضافة استدعاء لكل تغيير بدون فائدة.
  • حلقات التكرار: ستتم إعادة إدخال attributeChangedCallback في دالة setter التي تكتب سمة. يجب حماية هذه الطريقة من خلال التحقّق من 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 هي مجموعة، لذا تتألف الحالات المستقلة من (busy بالإضافة إلى error) بدون إنشاء تعداد data-status مجمّع.

الحالة المخصّصة هي وقت التشغيل فقط. لا يتم تسلسله إلى DOM، لذا لا يستمر بعد إعادة التحميل أو إعادة إنشاء العنصر أو عرض الخادم. إذا كان يجب ضبط الحالة بشكل تعريفي في الترميز أو الاحتفاظ بها على مستوى عمليات التحميل، فهذه مهمة سمة (راجِع مكان القيمة) - يمكنك استعادتها في connectedCallback من مصدر الحقيقة. قاعدة عامة: الحالة المخصّصة لما يستمدّه المكوّن ويمتلكه في وقت التشغيل ويحتاج فقط إلى تنسيقه، وهي سمة لما يجب ضبطه من الترميز أو تسلسله.

للاطّلاع على كيفية تصميم هذه الحالات، سواء داخل المكوّن أو من ورقة أنماط المستهلك، يُرجى مراجعة تصميم مكوّنات الويب.

اصطلاحات التسمية

  • الحالة الخاصة الحقيقية: حقول # أصلية (#count) للتغليف الثابت، وبادئة _ فقط عندما تظل الفئات الفرعية بحاجة إلى الوصول إليها. يُرجى العِلم أنّ حقول # لا يتم اكتسابها، أي لا يمكن لفئة فرعية الاطّلاع على الحقول الخاصة لصنف أساسي.
  • السمات التي تستخدم الرموز كمفاتيح هي حل وسط: فهي مخفية عن التعداد العادي، ولكن يمكن الوصول إليها من خلال أي رمز يحمل الرمز (تصديره أو عرضه كحقل static في الفئة). بما أنّ كل رمز فريد، لا يحدث تداخل بين رمزين لهما الوصف نفسه، وهو أمر مفيد عندما تحتاج فئة فرعية أو فئة شقيقة إلى وصول خاضع للتحكّم لا يمكن أن توفّره حقول #.
  • لا تحجب السمات العامة (style وclass وid وslot وpart وtitle وlang وdir) باستخدام سماتك الخاصة، لأنّ ذلك يؤدي إلى تعطيل سلوكها المضمّن. يُرجى العِلم أنّ السمة disabled تكون عامة فقط في عناصر التحكّم في النماذج، أما في العناصر الأخرى، فيجب تنفيذ تأثيرها بنفسك.

عند تسمية واجهة برمجة تطبيقات جديدة، اتّبِع اصطلاحات المنصة: استخدِم الأحرف الصغيرة مع فصل الكلمات بشرطة مع شرطة واحدة على الأقل لأسماء علامات العناصر المخصّصة (على سبيل المثال my-toggle)، وlowerCamelCase للسمات والطرق، وأسماء السمات بالأحرف الصغيرة المتزامنة مع السمات المقابلة لها.