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

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

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

حدِّد التفاعلات إذا كان ذلك منطقيًا. إذا كان العنصر معقّدًا بما يكفي ويفتح اتصالاً بقاعدة بيانات IndexedDB في 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 للحفاظ على مزامنة سمة 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. ويُعدّ ذلك مفيدًا لضبط أسلوب مكوّن مسبقًا. على سبيل المثال، قد تريد منع تخطيط أو عنصر تحكم تفاعلي في المحتوى المرئي آخر من خلال إخفاء المكوّنات غير المحدّدة وإظهارها بشكل تدريجي عندما تصبح محدّدة.

مثال: إخفاء <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!');
});

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

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

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

دعم المتصفح

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

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

const supportsCustomElementsV1 = 'customElements' in window;

حشو بوليستر

إلى أن يصبح التوافق مع المتصفّحات متاحًا على نطاق واسع، يتوفّر برنامج polyfill مستقل لإصدار Custom Elements 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 المخصّصة وما إلى ذلك)
  • يمكن دمجه مع أدوات مطوّري البرامج في المتصفّح
  • استفِد من ميزات تسهيل الاستخدام الحالية.