الإصدار 1 من العناصر المخصّصة - مكونات ويب قابلة لإعادة الاستخدام

تسمح العناصر المخصّصة لمطوّري الويب بتحديد علامات HTML جديدة وتوسيع نطاق العلامات الحالية وإنشاء مكونات ويب قابلة لإعادة الاستخدام.

باستخدام العناصر المخصّصة، يمكن لمطوّري الويب إنشاء علامات HTML جديدة، أو تعزيز علامات HTML الحالية، أو توسيع المكونات التي أنشأها مطوّرون آخرون. واجهة برمجة التطبيقات هي الأساس لمكوّنات الويب. وتوفّر هذه الطريقة المتوافقة مع معايير الويب لإنشاء مكوّنات قابلة لإعادة الاستخدام باستخدام لغة برمجة JS/HTML/CSS. والنتيجة هي استخدام رموز أقل ورمز وحدات وإعادة استخدام أكبر في تطبيقاتنا.

مقدمة

يوفّر لنا المتصفّح أداة ممتازة لتنظيم تطبيقات الويب. يُعرف باسم HTML. ربما سمعت عن ذلك. وهي لغة برمجة تعريفية وقابلة للنقل ومدعومة بشكل جيد وسهلة الاستخدام. على الرغم من أنّ لغة HTML رائعة، إلا أنّ مفرداتها وإمكانية توسيع نطاقها محدودة. لم يكن المعيار المتغيّر لـ HTML يتضمّن دائمًا طريقة لربط سلوك JavaScript تلقائيًا بعلامات الترميز، إلى أن تم توفير هذه الميزة الآن.

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

تحديد عنصر جديد

لتعريف عنصر HTML جديد، نحتاج إلى إمكانات JavaScript.

يتم استخدام العنصر الشامل customElements لتحديد عنصر مخصّص وإعلام المتصفّح بعلامة جديدة. استخدِم customElements.define() مع اسم العلامة التي تريد إنشاؤها وclass JavaScript الذي يمدّد القاعدة HTMLElement.

مثال: تحديد لوحة أدراج على الأجهزة الجوّالة، <app-drawer>:

class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);

// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', class extends HTMLElement {...});

مثال على الاستخدام:

<app-drawer></app-drawer>

من المهم تذكُّر أنّ استخدام عنصر مخصّص لا يختلف عن استخدام <div> أو أي عنصر آخر. يمكن الإعلان عن النماذج على الصفحة، وإنشاؤها ديناميكيًا في JavaScript، ويمكن إرفاق مستمعي الأحداث، وما إلى ذلك. يمكنك متابعة قراءة هذه المقالة للاطّلاع على مزيد من الأمثلة.

تحديد واجهة برمجة تطبيقات JavaScript للعنصر

يتم تحديد وظيفة العنصر المخصّص باستخدام ES2015 class الذي يمتد إلى HTMLElement. يضمن تمديد HTMLElement أن يرث العنصر المخصّص واجهة برمجة التطبيقات DOM API بالكامل، ويعني ذلك أنّ أيّ سمات أو طرق تضيفها إلى الفئة تصبح جزءًا من واجهة DOM للعنصر. في الأساس، استخدِم الفئة ل إنشاء واجهة برمجة تطبيقات JavaScript عامة لعلامتك.

مثال: تحديد واجهة DOM لعنصر <app-drawer>:

class AppDrawer extends HTMLElement {

  // A getter/setter for an open property.
  get open() {
    return this.hasAttribute('open');
  }

  set open(val) {
    // Reflect the value of the open property as an HTML attribute.
    if (val) {
      this.setAttribute('open', '');
    } else {
      this.removeAttribute('open');
    }
    this.toggleDrawer();
  }

  // A getter/setter for a disabled property.
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    // Reflect the value of the disabled property as an HTML attribute.
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Can define constructor arguments if you wish.
  constructor() {
    // If you define a constructor, always call super() first!
    // This is specific to CE and required by the spec.
    super();

    // Setup a click listener on <app-drawer> itself.
    this.addEventListener('click', e => {
      // Don't toggle the drawer if it's disabled.
      if (this.disabled) {
        return;
      }
      this.toggleDrawer();
    });
  }

  toggleDrawer() {
    // ...
  }
}

customElements.define('app-drawer', AppDrawer);

في هذا المثال، سننشئ درجًا يحتوي على سمة open وسمة disabled وطريقة toggleDrawer(). ويُظهر أيضًا السمات على أنّها سمات HTML.

من الميزات الرائعة للعناصر المخصّصة أنّ this داخل تعريف فئة يشير إلى عنصر DOM نفسه، أي مثيل الفئة. في مثالنا، يشير this إلى <app-drawer>. (😉) هذه هي الطريقة التي يمكن للعنصر من خلالها إرفاق مستمع click بنفسه. ولا تقتصر على أدوات معالجة الأحداث. تتوفّر واجهة برمجة التطبيقات DOM API بالكامل داخل رمز العنصر. استخدِم this للوصول إلى سمات العنصر، وفحص عناصره الفرعية (this.children)، وعقد الاستعلامات (this.querySelectorAll('.items'))، وما إلى ذلك.

قواعد إنشاء عناصر مخصّصة

  1. يجب أن يحتوي اسم العنصر المخصّص على واصلة (-). وبالتالي، فإنّ <x-tags> و<my-element>و<my-awesome-app> هي أسماء صالحة، في حين أنّ <tabs> و<foo_bar> غير صالحَين. ويُستخدَم هذا الشرط لكي يتمكّن منظِّم HTML من التمييز بين العناصر المخصّصة والعناصر العادية. ويضمن أيضًا التوافق مع الإصدارات القادمة عند إضافة علامات جديدة إلى HTML.
  2. لا يمكنك تسجيل العلامة نفسها أكثر من مرّة. سيؤدي محاولة إجراء ذلك إلى ظهور DOMException. بعد إبلاغ المتصفّح بعلامة جديدة، لن يكون هناك أي إجراء آخر. لا يمكن إرجاع المنتجات.
  3. لا يمكن أن تكون العناصر المخصّصة ذات إغلاق ذاتي لأنّ HTML لا يسمح إلا ببضعة عناصر بإغلاق ذاتي. اكتب دائمًا علامة إغلاق (<app-drawer></app-drawer>).

تفاعلات العناصر المخصّصة

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

الاسم يتم استدعاؤه عند
constructor يتم إنشاء مثيل للعنصر أو ترقيته. وهي مفيدة لإعداد الحالة أو إعداد أدوات معالجة الأحداث أو إنشاء عنصر shadow dom. يمكنك الاطّلاع على المواصفات لمعرفة القيود المفروضة على الإجراءات التي يمكنك اتّخاذها في constructor.
connectedCallback يتمّ استدعاؤها في كلّ مرّة يتمّ فيها إدخال عنصر في نموذج DOM. مفيدة لتشغيل رمز الإعداد، مثل جلب الموارد أو المعالجة بشكل عام، يجب محاولة تأخير العمل إلى هذا الوقت.
disconnectedCallback يتمّ استدعاؤه في كلّ مرّة تتمّ فيها إزالة العنصر من DOM. مفيد لتشغيل رمز برمجي لتنظيف الملفات.
attributeChangedCallback(attrName, oldVal, newVal) يتمّ استدعاؤه عند إضافة سمة تمّ رصدها أو إزالتها أو تعديلها أو استبدالها. يتمّ استدعاء هذه الوظيفة أيضًا للقيم الأوّلية عند إنشاء عنصر من قِبل المُحلِّل أو ترقيته. ملاحظة: لن تتلقّى سوى السمات المدرَجة في السمة observedAttributes هذا المرجع إلى دالة.
adoptedCallback تم نقل العنصر المخصّص إلى document جديد (مثل شخص يُدعى document.adoptNode(el)).

تكون عمليات استدعاء التفاعلات متزامنة. إذا اتصل أحد الأشخاص بـ el.setAttribute() في العنصر، سيتصل المتصفّح بـ attributeChangedCallback() على الفور. وبالمثل، ستتلقّى disconnectedCallback() مباشرةً بعد إزالة العنصر من DOM (على سبيل المثال، يطلب المستخدم el.remove()).

مثال: إضافة تفاعلات عناصر مخصّصة إلى <app-drawer>:

class AppDrawer extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    // ...
  }

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    // ...
  }
}

حدِّد التفاعلات إذا كان ذلك منطقيًا. إذا كان العنصر معقّدًا بما يكفي ويفتح اتصالاً بقاعدة بيانات IndexedDB في connectedCallback()، عليك تنفيذ عملية التنظيف اللازمة في disconnectedCallback(). ولكن يُرجى توخّي الحذر. لا يمكنك الاعتماد على إزالة العنصر من DOM في جميع الحالات. على سبيل المثال، لن يتم استدعاء disconnectedCallback() مطلقًا إذا أغلق المستخدم علامة التبويب.

السمات والخصائص

ربط الخصائص بالسمات

من الشائع أن تعكس سمات HTML قيمتها في نموذج DOM كسمة HTML. على سبيل المثال، عند تغيير قيم hidden أو id في JS:

div.id = 'my-id';
div.hidden = true;

يتم تطبيق القيم على نموذج DOM المباشر كسمات:

<div id="my-id" hidden>

ويُعرف ذلك باسم عرض السمات على الخصائص. تُجري كل المواقع تقريبًا في HTML ذلك. لماذا؟ تكون السمات مفيدة أيضًا ل ضبط عنصر بشكل تعريفي، وتعتمد بعض واجهات برمجة التطبيقات، مثل أدوات اختيار CSS وأدوات اختيار تسهيل الاستخدام، على السمات للعمل.

يكون عرض السمة مفيدًا في أيّ مكان تريد فيه مزامنة تمثيل نموذج العناصر في المستند (DOM) مع حالة JavaScript. من بين الأسباب التي قد تدفعك إلى عكس خاصية هو تطبيق التصميم الذي يحدّده المستخدم عند تغيير حالة JavaScript.

نذكّرك بأنّنا تواصلنا معك في ‎<app-drawer>. قد يريد مستهلك هذا المكوّن إخفاءه تدريجيًا و/أو منع تفاعل المستخدم معه عندما يكون غير مفعّل:

app-drawer[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

عند تغيير السمة disabled في JavaScript، نريد أن تتم إضافة هذه السمة إلى DOM حتى يتطابق مع عنصر اختيار المستخدم. يمكن أن يقدّم العنصر هذا السلوك من خلال عرض القيمة لسمة تحمل الاسم نفسه:

get disabled() {
  return this.hasAttribute('disabled');
}

set disabled(val) {
  // Reflect the value of `disabled` as an attribute.
  if (val) {
    this.setAttribute('disabled', '');
  } else {
    this.removeAttribute('disabled');
  }
  this.toggleDrawer();
}

مراقبة التغييرات في السمات

سمات HTML هي طريقة ملائمة للمستخدمين لتحديد الحالة الأولية:

<app-drawer open disabled></app-drawer>

يمكن للعناصر التفاعل مع تغييرات السمات من خلال تحديد attributeChangedCallback. سيستدعي المتصفّح هذه الطريقة عند حدوث أي تغيير في السمات المدرَجة في صفيف observedAttributes.

class AppDrawer extends HTMLElement {
  // ...

  static get observedAttributes() {
    return ['disabled', 'open'];
  }

  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Only called for the disabled and open attributes due to observedAttributes
  attributeChangedCallback(name, oldValue, newValue) {
    // When the drawer is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the open attribute changing.
  }
}

في المثال، نضبط سمات إضافية على <app-drawer> عند تغيير سمة disabled. على الرغم من أنّنا لن نفعل ذلك هنا، يمكنك أيضًا استخدام attributeChangedCallback للحفاظ على مزامنة سمة JS مع ملف JS.

ترقيات العناصر

تحسين HTML بشكل تدريجي

سبق أن عرفنا أنّه يتمّ تحديد العناصر المخصّصة من خلال استدعاء customElements.define(). ولكن هذا لا يعني أنّك يجب أن تحدِّد عنصرًا مخصّصًا وتُسجِّله دفعة واحدة.

يمكن استخدام العناصر المخصّصة قبل تسجيل تعريفها.

التحسين التدريجي هو ميزة للعناصر المخصّصة. بعبارة أخرى، يمكنك تعريف مجموعة من عناصر <app-drawer> على الصفحة وعدم استدعاء customElements.define('app-drawer', ...) مطلقًا إلى وقت لاحق. ويعود سبب ذلك إلى أنّ المتصفّح يتعامل مع العناصر المخصّصة المحتمَلة بشكلٍ مختلف بفضل العلامات غير المعروفة. تُعرف عملية استدعاء define() ومنح عنصر حالي تعريف فئة باسم "ترقيات العناصر".

لمعرفة وقت تحديد اسم علامة، يمكنك استخدام window.customElements.whenDefined(). ويعرِض وعدًا يتم حلّه عندما يصبح العنصر محدّدًا.

customElements.whenDefined('app-drawer').then(() => {
  console.log('app-drawer defined');
});

مثال: تأخير العمل إلى أن تتم ترقية مجموعة من العناصر الفرعية

<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map((socialButton) => {
  return customElements.whenDefined(socialButton.localName);
});

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});

المحتوى الذي يحدّده العنصر

يمكن للعناصر المخصّصة إدارة محتواها باستخدام واجهات برمجة تطبيقات DOM داخل رمز العنصر. يمكنك استخدام التفاعلات لهذا الغرض.

مثال: أنشئ عنصرًا يتضمّن بعض علامات HTML التلقائية:

customElements.define('x-foo-with-markup', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  }
  // ...
});

سيؤدي تحديد هذه العلامة إلى إنشاء ما يلي:

<x-foo-with-markup>
  <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

// TODO: DevSite - تمّت إزالة نموذج الرمز البرمجي لأنّه كان يستخدم معالجات الأحداث المضمّنة

إنشاء عنصر يستخدم Shadow DOM

توفّر تقنية Shadow DOM طريقة للعنصر كي يمتلك قطعة من DOM منفصلة عن بقية الصفحة ويعرضها وينسّقها. يمكنك حتى إخفاء تطبيق كامل في علامة واحدة:

<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>

لاستخدام Shadow DOM في عنصر مخصّص، استخدِم this.attachShadow داخل constructor:

let tmpl = document.createElement('template');
tmpl.innerHTML = `
  <style>:host { ... }</style> <!-- look ma, scoped styles -->
  <b>I'm in shadow dom!</b>
  <slot></slot>
`;

customElements.define('x-foo-shadowdom', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to the element.
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(tmpl.content.cloneNode(true));
  }
  // ...
});

مثال على الاستخدام:

<x-foo-shadowdom>
  <p><b>User's</b> custom text</p>
</x-foo-shadowdom>

<!-- renders as -->
<x-foo-shadowdom>
  #shadow-root
  <b>I'm in shadow dom!</b>
  <slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>

النص المخصّص للمستخدم

// TODO: DevSite - تمّت إزالة نموذج الرمز البرمجي لأنّه كان يستخدم معالجات الأحداث المضمّنة

إنشاء عناصر من <template>

بالنسبة إلى غير المألوفين، يسمح لك عنصر<template> بتعريف أجزاء من نموذج DOM التي يتم تحليلها، وهي غير نشطة عند تحميل الصفحة، ويمكن تفعيلها لاحقًا أثناء التشغيل. وهي عنصر أساسي آخر لواجهة برمجة التطبيقات في عائلة مكونات الويب. النماذج هي عنصر نائب مثالي لتعريف بنية عنصر مخصّص.

مثال: تسجيل عنصر يتضمّن محتوى Shadow DOM تم إنشاؤه من <template>:

<template id="x-foo-from-template">
  <style>
    p { color: green; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p>
</template>

<script>
  let tmpl = document.querySelector('#x-foo-from-template');
  // If your code is inside of an HTML Import you'll need to change the above line to:
  // let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');

  customElements.define('x-foo-from-template', class extends HTMLElement {
    constructor() {
      super(); // always call super() first in the constructor.
      let shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(tmpl.content.cloneNode(true));
    }
    // ...
  });
</script>

هذه الأسطر القليلة من التعليمات البرمجية فعّالة جدًا. لنطّلِع على أهم التغييرات التي حدثت:

  1. سنحدّد عنصرًا جديدًا في HTML: <x-foo-from-template>
  2. يتم إنشاء Shadow DOM للعنصر من <template>
  3. يكون نموذج DOM للعنصر محليًا للعنصر بفضل Shadow DOM.
  4. يتمّ حصر نطاق ملف CSS الداخلي للعنصر في العنصر نفسه بفضل Shadow DOM.

أنا في Shadow DOM. تم وضع علامة على علامات الترميز من <template>.

// TODO: DevSite - تمّت إزالة نموذج الرمز البرمجي لأنّه كان يستخدم معالجات الأحداث المضمّنة

تصميم عنصر مخصّص

حتى إذا كان العنصر يحدّد أسلوبه الخاص باستخدام Shadow DOM، يمكن للمستخدمين تنسيق العنصر المخصّص من صفحتهم. وتُعرف هذه الأنماط باسم "الأنماط التي يحدّدها المستخدم".

<!-- user-defined styling -->
<style>
  app-drawer {
    display: flex;
  }
  panel-item {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  panel-item:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > panel-item {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-drawer>
  <panel-item>Do</panel-item>
  <panel-item>Re</panel-item>
  <panel-item>Mi</panel-item>
</app-drawer>

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

وضع تنسيق مُسبَق للعناصر غير المسجَّلة

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

مثال: إخفاء <app-drawer> قبل تحديدها:

app-drawer:not(:defined) {
  /* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
  display: inline-block;
  height: 100vh;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

بعد تحديد <app-drawer>، لن يتطابق المحدّد (app-drawer:not(:defined)) معه.

عناصر التمديد

إنّ واجهة برمجة التطبيقات Custom Elements API مفيدة لإنشاء عناصر HTML جديدة، ولكنها مفيدة أيضًا لتوسيع نطاق عناصر مخصّصة أخرى أو حتى HTML المضمّن في المتصفّح.

توسيع عنصر مخصّص

يتم توسيع عنصر مخصّص آخر من خلال توسيع تعريف فئته.

مثال: إنشاء <fancy-app-drawer> الذي يمتد إلى <app-drawer>:

class FancyDrawer extends AppDrawer {
  constructor() {
    super(); // always call super() first in the constructor. This also calls the extended class' constructor.
    // ...
  }

  toggleDrawer() {
    // Possibly different toggle implementation?
    // Use ES2015 if you need to call the parent method.
    // super.toggleDrawer()
  }

  anotherMethod() {
    // ...
  }
}

customElements.define('fancy-app-drawer', FancyDrawer);

توسيع عناصر HTML الأصلية

لنفترض أنّك أردت إنشاء <button> أكثر أناقة. بدلاً من تكرار سلوك <button> ووظائفه، من الأفضل تحسين العنصر الحالي باستخدام عناصر مخصّصة.

العنصر المضمّن المخصّص هو عنصر مخصّص يمدّد إحدى علامات HTML المضمّنة في المتصفّح. الفائدة الأساسية من توسيع عنصر حالي هي الحصول على جميع ميزاته (سمات DOM وطُرقه وإمكانية الوصول إليه). لا توجد طريقة أفضل لكتابة تطبيق ويب تقدّمي من تحسين عناصر HTML الحالية تدريجيًا.

لتوسيع عنصر، عليك إنشاء تعريف فئة يكتسب الخصائص من واجهة DOM الصحيحة. على سبيل المثال، يجب أن يكتسب العنصر المخصّص الذي يمتد <button> سمات من HTMLButtonElement بدلاً من HTMLElement. وبالمثل، يجب أن يمتد العنصر الذي يمتد إلى <img> إلى HTMLImageElement.

مثال - تمديد <button>:

// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
  constructor() {
    super(); // always call super() first in the constructor.
    this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));
  }

  // Material design ripple animation.
  drawRipple(x, y) {
    let div = document.createElement('div');
    div.classList.add('ripple');
    this.appendChild(div);
    div.style.top = `${y - div.clientHeight/2}px`;
    div.style.left = `${x - div.clientWidth/2}px`;
    div.style.backgroundColor = 'currentColor';
    div.classList.add('run');
    div.addEventListener('transitionend', (e) => div.remove());
  }
}

customElements.define('fancy-button', FancyButton, {extends: 'button'});

يُرجى ملاحظة أنّ طلب define() يتغيّر قليلاً عند توسيع عنصر أصلي. تُعلم المَعلمة الثالثة المطلوبة المتصفّح بالعلامة التي تتم إضافتها. وهذا ضروري لأنّ العديد من علامات HTML تتشارك واجهة DOM نفسها. تشترك كل من <section> و<address> و<em> (من بين غيرها) في HTMLElement، ويشترك كل من <q> و<blockquote> في HTMLQuoteElement، وما إلى ذلك. يُعلم تحديد {extends: 'blockquote'} المتصفّح بأنّك تنشئ <blockquote> محسّنًا بدلاً من <q>. اطّلِع على مواصفات HTML للحصول على القائمة الكاملة لواجهات نموذج DOM في HTML.

يمكن لمستخدِمي العنصر المضمّن المخصّص استخدامه بعدة طرق. ويمكنهم تحديدها من خلال إضافة سمة is="" إلى العلامة الأصلية:

<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>

أنشئ مثيلًا في JavaScript:

// Custom elements overload createElement() to support the is="" attribute.
let button = document.createElement('button', {is: 'fancy-button'});
button.textContent = 'Fancy button!';
button.disabled = true;
document.body.appendChild(button);

أو استخدِم عامل التشغيل new:

let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;

في ما يلي مثال آخر يمتد إلى <img>.

مثال - تمديد <img>:

customElements.define('bigger-img', class extends Image {
  // Give img default size if users don't specify.
  constructor(width=50, height=50) {
    super(width * 10, height * 10);
  }
}, {extends: 'img'});

يُعلِن المستخدمون عن هذا المكوّن على النحو التالي:

<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">

أو يمكنك إنشاء مثيل في JavaScript:

const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15, 20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);

تفاصيل متنوعة

العناصر غير المعروفة مقابل العناصر المخصّصة غير المحدّدة

لغة HTML مرنة وسهلة الاستخدام. على سبيل المثال، يمكنك استخدام العنصر <randomtagthatdoesntexist> في إحدى الصفحات وسيقبله المتصفح بكل سرور. لماذا تعمل العلامات غير العادية؟ الإجابة هي أنّ مواصفات HTML تسمح بذلك. يتم تحليل العناصر التي لم تحدّدها المواصفات على أنّها HTMLUnknownElement.

ولا ينطبق ذلك على العناصر المخصّصة. يتمّ تحليل العناصر المخصّصة المحتمَلة كعنصر HTMLElement إذا تمّ إنشاؤها باسم صالح (يتضمّن "-"). يمكنك التحقّق من ذلك في متصفّح يتيح استخدام العناصر المخصّصة. افتح وحدة التحكّم: Ctrl+Shift+J (أو Cmd+Opt+J على أجهزة Mac) والصق أسطر الرمز البرمجي التالية:

// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true

مرجع واجهة برمجة التطبيقات

تحدِّد السمة العامة customElements طرقًا مفيدة للعمل مع العناصر المخصّصة.

define(tagName, constructor, options)

يحدِّد عنصرًا مخصّصًا جديدًا في المتصفّح.

مثال

customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
    'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});

get(tagName)

عند توفّر اسم صالح لعلامة عنصر مخصّص، يتم عرض الدالة الإنشائية للعنصر. تعرِض القيمة undefined إذا لم يتم تسجيل تعريف عنصر.

مثال

let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();

whenDefined(tagName)

تعرِض وعدًا يتم حلّه عند تحديد العنصر المخصّص. إذا سبق تعريف العنصر، عليك حلّ المشكلة على الفور. يتم الرفض إذا لم يكن اسم العلامة اسم عنصر مخصّص صالحًا.

مثال

customElements.whenDefined('app-drawer').then(() => {
  console.log('ready!');
});

التوافق مع السجلّ والمتصفّح

إذا كنت تتابع مكونات الويب خلال العامين الماضيين، ستعرف أنّ الإصدار 36 من Chrome والإصدارات الأحدث نفّذت إصدارًا من واجهة برمجة التطبيقات Custom Elements API يستخدم document.registerElement() بدلاً من customElements.define(). يُعدّ هذا الإصدار الآن قديمًا من المعيار، ويُعرف باسم الإصدار 0. customElements.define() هو الميزة الجديدة الرائجة التي بدأ بائعو المتصفّحات في تطبيقها. يُطلق عليه الإصدار 1 من "العناصر المخصّصة".

إذا كنت مهتمًا بمواصفات الإصدار 0 القديم، يمكنك الاطّلاع على مقالة html5rocks.

دعم المتصفح

يتضمّن الإصدار 54 من Chrome (الحالة) وSafari 10.1 (الحالة) وFirefox 63 (الحالة) الإصدار 1 من Custom Elements. بدأ تطوير Edge.

لعرض ميزة رصد العناصر المخصّصة، تحقّق من توفّر window.customElements:

const supportsCustomElementsV1 = 'customElements' in window;

Polyfill

إلى أن تصبح مكونات العناصر المخصّصة متوافقة مع المتصفّحات على نطاق واسع، يتوفّر حزمة polyfill مستقلة لإصدار 1 من مكونات العناصر المخصّصة. ومع ذلك، ننصحك باستخدام webcomponents.js loader لتحميل مكونات الويب polyfills على النحو الأمثل. يستخدم أداة التحميل ميزة رصد العناصر لتحميل وحدات pollyfills الضرورية فقط بشكل غير متزامن والتي يطلبها المتصفّح.

تثبيت التطبيق:

npm install --save @webcomponents/webcomponentsjs

الاستخدام:

<!-- Use the custom element on the page. -->
<my-element></my-element>

<!-- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>

<!-- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
  function loadScript(src) {
    return new Promise(function(resolve, reject) {
      const script = document.createElement('script');
      script.src = src;
      script.onload = resolve;
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }

  WebComponents.waitFor(() => {
    // At this point we are guaranteed that all required polyfills have
    // loaded, and can use web components APIs.
    // Next, load element definitions that call `customElements.define`.
    // Note: returning a promise causes the custom elements
    // polyfill to wait until all definitions are loaded and then upgrade
    // the document in one batch, for better performance.
    return loadScript('my-element.js');
  });
</script>

الخاتمة

تمنحنا العناصر المخصّصة أداة جديدة لتحديد علامات HTML جديدة في المتصفّح و إنشاء مكوّنات قابلة لإعادة الاستخدام. وعندما نجمعها مع العناصر الأساسية الجديدة الأخرى للمنصة، مثل Shadow DOM و<template>، نبدأ في التعرّف على الصورة الكبيرة لـ Web Components:

  • متوافقة مع جميع المتصفّحات (معيار الويب) لإنشاء مكوّنات قابلة لإعادة الاستخدام وتوسيع نطاقها
  • لا تتطلّب أي مكتبة أو إطار عمل للبدء. Vanilla JS/HTML FTW!
  • يوفّر نموذج برمجة مألوفًا. إنّه مجرد DOM/CSS/HTML.
  • أن تعمل بشكل جيد مع ميزات أخرى جديدة لمنصّة الويب (Shadow DOM و<template> وسمات CSS المخصّصة وما إلى ذلك)
  • تم دمجها بإحكام مع أدوات مطوّري البرامج في المتصفّح.
  • الاستفادة من ميزات تسهيل الاستخدام الحالية