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

مقدمة

لا يقدّم الويب تعبيرًا قويًا. لمعرفة ما أعنيه، يمكنك إلقاء نظرة على تطبيق ويب "حديث" مثل Gmail:

Gmail

لا شيء عصري في حساء <div>. ومع ذلك، هذه هي الطريقة التي ننشئ بها تطبيقات الويب. هذا أمر محزن. ألا يجب أن نطلب المزيد من منصتنا؟

ترميز مثير. لنبدأ

يوفّر لنا تنسيق HTML أداة ممتازة لتنظيم مستند، ولكنّ مفرداته تقتصر على العناصر التي يحدّدها معيار HTML.

ماذا لو لم تكن علامات ترميز Gmail سيئة؟ ماذا لو كان جميلًا:

<hangout-module>
    <hangout-chat from="Paul, Addy">
    <hangout-discussion>
        <hangout-message from="Paul" profile="profile.png"
            profile="118075919496626375791" datetime="2013-07-17T12:02">
        <p>Feelin' this Web Components thing.
        <p>Heard of it?
        </hangout-message>
    </hangout-discussion>
    </hangout-chat>
    <hangout-chat>...</hangout-chat>
</hangout-module>

كم هذا رائع. هذا التطبيق مناسب تمامًا أيضًا. يجب أن يكون مفيدًا وسهل الفهم، والأهم من ذلك، يجب أن يكون قابلاً للصيانة. ستعرف أنت أو أنا في المستقبل بالضبط ما يفعله فقط من خلال فحص العمود الفقري التعريفي.

الخطوات الأولى

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

  1. تحديد عناصر HTML/DOM جديدة
  2. إنشاء عناصر تمتد من عناصر أخرى
  3. تجميع الوظائف المخصّصة معًا بشكل منطقي في علامة واحدة
  4. توسيع نطاق واجهة برمجة التطبيقات لعناصر DOM الحالية

تسجيل عناصر جديدة

يتم إنشاء العناصر المخصّصة باستخدام document.registerElement():

var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());

الوسيطة الأولى لـ document.registerElement() هي اسم علامة العنصر. يجب أن يحتوي الاسم على شرطة (-). على سبيل المثال، <x-tags> و<my-element> و<my-awesome-app> هي أسماء صالحة، في حين أنّ <tabs> و<foo_bar> غير صالحَين. يسمح هذا القيد للمحلِّل بالتمييز بين العناصر المخصّصة والعناصر العادية، ولكنه يضمن أيضًا اتّساق المحتوى مع الإصدارات اللاحقة عند إضافة علامات جديدة إلى HTML.

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

ترث العناصر المخصّصة تلقائيًا من HTMLElement. وبالتالي، فإنّ المثال السابق يعادل:

var XFoo = document.registerElement('x-foo', {
    prototype: Object.create(HTMLElement.prototype)
});

تُعلِم عملية الاستدعاء document.registerElement('x-foo') المتصفّح بالعنصر الجديد، وتُعرِض عنصر إنشاء يمكنك استخدامه لإنشاء نُسخ من <x-foo>. بدلاً من ذلك، يمكنك استخدام تقنيات إنشاء العناصر الأخرى إذا كنت لا تريد استخدام المُنشئ.

توسيع العناصر

تتيح لك العناصر المخصّصة توسيع نطاق عناصر HTML الحالية (المدمجة) بالإضافة إلى غيرها من العناصر المخصّصة. لتوسيع عنصر، عليك تمرير registerElement() الاسم وprototype للعنصر الذي تريد اكتساب السمات منه.

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

لنفترض أنّك غير راضٍ عن "جو العادي" <button>. تريد تعزيز إمكاناته ليصبح "زرًا ضخمًا". لتوسيع عنصر <button>، أنشئ عنصرًا جديدًا يكتسب prototype عنصر HTMLButtonElement وextends اسم العنصر. في هذه الحالة، "button":

var MegaButton = document.registerElement('mega-button', {
    prototype: Object.create(HTMLButtonElement.prototype),
    extends: 'button'
});

تُعرف العناصر المخصّصة التي تكتسِب سمات من العناصر المدمجة باسم العناصر المخصّصة لتوسيع نطاق أنواع العناصر. ويتم اكتسابها من إصدار مخصّص من HTMLElement كطريقة للتعبير عن أنّ "العنصر X هو Y".

مثال:

<button is="mega-button">

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

لإنشاء عنصر <x-foo-extended> يُوسّع العنصر المخصّص <x-foo>، ما عليك سوى اكتساب النموذج الأولي له وتحديد العلامة التي يتم اكتسابها منها:

var XFooProto = Object.create(HTMLElement.prototype);
...

var XFooExtended = document.registerElement('x-foo-extended', {
    prototype: XFooProto,
    extends: 'x-foo'
});

اطّلِع على إضافة سمات JS وطُرقها أدناه للحصول على مزيد من المعلومات عن إنشاء نماذج أولية للعناصر.

كيفية ترقية العناصر

هل تساءلت يومًا عن سبب عدم اعتراض منظِّم HTML على العلامات غير العادية؟ على سبيل المثال، لا بأس إذا أعلنّا عن <randomtag> على الصفحة. وفقًا لمواصفات HTML:

عذرًا <randomtag>. إذا كنت غير عادي وتحصل على بيانات من HTMLUnknownElement

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

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

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

العناصر التي لم يتم حلّها

بما أنّ العناصر المخصّصة يتم تسجيلها من خلال نص برمجي باستخدام document.registerElement()، يمكن الإعلان عنها أو إنشاؤها قبل تسجيل تعريفها من خلال المتصفّح. على سبيل المثال، يمكنك الإعلان عن <x-tabs> في الصفحة ولكن ينتهي بك الأمر باستدعاء document.registerElement('x-tabs') بعد فترة طويلة.

قبل ترقية العناصر إلى تعريفها، تُعرف باسم العناصر غير المحسَّنة. هذه هي عناصر HTML التي تحتوي على اسم عنصر مخصّص صالح ولكن لم يتم تسجيلها.

قد يساعدك هذا الجدول في ترتيب الأمور:

الاسم اكتساب من أمثلة
عنصر لم يتم حلّه HTMLElement "<x-tabs>" و"<my-element>"
عنصر غير معروف HTMLUnknownElement "<tabs>" و"<foo_bar>"

إنشاء عناصر

لا تزال الأساليب الشائعة لإنشاء العناصر سارية على العناصر المخصّصة. وكما هو الحال مع أي عنصر عادي، يمكن تحديدها في HTML أو إنشاؤها في DOM باستخدام JavaScript.

إنشاء مثيل للعلامات المخصّصة

الإفصاح عنها:

<x-foo></x-foo>

إنشاء DOM في JS:

var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
    alert('Thanks!');
});

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

var xFoo = new XFoo();
document.body.appendChild(xFoo);

إنشاء عناصر إضافة النوع

إنّ إنشاء عناصر مخصّصة من النوع "إضافة" يشبه إلى حدّ كبير إنشاء العلامات المخصّصة.

الإفصاح عنها:

<!-- <button> "is a" mega button -->
<button is="mega-button">

إنشاء DOM في JS:

var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true

كما ترى، هناك الآن إصدار مُحمَّل بشكل زائد من document.createElement() يأخذ سمة is="" كمَعلمة ثانية.

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

var megaButton = new MegaButton();
document.body.appendChild(megaButton);

لقد تعلّمنا حتى الآن كيفية استخدام document.registerElement() لإعلام المتصفّح بعلامة جديدة، ولكن لا تؤدي هذه العلامة إلى الكثير. لنضيف السمات والطُرق.

إضافة سمات JS وأساليبها

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

في ما يلي مثال كامل:

var XFooProto = Object.create(HTMLElement.prototype);

// 1. Give x-foo a foo() method.
XFooProto.foo = function() {
    alert('foo() called');
};

// 2. Define a property read-only "bar".
Object.defineProperty(XFooProto, "bar", {value: 5});

// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});

// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');

// 5. Add it to the page.
document.body.appendChild(xfoo);

بالطبع، هناك آلاف الطرق لإنشاء prototype. إذا لم تكن من محبّي إنشاء النماذج الأولية بهذه الطريقة، إليك نسخة أكثر تكثيفًا من الخطوات نفسها:

var XFoo = document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    bar: {
      get: function () {
        return 5;
      }
    },
    foo: {
      value: function () {
        alert('foo() called');
      }
    }
  })
});

يسمح التنسيق الأول باستخدام ES5 Object.defineProperty. ويسمح الخيار الثاني باستخدام get/set.

طرق ردّ الاتصال الخاصة بمراحل النشاط

يمكن للعناصر تحديد طرق خاصة للاستفادة من الأوقات المناسبة لظهورها. وتُعرف هذه الطرق باسم وظائف الاستدعاء المتعلّقة بالدورة الحيوية. ولكلّ منها اسم وهدف محدّدَين:

اسم رقم هاتف معاودة الاتصال يتم استدعاؤه عندما
createdCallback يتم إنشاء مثيل للعنصر
attachedCallback تم إدراج مثيل في المستند
detachedCallback تمّت إزالة مثيل من المستند
attributeChangedCallback(attrName, oldVal, newVal) تمت إضافة سمة أو إزالتها أو تعديلها

مثال: تحديد createdCallback() وattachedCallback() في <x-foo>:

var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};

var XFoo = document.registerElement('x-foo', {prototype: proto});

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

من حالات الاستخدام الأخرى لعمليات الاستدعاء في دورة الحياة هي إعداد مستمعي الأحداث التلقائيين على العنصر:

proto.createdCallback = function() {
  this.addEventListener('click', function(e) {
    alert('Thanks!');
  });
};

إضافة ترميز

لقد أنشأنا <x-foo> وأضفنا إليها واجهة برمجة تطبيقات JavaScript، ولكنّها فارغة. هل نريد إرسال بعض صفحات HTML لعرضها؟

تكون وظائف الاستدعاء المتعلّقة بالدورة الحيوية مفيدة في هذه الحالة. على وجه الخصوص، يمكننا استخدام createdCallback() لمنح عنصر بعض علامات HTML التلقائية:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    this.innerHTML = "**I'm an x-foo-with-markup!**";
};

var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});

من المفترض أن يؤدي إنشاء مثيل لهذه العلامة وفحصها في DevTools (النقر بزر الماوس الأيمن واختيار "فحص العنصر") إلى عرض ما يلي:

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

تجميع العناصر الداخلية في Shadow DOM

يُعدّ Shadow DOM أداة فعّالة من تلقاء نفسه لتغليف المحتوى. استخدِم هذه الميزة مع العناصر المخصّصة للحصول على نتائج سحرية.

يوفّر Shadow DOM للعناصر المخصّصة ما يلي:

  1. طريقة لإخفاء أمعائها، وبالتالي حماية المستخدمين من تفاصيل التنفيذ الدموية
  2. تجميع الأنماط…مجانًا

إنّ إنشاء عنصر من Shadow DOM يشبه إنشاء عنصر يعرض علامات أساسية. يكمن الفرق في createdCallback():

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    // 1. Attach a shadow root on the element.
    var shadow = this.createShadowRoot();

    // 2. Fill it with markup goodness.
    shadow.innerHTML = "**I'm in the element's Shadow DOM!**";
};

var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});

بدلاً من ضبط .innerHTML للعنصر، أنشأت جذرًا للظل لعنصر <x-foo-shadowdom> ثم ملأته بعلامات الترميز. عند تفعيل الإعداد "عرض Shadow DOM" في DevTools، سيظهر لك رمز #shadow-root يمكن توسيعه:

<x-foo-shadowdom>
  #shadow-root
    **I'm in the element's Shadow DOM!**
</x-foo-shadowdom>

هذا هو جذر الظل.

إنشاء عناصر من نموذج

نماذج HTML هي عنصر أساسي جديد لواجهة برمجة التطبيقات يناسب بشكلٍ جيد عالم العناصر المخصّصة.

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

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

<script>
  var proto = Object.create(HTMLElement.prototype, {
    createdCallback: {
      value: function() {
        var t = document.querySelector('#sdtemplate');
        var clone = document.importNode(t.content, true);
        this.createShadowRoot().appendChild(clone);
      }
    }
  });
  document.registerElement('x-foo-from-template', {prototype: proto});
</script>

<template id="sdtemplate">
  <style>:host p { color: orange; }</style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template&gt;.
</template>

<div class="demoarea">
  <x-foo-from-template></x-foo-from-template>
</div>

تُحقّق هذه الأسطر القليلة من الرمز البرمجي تأثيرًا كبيرًا. لنفهم كل ما يحدث:

  1. لقد سجّلنا عنصرًا جديدًا في HTML: <x-foo-from-template>
  2. تم إنشاء عنصر DOM من <template>
  3. يتم إخفاء التفاصيل المخيفة للعنصر باستخدام Shadow DOM
  4. توفّر تقنية Shadow DOM ميزة "تجميع أنماط العناصر" (مثلاً، لا يؤدي p {color: orange;} إلى جعل الصفحة بأكملها برتقالية).

رائع.

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

كما هو الحال مع أي علامة HTML، يمكن لمستخدمي علامتك المخصّصة تصميمها باستخدام أدوات الاختيار:

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

<app-panel>
    <li is="x-item">Do</li>
    <li is="x-item">Re</li>
    <li is="x-item">Mi</li>
</app-panel>

تصميم العناصر التي تستخدم Shadow DOM

تصبح الأمور أكثر تعقيدًا كثيرًا عند استخدام Shadow DOM. تستفيد العناصر المخصّصة التي تستخدم Shadow DOM من مزاياه الرائعة.

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

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

منع عمليات التزوير في طلب المصادقة باستخدام :unresolved

للحدّ من FOUC، تحدّد العناصر المخصّصة فئة صورية جديدة في CSS، وهي :unresolved. استخدِم هذه الطريقة لاستهداف العناصر غير المحسَّنة، حتى تصل إلى النقطة التي يستدعي فيها المتصفّح createdCallback() (راجِع طرق دورة الحياة). بعد حدوث ذلك، لن يعود العنصر غير محدّد المصدر. اكتملت عملية الترقية وتحوّل العنصر إلى تعريفه.

مثال: تمويه علامات "x-foo" عند تسجيلها:

<style>
  x-foo {
    opacity: 1;
    transition: opacity 300ms;
  }
  x-foo:unresolved {
    opacity: 0;
  }
</style>

يُرجى العِلم أنّ :unresolved لا ينطبق إلا على العناصر غير المحسَّنة، وليس على العناصر التي تكتسِب سمات من HTMLUnknownElement (راجِع كيفية ترقية العناصر).

<style>
  /* apply a dashed border to all unresolved elements */
  :unresolved {
    border: 1px dashed red;
    display: inline-block;
  }
  /* x-panel's that are unresolved are red */
  x-panel:unresolved {
    color: red;
  }
  /* once the definition of x-panel is registered, it becomes green */
  x-panel {
    color: green;
    display: block;
    padding: 5px;
    display: block;
  }
</style>

<panel>
    I'm black because :unresolved doesn't apply to "panel".
    It's not a valid custom element name.
</panel>

<x-panel>I'm red because I match x-panel:unresolved.</x-panel>

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

رصد الميزات

إنّ رصد العناصر هو عملية التحقّق من توفّر document.registerElement():

function supportsCustomElements() {
    return 'registerElement' in document;
}

if (supportsCustomElements()) {
    // Good to go!
} else {
    // Use other libraries to create components.
}

دعم المتصفح

بدأ ظهور document.registerElement() لأول مرة في الإصدار 27 من Chrome والإصدار 23 تقريبًا من Firefox. ومع ذلك، تطورت المواصفات كثيرًا منذ ذلك الحين. الإصدار 31 من Chrome هو أول إصدار يقدّم دعمًا حقيقيًا للمواصفات المعدَّلة.

إلى أن يصبح توفّر المتصفّحات ممتازًا، هناك polyfill يستخدمه Polymer من Google وX-Tag من Mozilla.

ماذا حدث لعنصر HTMLElementElement؟

بالنسبة إلى المستخدمين الذين تابعوا عملنا على التنسيق، سيعرفون أنّه كان هناك <element>. لقد كان رائعًا. يمكنك استخدامه لتسجيل عناصر جديدة بشكل تعريفي:

<element name="my-element">
    ...
</element>

للأسف، حدثت الكثير من المشاكل المتعلقة بالتوقيت في عملية الترقية، وحالات استثنائية، وسيناريوهات معقدة للغاية، ما أدى إلى تأخير حلّ جميع المشاكل. تمّ إيقاف <element> نهائيًا. في آب (أغسطس) 2013، نشر ديمتري جلاسكوف على public-webapps إعلانًا عن إزالته، على الأقل في الوقت الحالي.

تجدر الإشارة إلى أنّ Polymer تنفِّذ نموذجًا توضيحيًا لتسجيل العناصر باستخدام <polymer-element>. الطريقة ويستخدم document.registerElement('polymer-element') و التقنيات التي وصفتها في مقالة إنشاء عناصر من نموذج.

الخاتمة

تمنحنا العناصر المخصّصة أداة لتوسيع مفردات HTML وتعليمها حيلًا جديدة، والانتقال عبر ثقوب دودية في منصة الويب. ونجمعها مع العناصر الأساسية الجديدة الأخرى للمنصة، مثل Shadow DOM و<template>، ونبدأ في تنفيذ صورة Web Components. يمكن أن تصبح ميزة "الترميز" جذابة مرة أخرى.

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