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

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

باستخدام العناصر المخصّصة، يمكن لمطوّري البرامج على الويب إنشاء علامات HTML جديدة أو تحسين علامات HTML الحالية أو توسيع المكوّنات التي ألّفها مطوِّرون آخرون. واجهة برمجة التطبيقات هي أساس مكونات الويب. وهو يوفر طريقة تستند إلى معايير الويب لإنشاء مكونات قابلة لإعادة الاستخدام باستخدام لا شيء سوى Vanilla 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 يتم إنشاء مثيل للعنصر أو ترقيته. ويكون مفيدًا لإعداد الحالة أو إعداد أدوات معالجة الأحداث أو إنشاء نطاق ظل. راجِع المواصفات للتعرّف على القيود المفروضة على ما يمكنك فعله في 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) {
    // ...
  }
}

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

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

انعكاس الخصائص للسمات

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

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 للحفاظ على مزامنة سمة JavaScript مع سمةها.

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

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(). يعد هذا الآن إصدارًا متوقفًا من المعيار يسمى v0. customElements.define() هو الميزات الجديدة كل ذلك وما بدأ مورّدو المتصفحات في تنفيذه. ويُطلق عليه اسم الإصدار 1 من العناصر المخصصة.

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

المتصفحات المتوافقة

تحتوي Chrome 54 (الحالة) وSafari 10.1 (الحالة) وFirefox 63 (الحالة) على عناصر مخصصة v1. بدأ Edge التطوير.

لكي ترصد الميزة العناصر المخصّصة، تأكَّد من توفّر window.customElements:

const supportsCustomElementsV1 = 'customElements' in window;

الملء التلقائي

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

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

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>، بدأنا ندرك الصورة الكبيرة لمكونات الويب:

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