You are on page 1of 15

‫تنص القاعدة الشهيرة في تطوير البرمجيات المعقدة على أن ال تط ّور برمجيات معقّدة‪ ،‬فإذا وصل بك‬

‫األمر إلى أمر معقد‪ ،‬فافصله إلى أجزاء أبسط واربطها ببعضها بأكثر الطرق وضو ًحا‪ ،‬فالمعماري‬
‫األمهر هو من يستطيع تحويل المعقد إلى بسيط‪.‬‬

‫يمكن تقسيم‪ ‬واجهة المستخدم‪ ‬إلى مك ّونات مرئية لكلٍّ منها موقعه في الصفحة‪ ،‬وتُنفِّذ مهمةً محددةً بعناية‬
‫وتكون مفصولةً عن بعضها البعض‪ ،‬لنلق نظرةً على‪ ‬موقع ويب‪ ،‬وليكن تويتر مثاًل ‪ ،‬يُقسم الموقع إلى‬
‫مكوّنات‪:‬‬

‫مكوّن للتنقل‪.‬‬ ‫‪.1‬‬


‫معلومات المستخدم‪.‬‬ ‫‪.2‬‬
‫اقتراحات المتابعة‪.‬‬ ‫‪.3‬‬
‫نموذج إرسال‪.‬‬ ‫‪.4‬‬
‫رسائل‪.‬‬ ‫‪.5‬‬
‫غيرها من المك ِّونات‪.‬‬ ‫‪.6‬‬

‫ضا‪ ،‬فقد تكون الرسائل مك ّونات فرعيةً من مكوّن أعلى مستو ً‬


‫ى مثل‬ ‫قد يكون للمكوّنات مك ّونات فرعيةً أي ً‬
‫قائمة الرسائل‪ ،‬كما قد تكون صورة المستخدم التي تقبل النقر مك ّونًا‪ ..‬وهكذا‪.‬‬

‫المكون إ ًذا؟ يأتي ذلك من حسن البديهة والخبرة والمنطق‪ ،‬حيث تش ًكل المك ّونات عادةً‬
‫ِّ‬ ‫كيف نحدد ما هو‬
‫كيانات مرئيةً نميزها وفقًا لوظيفتها وكيفية تتفاعلها مع الصفحة‪ ،‬ففي حالة الصفحة السابقة‪ ،‬سنجد أنها‬
‫تحوي وحدات يلعب كل منها دوره الخاص‪ ،‬ومن المنطق إ ًذا أن نجعلها مكوّنات‪.‬‬

‫للمكون‪:‬‬
‫ِّ‬

‫صنف ‪ JavaScript‬خاص به‪.‬‬ ‫‪‬‬


‫بنية ‪ DOM‬تُدار منفردةً من قبل الصنف الخاص بها‪ ،‬وال يمكن الوصول إليها من قبل شيفرة‬ ‫‪‬‬
‫خارجية (مبدأ التغليف)‪.‬‬
‫تنسيق ‪ CSS‬يُطبَّق على المكوّن‪.‬‬ ‫‪‬‬
‫واجهة برمجية‪ ،API ‬أحداث وتوابع يستخدمها الصنف وغيره من الكائنات التي تتفاعل مع‬ ‫‪‬‬
‫المكوّنات األخرى‪.‬‬

‫تذكر ّ‬
‫أن فكرة الكائن ككل ال تفرض وجود أي شيء خاص‪.‬‬

‫تتواجد الكثير من المنصات وإطارات العمل التي تقدم وسائل إلنشاء المك ّونات‪ ،‬وتستخدم أصناف ‪CSS‬‬
‫باإلضافة إلى ترتيبات أخرى لتعزيز المك ّونات وتحديد مجال تطبيق ‪ CSS‬وتغليف ‪.DOM‬‬

‫مكونات الويب بإمكانيات تصفّح مدمجةً معها وال حاجة لمحاولة تقليدها بعد اآلن‪.‬‬
‫تزوّدنا ِّ‬
‫عناصر مخصصة‪ :Custom elements ‬لتعريف‪ ‬عناصر ‪ HTML‬مخصصة‪.‬‬ ‫‪‬‬
‫شجرة ‪ DOM‬مخفية‪ :Shadow DOM ‬إلنشاء شجرة داخلية للمكوّن مخفية عن بقية‬ ‫‪‬‬
‫المكوّنات‪.‬‬
‫مجال لتطبيق أوراق التنسيق‪ :CSS Scoping ‬لتعريف قواعد تنسيق يمكن تطبيقها على‬ ‫‪‬‬
‫الشجرة الداخلية لمكوّن‪.‬‬
‫إعادة توجيه الحدث‪ Event retargeting ‬وغيره من األمور الثانوية‪ :‬لتحسين قدرة المكوّنات‬ ‫‪‬‬
‫على التالؤم مع عملية التطوير‪.‬‬

‫كل الميزات المدعومة جيدًا لمك ّونات الويب جيدة في األمور التي تقدر على تنفيذها‪.‬‬

‫عناصر ‪ HTML‬المخصصة‬

‫يمكننا إنشاء عناصر ‪ HTML‬مخصصة يصفها الصنف الذي ننشئه لهذا الغرض مز ّودًا بتوابعه‬
‫وخصائصه وأحداثه الخاصة‪ ،‬وسنتمكن من استخدام العنصر ضمن عناصر ‪ HTML‬األصلية بعد أن‬
‫نع ّرفه‪ ،‬وهذا أمر جيد‪ ،‬فعلى الرغم من غنى ‪ HTML‬بالعناصر‪ ،‬إال أنها ال تحوي عناصرًا مثل عالمة‬
‫جداول سهلة التعامل‪ >easy-tabs< ‬أو سير دائري منزلق‪ >sliding-carousel< ‬أو‬
‫الرفع بطريقة جذابة‪ >beautiful-upload< ‬أو أي عناصر أخرى قد نحتاجها‪.‬‬

‫يمكن تعريف العناصر المخصصة ضمن أصناف خاصة‪ ،‬واستخدامها كما لو أنها جزء من ‪،HTML‬‬
‫ويوجد نوعان من هذه العناصر المخصصة‪ ،‬وهما‪:‬‬

‫عناصر مخصصة ذاتية التصرف ‪ :Autonomous‬وهي عناصر جديدة توّسع عمل الصنف‬ ‫‪.1‬‬
‫المجرّد‪.HTMLElement ‬‬
‫عناصر أصلية مخصصة ‪ :customized‬توسّع العناصر األصلية‪ ،‬إلنجاز أزرار مخصصة‬ ‫‪.2‬‬
‫مبنية على أساس الصنف‪ HTMLButtonElement ‬مثاًل ‪.‬‬

‫سنغطي أواًل العناصر ذاتية التصرف‪ ،‬ثم سننتقل إلى العناصر المخصصة‪ ،‬وإلنشاء عنصر مخصص ال‬
‫ب ّد من إخبار المتصفح عن جملة من التفاصيل حوله‪ ،‬مثل كيف سيُظهره؟ وماذا سيفعل عند إضافته أو‬
‫إزالته من الصفحة؟ يجري ذلك بإنشاء صنف له توابعه الخاصة‪ ،‬وسيكون األمر بسيطًا‪ ،‬فهي عدة توابع‬
‫فقط وجميعها اختيارية‪.‬‬

‫إليك تصو ًرا مع قائمة كاملة من التوابع‪:‬‬

‫‪{ class MyElement extends HTMLElement‬‬


‫‪{ )(constructor‬‬
‫‪;)(super‬‬
‫‪ //‬إنشاء العنصر‬
‫}‬
‫‪{ )(connectedCallback‬‬
‫‪//‬يستدعيه المتصفح عند إضافة عنصر ويمكن استدعاؤه عدة مرات إن‬
‫أضيف أو حذف العنصر‬

‫}‬

‫‪{ )(disconnectedCallback‬‬
‫‪//‬يستدعيه المتصفح عند حذف عنصر ويمكن استدعاؤه عدة مرات إن أضيف‬
‫أو حذف العنصر‬

‫}‬

‫‪{ )(static get observedAttributes‬‬


‫‪ */[ return‬مصفوفة من أسماء السمات التي تراقب التغيرات *‪;]/‬‬
‫}‬

‫)‪{ attributeChangedCallback(name, oldValue, newValue‬‬


‫يستدعى عندما تتغير صفة من السمات المذكوره أعاله‬
‫‪ُ //‬‬
‫}‬

‫‪{ )(adoptedCallback‬‬
‫ينقل العنصر إلى صفحة أخرى‬
‫يستدعى عندما ُ‬
‫‪ُ //‬‬

‫}‬

‫‪ //‬إن كان هناك عناصر أو سمات أخرى‬


‫}‬

‫نحتاج بعد ذلك إلى تسجيل العنصر‪:‬‬

‫دم من قبل الصنف الجديد <‪ >my-element‬أبلغ المتصفح أن‬


‫ُخّ‬
‫‪ //‬سي‬
‫)‪;customElements.define("my-element", MyElement‬‬

‫ستتولد اآلن نسخة عن الصنف‪ MyElement ‬لكل عنصر ‪ HTML‬له الوسم‪my-< ‬‬


‫‪ ، >element‬وستستدعى التوابع المذكورة‪ .‬كما يمكن تنفيذ ذلك باستخدام شيفرة ‪JavaScript‬‬
‫كالتالي‪:‬‬
document.createElement('my-element')

‫و‬ my-element ‫ فبعض األسماء مثل‬،"-" ‫يجب أن يحتوي اسم العنصر المخصص على الواصلة‬
‫ وذلك لمنع‬،‫مرفوض‬ myelement ‫ لكن االسم‬،‫ هي أسماء مقبولة‬،super-button
.‫ األصلية‬HTML ‫التضارب مع عناصر‬

‫مثال عنصر لتنسيق الوقت‬

‫ لكنه ال يقدم أي تنسيق للبيانات التي‬،‫ إلظهار الوقت والتاريخ‬ >time< ‫يُستخدم العنصر األصلي‬
‫الذي يعرض الوقت بتنسيق جميل يأخذ اللغة‬ >time-formatted< ‫ لننشئ إ ًذا العنصر‬،‫يعطيها‬
:‫بالحسبان‬

>script<
class TimeFormatted extends HTMLElement { // (1)

{ )(connectedCallback
let date = new Date(this.getAttribute('datetime') ||
;Date.now())

{ ,"this.innerHTML = new Intl.DateTimeFormat("default


,year: this.getAttribute('year') || undefined
,month: this.getAttribute('month') || undefined
,day: this.getAttribute('day') || undefined
,hour: this.getAttribute('hour') || undefined
,minute: this.getAttribute('minute') || undefined
,second: this.getAttribute('second') || undefined
timeZoneName: this.getAttribute('time-zone-name') ||
,undefined
;format(date).)}
}

customElements.define("time-formatted", TimeFormatted); // (2)


>script/<

>-- )3( --!<


‫<‪"time-formatted datetime="2019-12-01‬‬
‫‪"year="numeric" month="long" day="numeric‬‬
‫‪"hour="numeric" minute="numeric" second="numeric‬‬
‫‪"time-zone-name="short‬‬
‫><‪>time-formatted/‬‬

‫تشير (‪ )1‬إلى الصنف تابع واحد فقط هو‪ ،connectedCallback)( ‬حيث يستدعيه‬ ‫‪.1‬‬
‫المتصفح عندما يُضاف العنصر إلى الصفحة‪ ،‬أو عندما يكتشف مفسّر ‪ HTML‬وجوده‪،‬‬
‫ويستخدم العنصر تابع تنسيق البيانات المدمج‪ Intl.DateTimeFormat ‬الذي يلقى دع ًما جيدًا‬
‫من معظم المتصفحات لعرض الوقت بتنسيق جميل‪.‬‬
‫تعني (‪ )2‬أنه ال ب ّد من تسجيل العنصر الجديد باستخدام األمر‪( ‬‬ ‫‪.2‬‬
‫)‪.customElements.define(tag, class‬‬
‫تعني (‪ )3‬أنه يمكن اآلن استخدام العنصر في أي مكان‪.‬‬ ‫‪.3‬‬

‫تحديث العنصر المخصص‬

‫إذا صادف المتصفح العنصر‪ >time-formatted< ‬قبل عبارة‬


‫التعريف‪ customElements.define ‬فلن يع ّد ذلك خطًأ‪ ،‬بل سيع ّده مجرد عنصر غير‬
‫معروف مثل أي وسم غير معياري‪ ،‬ويمكن تنسيق هذه العناصر غير المعروفة باستخدام المحدد‪( ‬‬
‫‪ ،:not(:defined‬وهو أحد‪ ‬محددات ‪ .CSS‬ستُح ّدث هذه العناصر عندما يُستدعى‬
‫التابع‪ ،customElement.define ‬وستتولد نسخة من الصنف‪ TimeFormatted ‬لك ٍّل‬
‫منها‪ ،‬ثم يُستدعى التابع‪ connectedCallback ‬وتتحول إلى عناصر معرّفة‪.:defined ‬‬

‫توجد توابع عدة للحصول على معلومات عن عناصر مخصصة‪:‬‬

‫(‪ :customElements.get(name‬يعيد الصنف الخاص بالعنصر الذي يحمل‬ ‫‪‬‬


‫االسم المحدد‪.name ‬‬
‫(‪ :customElements.whenDefined(name‬يعيد وعدًا ‪ promise‬ي َح َّل ‪-‬‬ ‫‪‬‬
‫دون قيمة‪ -‬عندما يُعرَّف العنصر المخصص الذي له االسم‪ name ‬المحدد‪.‬‬

‫التصيير داخل ‪ connectedCallback‬ال في ‪constructor‬‬

‫رأينا في المثال السابق كيف جرى تصيير محتوى العنصر ضمن‬


‫التابع‪ ،connectedCallback ‬لكن لماذا لم يُصيّر ضمن الدالة البانية‪constructor ‬؟‬

‫ّ‬
‫إن السبب بسيط‪ ،‬ألنه من المبكر جدًا تصيير المك ّون عند استدعاء‪ ،constructor ‬فعندما يُنشأ‬
‫العنصر في هذه المرحلة‪ ،‬فلن يُعالجه المتصفح أو يسند إليه السمات الخاصة به‪،‬‬
‫فاستدعاء‪ getAttribute ‬سيعيد القيمة "الشيء"‪ ،null ‬وبالتالي لن نتمكن من تصيير شيء‪،‬‬
‫ولو فكرنا باألمر قلياًل فسنجد ّ‬
‫أن هذا األداء أفضل لتأخير العمل حتى يكون كل شيء جاه ًزا‪.‬‬
‫يقع الحدث‪ connectedCallback ‬عندما يُضاف العنصر إلى الصفحة‪ ،‬ولن يُضاف إلى‬
‫عنصر آخر مثل ابن فقط‪ ،‬بل سيغدو عمليًا جز ًءا من الصفحة‪ ،‬وهكذا سنتمكن من بناء‪ ‬شجرة‬
‫‪ DOM‬منفصلة‪ ،‬وإنشاء عناصر وتحضيرها للعمل الحقًا‪ ،‬وستصيّر هذه العناصر فعليًا عندما تُضاف‬
‫إلى الصفحة‪.‬‬

‫مراقبة السمات‬

‫ليس لتغير السمات ‪ attribute‬في الطريقة الحالية لتنفيذ العنصر‪ >time-formatted< ‬أي‬


‫تأثير بعد تصييره‪ ،‬وهذا غريب بالنسبة إلى عنصر ‪ ،HTML‬فعندما نغيّر في صفة مثل‪،a.href ‬‬
‫فسنتوقع عادةً أن نرى التغير مباشرةً‪ .‬نصلح هذا األمر إ ًذا‪.‬‬

‫يمكن مراقبة السمات بتزويد دالة اإلحضار الساكنة‪ observedAttributes)( ‬بقائمة تضم‬


‫أسماءهم‪ ،‬وهكذا ستُستدعى الدالة‪ attributeChangedCallback ‬عندما نجري أي‬
‫تعديالت على تلك السمات‪ .‬لن يقع هذا الحدث بالطبع عندما تتغير سمات غير مدرجة في القائمة‪،‬‬
‫ألسباب تتعلق باألداء‪.‬‬

‫إليك شيفرة العنصر‪ >time-formatted< ‬الذي يُح َّدث تلقائيًا عندما تتغير سماته‪:‬‬

‫<‪>script‬‬
‫‪{ class TimeFormatted extends HTMLElement‬‬

‫)‪render() { // (1‬‬
‫|| )'‪let date = new Date(this.getAttribute('datetime‬‬
‫))(‪;Date.now‬‬

‫‪{ ,"this.innerHTML = new Intl.DateTimeFormat("default‬‬


‫‪,year: this.getAttribute('year') || undefined‬‬
‫‪,month: this.getAttribute('month') || undefined‬‬
‫‪,day: this.getAttribute('day') || undefined‬‬
‫‪,hour: this.getAttribute('hour') || undefined‬‬
‫‪,minute: this.getAttribute('minute') || undefined‬‬
‫‪,second: this.getAttribute('second') || undefined‬‬
‫|| )'‪timeZoneName: this.getAttribute('time-zone-name‬‬
‫‪,undefined‬‬
‫})‪;format(date).‬‬
‫}‬
connectedCallback() { // (2)
{ if (!this.rendered)
;)(this.render
;this.rendered = true
}
}

static get observedAttributes() { // (3)


return ['datetime', 'year', 'month', 'day', 'hour', 'minute',
;'second', 'time-zone-name']
}

attributeChangedCallback(name, oldValue, newValue) { // (4)


;)(this.render
}

;customElements.define("time-formatted", TimeFormatted)
>script/<

time-formatted id="elem" hour="numeric" minute="numeric"<


>second="numeric"></time-formatted

>script<
setInterval(() => elem.setAttribute('datetime', new Date()),
1000); // (5)
>script/<

.>time-formatted< ‫) أنه قد نُقل منطق التصيير إلى التابع المساعد‬1( ‫ُقصد بـ‬ َ ‫ي‬ .1
.‫) أن هذا التابع يُستدعى مباشرةً في اللحظة التي يضاف فيها العنصر إلى الصفحة‬2( ‫تعني‬ .2
‫عند حدوث أي تغير‬ attributeChangedCallback ‫) أنه يقع الحدث‬3( ‫تعني‬ .3
observedAttributes)( ‫في السمات التي مررت على شكل قائمة إلى‬
.‫) أنه يُعاد تصيير العنصر بعد ذلك‬4( ‫تعني‬ .4
.‫) أنه في النهاية يمكننا إنشاء مؤقت مباشر بكل سهولة‬5( ‫تعني‬ .5

‫تسلسل التصيير‬
‫عندما يبني مفسِّر ‪ HTML‬الشجرة ‪ ،DOM‬تعالَج العناصر الواحد تلو اآلخر واألب قبل االبن‪ ،‬فلو كان‬
‫لدينا العنصر‪:‬‬

‫<‪>outer><inner></inner></outer‬‬

‫فسيتولد العنصر‪ >outer< ‬أواًل ويضاف إلى ‪ ،DOM‬ثم العنصر‪ ،>inner< ‬وسيقود ذلك إلى‬


‫تبعات بالنسبة للعناصر المخصصة‪ ،‬فإذا أراد العنصر المخصص الوصول‬
‫إلى‪ innerHTML ‬ضمن‪ connectedCallback ‬مثاًل ‪ ،‬فلن يحصل على شيء‪.‬‬

‫<‪>script‬‬
‫‪{ customElements.define('user-info', class extends HTMLElement‬‬

‫‪{ )(connectedCallback‬‬
‫‪)*( alert(this.innerHTML); // empty‬‬
‫}‬

‫});‬
‫<‪>script/‬‬

‫<‪>user-info‬محمد<‪>user-info/‬‬

‫إذا نفّذت الشيفرة السابقة فستظهر الرسالة‪ alert ‬فارغة‪ ،‬والسبب في ذلك هو عدم وجود عناصر‬
‫أبناء في هذه المرحلة‪ ،‬وستكون الشجرة ‪ DOM‬غير مكتملة‪ ،‬يربط مفسِّر ‪ HTML‬العنصر‬
‫المخصص‪ >user-info< ‬وسيستأنف العمل بعدها لربط أبنائه لكنه لم يفعلها بعد‪.‬‬

‫إذا أردنا تمرير معلومات إلى عنصر مخصص‪ ،‬فيمكننا استخدام السمات فهي متاحة مباشرةً‪ ،‬وإذا كنا‬
‫بحاجة فعاًل إلى األبناء‪ ،‬فيمكن تأجيل الوصول إليهم باستخدام الحدث‪setTimeout ‬دون تأخير‬
‫زمني ‪.Zero-delay‬‬

‫<‪>script‬‬
‫‪{ customElements.define('user-info', class extends HTMLElement‬‬

‫‪{ )(connectedCallback‬‬
‫))‪ // ;setTimeout(() => alert(this.innerHTML‬محمد (*)‬
‫}‬
‫});‬
‫<‪>script/‬‬

‫<‪>user-info>John</user-info‬‬

‫وهكذا ستُظهر الرسالة‪  alert ‬في السطر (*) الكلمة "محمد" طالما أننا ننفذ األمر دون تزامن‪ ،‬وذلك‬
‫بعد اكتمال تفسير شيفرة ‪ ،HTML‬ويمكن معالجة األبناء إذا اقتضى األمر ثم إنهاء مرحلة التهيئة‪ ،‬لكن‬
‫هذا الحل ليس مثاليًا‪.‬‬

‫فإذا استخدمت العناصر المخصصة الحدث لتهيئة نفسها‪ ،‬فإنها ستصطف بحيث يقع‬
‫الحدث‪  setTimeout ‬للعنصر الخارجي ثم الداخلي‪ ،‬وبالتالي ستنهي العناصر الخارجية تهيئة‬
‫نفسها قبل الداخلية‪ ،‬لنشرح ذلك من خالل المثال التالي‪:‬‬

‫<‪>script‬‬
‫‪{ customElements.define('user-info', class extends HTMLElement‬‬
‫‪{ )(connectedCallback‬‬
‫)`‪;alert(`${this.id} connected.‬‬
‫))`‪;setTimeout(() => alert(`${this.id} initialized.‬‬
‫}‬
‫});‬
‫<‪>script/‬‬

‫<‪>"user-info id="outer‬‬
‫<‪>user-info id="inner"></user-info‬‬
‫<‪>user-info/‬‬

‫تسلسل الخرج‪:‬‬

‫يُربط العنصر الخارجي بالشجرة‪.‬‬ ‫‪.1‬‬


‫يُربط العنصر الداخلي‪.‬‬ ‫‪.2‬‬
‫يُهيأ العنصر الخارجي‪.‬‬ ‫‪.3‬‬
‫يُهيأ العنصر الداخلي‪.‬‬ ‫‪.4‬‬

‫يمكننا أن نرى بوضوح أن العنصر الخارجي سينهي تهيئة نفسه (الخطوة ‪ )3‬قبل الداخلي (الخطوة ‪،)4‬‬
‫وال يوجد استدعاء مدمج يمكن أن يقع بعد أن يصبح العنصر ضمن الشجرة‪ ،‬كما يمكن تنفيذ استدعاء‬
‫مثل هذا عند الحاجة بأنفسنا‪ ،‬حيث يمكن للعناصر الداخلية إرسال أحداث‬
‫مثل‪ initialized ‬تستمع إليها العناصر الخارجية وتتفاعل معها‪.‬‬
‫العناصر األصلية المخصصة‬

‫ليس للعناصر المخصصة ‪-‬مثل‪ ->time-formatted< ‬أي دالالت تتعلق بها‪ ،‬فهي غير‬
‫معروفة لمحركات البحث مثاًل ‪ ،‬وال تستطيع األجهزة التي تدعم مبدأ الوصول السهل ‪accessibility‬‬
‫التعامل معها‪ .‬قد تكون هذه األشياء مهمةً‪ ،‬فقد يرغب مح ّرك البحث بمعرفة أن هذا العنصر سيظهر‬
‫ص ا من األزرار‪ ،‬فلماذا ال يمكننا إعادة استخدام وظيفة الزر األصلي‪< ‬‬
‫الوقت‪ ،‬فإذا أنشأنا نوعًا خا ً‬
‫‪>button‬؟‬

‫يمكننا توسيع وتخصيص العناصر األصلية بالوراثة من أصنافها‪ ،‬فاألزرار مثاًل هي نسخ عن‬
‫الصنف‪ ،HTMLButtonElement ‬وإلنشاء عنصر يرث منه ويوسّعه عليك باتباع اآلتي‪:‬‬

‫وسِّع الصنف‪ HTMLButtonElement ‬عن طريق الصنف الذي سننشئه‪:‬‬ ‫‪.1‬‬

‫‪class HelloButton extends HTMLButtonElement { /* custom element‬‬


‫} ‪methods */‬‬

‫أعط قيمةً للوسيط الثالث للدالة‪ customElements.define ‬والذي يمثّل الوسم ‪tag‬‬ ‫‪.1‬‬


‫المستهدف‪:‬‬

‫‪customElements.define('hello-button', HelloButton, {extends:‬‬


‫)}'‪;'button‬‬

‫يمكن أن تشترك وسوم مختلفة بصنف ‪ ،DOM‬لذلك سنحتاج إلى استخدام التعليمة‪.extends ‬‬

‫لكي نستخدم أخي ًرا العنصر المخصص‪ ،‬استخدم الوسم األصلي النظامي‪ ،>button< ‬لكن‬ ‫‪.1‬‬
‫أضف إليه الصفة‪.is="hello-button" ‬‬

‫<‪>button is="hello-button">...</button‬‬

‫إليك المثال كاماًل ‪:‬‬

‫<‪>script‬‬
‫‪ //‬عند ضغطه "‪ "hello‬الزر الذي يظهر‬
‫‪{ class HelloButton extends HTMLButtonElement‬‬
‫‪{ )(constructor‬‬
‫‪;)(super‬‬
‫))"!‪;this.addEventListener('click', () => alert("Hello‬‬
‫}‬
‫}‬

‫‪customElements.define('hello-button', HelloButton, {extends:‬‬


‫)}'‪;'button‬‬
‫<‪>script/‬‬

‫<‪>button is="hello-button">Click me</button‬‬

‫<‪>button is="hello-button" disabled>Disabled</button‬‬

‫يوسِّع الزر الجديد الزر األصلي‪ ،‬وبالتالي سيحتفظ بتنسيق وميزات الزر األصلي مثل‬
‫الصفة‪.disabled ‬‬

‫قوالب ‪HTML‬‬

‫يقدم العنصر األصلي‪ >template< ‬قوالبًا لتخزين شيفرة ‪ HTML‬ويتجاهل محتوياتها‪ ،‬ويتحقق‬


‫من صحة التركيب اللغوي للشيفرة ضمنها حص ًرا‪ ،‬لكن يمكن الوصول إليها واستخدامها في إنشاء‬
‫عناصر أخرى‪.‬‬

‫يمكن نظريًا إنشاء أي عناصر مخفية ضمن ملف ‪ HTML‬لتخزين شيفرة ‪ ،HTML‬فما الغاية من‪< ‬‬
‫‪>template‬؟‬

‫أواًل ‪ :‬قد يحتوي على أي شيفرة ‪ HTM‬صالحة‪ ،‬حتى تلك التي تتطلب عادةً وسم إغالق ‪،Closing tag‬‬
‫فيمكن أن نضع ضمنها أسطر جدول‪:>tr< ‬‬

‫<‪>template‬‬
‫<‪>tr‬‬
‫<‪>td>Contents</td‬‬
‫<‪>tr/‬‬
‫<‪>template/‬‬

‫عندما نحاول عادةً وضع‪ >tr< ‬ضمن‪ >div< ‬مثاًل ‪ ،‬فسيكتشف المتصفح الخطأ في بنية ‪DOM‬‬


‫ويصلحها بإضافة الوسم‪ >table< ‬وجعل السطر ضمنه تلقائيًا‪ .‬من الناحية األخرى سيُبقي‪< ‬‬
‫‪ >template‬ما وضعناه في المكان الذي وضعناه فيه دون تعديل‪.‬‬

‫يمكننا وضع تنسيقات وسكربتات ضمن‪ >template< ‬أيضًا‪:‬‬


‫<‪>template‬‬
‫<‪>style‬‬
‫} ;‪p { font-weight: bold‬‬
‫<‪>style/‬‬
‫<‪>script‬‬
‫)"‪;alert("Hello‬‬
‫<‪>script/‬‬
‫<‪>template/‬‬

‫يَعُد المتصفح محتويات‪" >template< ‬خارج المستند"‪ ،‬أي لن تُطبق قواعد التنسيق الموجودة‬


‫ضمنه ولن يعمل‪ ،>video autoplay< ‬كما سيُصبح المحتوى حيًا ‪-‬أي يُطبَّق التنسيق وتنفَّذ‬
‫السكربتات‪ -‬عندما ندرجه ضمن المستند‪.‬‬

‫إدراج القالب‬

‫يُتاح محتوى قالب ضمن الخاصية‪ content ‬العائدة له على شكل عقدة من‬


‫النوع‪ ،DocumentFragment ‬وهي نوع خاص من عقد ‪ ،DOM‬ويمكن التعامل معها مثلما نتعامل‬
‫مع أي عقدة ‪ DOM‬عدا خاصية واحدة مميزة‪ ،‬وهي إدراج األبناء بداًل من العقدة عندما نحاول إدراجها‪،‬‬
‫فعلى سبيل المثال‪:‬‬

‫<‪>"template id="tmpl‬‬
‫<‪>script‬‬
‫)"‪;alert("Hello‬‬
‫<‪>script/‬‬
‫<‪>div class="message">Hello, world!</div‬‬
‫<‪>template/‬‬

‫<‪>script‬‬
‫)'‪;let elem = document.createElement('div‬‬

‫‪ //‬انسخ محتوى القالب الستخدامه مرات عدة‬


‫))‪;elem.append(tmpl.content.cloneNode(true‬‬

‫)‪;document.body.append(elem‬‬
‫‪ //‬سيعمل اآلن السكربت الموجود ضمن القالب‬
‫<‪>script/‬‬
:>template< ‫ الخفية في الفصل السابق باستخدام القالب‬DOM ‫دعونا نعيد كتابة مثال شجرة‬

>"template id="tmpl<
>style> p { font-weight: bold; } </style<
>p id="message"></p<
>template/<

>div id="elem">Click me</div<

>script<
{ )(elem.onclick = function
;elem.attachShadow({mode: 'open'})

)*( // ;elem.shadowRoot.append(tmpl.content.cloneNode(true))

elem.shadowRoot.getElementById('message').innerHTML = "Hello
;"!from the shadows
;}
>script/<

‫في السطر (*) مثل عقدة من‬ tmpl.content ‫عندما ننسخ وندرج‬


‫ وسيشكالن‬،‫>) بداًل منها‬p<‫و‬ >style<( ‫ فسيُدرج أبناؤها‬،DocumentFragment ‫النوع‬
:‫ الخفية‬DOM ‫شجرة‬

>"div id="elem<
shadow-root#
>style> p { font-weight: bold; } </style<
>p id="message"></p<
>div/<

‫خالصة‬

‫ لها وسم خاص‬،Autonomous ‫ األول عناصر مخصصة ذاتية التصرف‬:‫للعناصر المخصصة نوعان‬
:‫ وإليك تخطيط التعريف لهذه العناصر‬،HTMLElement ‫جديد وتوسع الصنف‬

{ class MyElement extends HTMLElement


constructor() { super(); /* ... */ }
‫‪} /* ... */ { )(connectedCallback‬‬
‫}‬ ‫‪/* ... */ { )(disconnectedCallback‬‬
‫} ;]‪static get observedAttributes() { return [/* ... */‬‬
‫)‪... */ { attributeChangedCallback(name, oldValue, newValue‬‬
‫*‪} /‬‬
‫‪} /* ... */ { )(adoptedCallback‬‬
‫}‬
‫)‪;customElements.define('my-element', MyElement‬‬
‫‪/* >my-element< */‬‬

‫أما النوع الثاني‪ ،‬فهو عناصر أصلية مع َّدلة ‪ :customized‬توّسع عناصرًا أصليةً موجودة‪ ،‬وتتطلب‬
‫وسيطًا ثالثًا للدالة‪ ،.define ‬وإلى الصفة‪ is="..." ‬ضمن وسمها‪.‬‬

‫‪} /*...*/ { class MyButton extends HTMLButtonElement‬‬


‫‪customElements.define('my-button', MyElement, {extends:‬‬
‫)}'‪;'button‬‬
‫‪/* >"button is="my-button< */‬‬

‫تدعم معظم المتصفحات العناصر المخصصة جيدًا‪ ،‬كما يوجد‪ ‬موائم ‪ polyfill‬أي تعويض نقص الدعم‬
‫للمتصفحات غير المدعومة‪.‬‬

‫قد يكون محتوى‪ >template< ‬أي عناصر ‪ HTML‬صالحة قواعديًا‪.‬‬ ‫‪‬‬


‫يعُد المتصفح محتويات‪ >template< ‬خارج المستند‪ ،‬إذ لن يكون لها أي تأثير‪.‬‬ ‫‪‬‬
‫يمكن الوصول إلى محتوى القالب‪ template.content ‬من ‪ ،JavaScript‬ونسخها‬ ‫‪‬‬
‫إلعادة استخدامها في مك َّون جديد‪.‬‬
‫الوسم فريد ألن‪:‬‬ ‫‪‬‬
‫‪ .0‬محتويات القالب‪ >template< ‬ستتعرض إلى تدقيق قواعدي من قِبل المتصفح‪،‬‬
‫وهذا مخالف الستخدام قالب نصي ضمن سكربت‪.‬‬
‫‪ .1‬يُسمح باستخدام وسوم ‪ HTML‬عالية المستوى حتى تلك التي ال يجب استخدامها دون‬
‫عنصر يغلّفها‪ ،‬مثل‪ >tr< ‬التي ينبغي تغليفها بالوسم‪.>table< ‬‬
‫‪ .2‬تصبح المحتويات حيّةً ‪-‬أي تُطبَّق وتُنفَّذ‪ -‬عندما تُدرج ضمن المستند‪.‬‬
‫ال يدعم العنصر‪ >template< ‬ال ُمكررات ‪ ،iterator‬وال آليات الربط بقواعد البيانات أو‬ ‫‪‬‬
‫تبديل المتغيرات‪ ،‬لكن يمكن إدراجها في أعلى القالب‪.‬‬

‫مهام إلنجازها‬

‫عنصر توقيت مباشر‬

‫أنشئ العنصر‪ >live-timer< ‬الذي يعرض الوقت الحالي‪:‬‬


‫ينبغي أن يستخدم عنصر التوقيت العنصر‪ >time-formatted< ‬الذي أنشأناه سابقًا‪،‬‬ ‫‪.1‬‬
‫بصورة ضمنية وليس بنسخ وظيفته‪.‬‬
‫يُح َّدث المؤقت كل ثانية‪.‬‬ ‫‪.2‬‬
‫مع كل تحديث للمؤقت سيتولد حدث خاص يُدعى‪ tick ‬يحمل التاريخ الحالي ضمن‬ ‫‪.3‬‬
‫الخاصية‪ ،event.detail ‬راجع فصل "إنشاء أحداث مخصصة في المتصفح عبر‬
‫جافاسكربت"‪.‬‬

You might also like