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

مقدمة

يفتقر الويب إلى التعبير بشدة. لمعرفة ما أعنيه، ألقِ نظرة خاطفة على تطبيق ويب "حديث" مثل 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'
});

راجع إضافة خصائص JavaScript وطرقها أدناه لمزيد من المعلومات حول إنشاء النماذج الأولية للعناصر.

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

هل تساءلت يومًا عن سبب عدم ملاءمة محلل 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) في JavaScript:

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) في JavaScript:

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() لإعلام المتصفّح بعلامة جديدة، ولكنّه لا يقدّم الكثير. لنقم بإضافة الخصائص والطرق.

إضافة خصائص JavaScript وطرقها

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

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

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});

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

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

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

إضافة ترميز

لقد أنشأنا واجهة برمجة التطبيقات <x-foo>، ولكنها فارغة. هل يجب أن نعطيه بعض 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});

من المفترض أن يظهر إجراء إنشاء فوري لهذه العلامة وفحصها في "أدوات مطوري البرامج" (انقر بزر الماوس الأيمن واختَر "فحص العنصر") ما يلي:

▾<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 للعنصر، أنشأت Shadow Root لـ <x-foo-shadowdom> ثم ملأته بالترميز. عند تفعيل إعداد "Show Shadow DOM" في "أدوات مطوّري البرامج"، سيظهر #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 هو موضوع ضخم. إذا كنت ترغب في معرفة المزيد حوله، أوصيك ببعض مقالاتي الأخرى:

منع FOUC باستخدام :لم يتم حلها

للحدّ من تأثير 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 وFirefox 23 تقريبًا. ومع ذلك، فقد تطورت المواصفات كثيرًا منذ ذلك الحين. Chrome 31 هو أول إصدار متوافق مع المواصفات المعدّلة.

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

ماذا حدث لـ HTMLElementElement؟

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

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

لسوء الحظ، كان هناك الكثير من مشكلات التوقيت في عملية الترقية، والحالات الزاوية، والسيناريوهات المشابهة لـ Armageddon، وحل المشكلة. كان لا بد من وضع "<element>" على الرف. في آب (أغسطس) 2013، نشر ديميتري غلازكوف على موقع public-webapps أعلن فيه عن إزالته في الوقت الحالي على الأقل.

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

الخلاصة

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

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