يمكّننا انتشار اﻷحداث نحو اﻷسفل واﻷعلى من تطبيق أحد أقوى أنماط معالجة الأحداث، وهو ما يُسمى تفويض الأحداث (event delegation).
تتلخّص الفكرة في أنه إذا كان لدينا الكثير من العناصر التي تُعالج بطريقة متماثلة، فبدل أن نسند معالجا لكلّ منها، فإنّنا نسند معالجا واحدا إلى السلف الذي يلتقون فيه.
في داخل المعالج، يمكن أن نتعرّف على مكان وقوع الحدث من خلال event.target
، ثم نعالجه.
لنرى مثالا على ذلك -- مخطط باكوا الذي يعكس الفلسفة الصينية القديمة، كما هو مبيّن من هنا. ويمكن تمثيله بواسطة HTML كالتالي:
<table> <tr> <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th> </tr> <tr> <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td> <td class="n">...</td> <td class="ne">...</td> </tr> <tr>...2 more lines of this kind...</tr> <tr>...2 more lines of this kind...</tr> </table>
يحتوي الجدول على 9 خانات، لكنها قد تكون 99 أو 9999، لا يهمّ ذلك.
مهمّتنا هي إبراز الخانة عند النقر عليها.
بدل إسناد معالج onclick
إلى كلّ <td>
(قد يكون هناك الكثير منها)، سنسند المعالج "catch-all" إلى العنصر <table>
.
يستخدم المعالجُ الخاصيّة event.target
للحصول على العنصر الذي نُقر عليه ثم يبرزه.
إليك الشيفرة:
let selectedTd; table.onclick = function(event) { let target = event.target; // أين كان النقر؟ if (target.tagName != 'TD') return; // ؟ إذًا لا يهمّنا ذلك TD ليس في highlight(target); // أبرزه }; function highlight(td) { if (selectedTd) { // أزل الإبراز الحالي إن وُجد selectedTd.classList.remove('highlight'); } selectedTd = td; selectedTd.classList.add('highlight'); // الجديدة td أبرز الـ }
لا تكترث هذه الشيفرة بعدد الخانات التي في الجدول. يمكننا إضافة وإزالة الخانات <td>
ديناميكيا في أي وقت، وستظل وظيفة الإبراز تعمل.
لكن تبقى هناك نقيصة. قد لا يقع النقر على العنصر <td>
بعينه ولكن على عنصر آخر بداخله. ففي حالتنا هذه، لو ألقينا نظرة داخل HTML، سنلاحظ أوسمة مدرجة داخل <td>
، مثل <strong>
:
<td> <strong>Northwest</strong> ... </td>
فمن الطبيعي أنه لو وقع النقر على <strong>
، فسيصير هو القيمة التي يحملها event.target
.
داخل المعالج table.onclick
، ينبغي علينا أن نأخذ event.target
ونتحقق إن كان النقر قد وقع داخل <td>
أو لا.
هذه هي الشيفرة المحسّنة:
table.onclick = function(event) { let td = event.target.closest('td'); // (1) if (!td) return; // (2) if (!table.contains(td)) return; // (3) highlight(td); // (4) };
إليك بعض الإيضاحات:
-
يعيد التابع
elem.closest(selector)
أقرب سلف يطابق المُحدِّد selector. في حالتنا، نبحث عن أقرب<td>
نصادفه صعودًا من العنصر المصدري. -
إذا لم يكن
event.target
بداخل أيّ<td>
، فسيُعاد الاستدعاء مباشرة، إذ ليس هناك شيء لفعله. -
في حال تداخل الجداول، قد تكون
event.target
هي<td>
، لكنّها موجودة خارج الجدول الحاليّ. فنتحقق إذًا من أنّ<td>
خاصّة بجدولنا الحاليّ. - وإذا كانت كذلك، نبرزها.
وبذلك، تكون لدينا شيفرة سريعة وفعّالة للإبراز، لا تكترث بعدد الخانات التي في الجدول.
مثال عن التفويض: الأفعال داخل الترميز (markup)
هناك استعمالات أخرى لتفويض الأحداث.
لنقُل أننا نود إنشاء قائمة من الأزرار: "حفظ" و "تحميل" و "بحث" وغير ذلك. ويوجد هناك كائن له التوابع save
و load
و search
… فكيف يتم الربط بين هذه التوابع واﻷزرار؟
أوّل ما قد يتبادر إلى الذهن هو إسناد معالج إلى كلّ زر. ولكنّ هناك حلًّا أكثر أناقة. يمكننا إسناد معالج إلى القائمة بأكملها و إضافة سمات data-action
للأزرار تحمل التابع الذي سيُستدعى:
<button data-action="save">Click to Save</button>
يقرأ المعالج السمة، وينفّذ التابع. ألقِ نظرة على المثال أدناه:
<div id="menu"> <button data-action="save">Save</button> <button data-action="load">Load</button> <button data-action="search">Search</button> </div> <script> class Menu { constructor(elem) { this._elem = elem; elem.onclick = this.onClick.bind(this); // (*) } save() { alert('saving'); } load() { alert('loading'); } search() { alert('searching'); } onClick(event) { let action = event.target.dataset.action; if (action) { this[action](); } }; } new Menu(menu); </script>
يُرجى ملاحظة أن this.onClick
مرتبط بـ this
في (*)
. هذا مهمّ، لأنّه لو لم يكن كذلك فإن this
الذي بداخله سيشير إلى عنصر (elem
) ، وليس الكائن Menu
، ولا يكون بذلك this[action]
هو ما نحتاجه.
فما هي إذًا المزايا التي يقدّمها تفويض الأحداث هنا؟
- لا نحتاج كتابة شيفرة لإسناد معالج لكل زر. بل ننشئ فقط تابعًا ونضعه في الترميز.
- تصبح بنية HTML مرنة، فيمكننا إضافة وإزالة أزرار بسهولة.
يمكننا أيضا استخدام أصناف مثل .action-save
و .action-load
، لكن سمة مثل data-action
أفضل من الناحية الدلالية، بالإضافة إلى إمكانية استخدامها في قواعد CSS.
نمط "السلوك"
يمكننا أيضا استخدام تفويض الأحداث لإضافة "سلوكيّات" للعناصر تصريحيًّا، بواسطة سمات وأصناف خاصة.
يتألّف هذا النمط من جزأين:
- نضيف سمة مخصّصة إلى العنصر تعبّر عن سلوكه.
- يتتبع الأحداثَ معالجٌ على نطاق المستند، فإذا وقع حدث على عنصر له سمة، فإنه يقوم بالفعل المناسب.
مثال عن السلوك: العداد
على سبيل المثال، تضيف السمة data-counter
هنا سلوك "زيادة القيمة عند النقر" للأزرار:
Counter: <input type="button" value="1" data-counter> One more counter: <input type="button" value="2" data-counter> <script> document.addEventListener('click', function(event) { if (event.target.dataset.counter != undefined) { // إذا كانت السمة موجودة... event.target.value++; } }); </script>
إذا نقرنا على أحد الأزرار، فإن القيمة التي عليه ستزداد. بغضّ النظر عن هذه الأزرار، فإن المنهجية العامة المتبعة هنا مهمّة.
يمكن أن يكون هناك من السمات مع data-counter
بقدر ما نرغب. يمكننا إضافة سمات جديدة إلى HTML في أي وقت. باستخدام تفويض الأحداث، نكون قد "وسّعنا" HTML من خلال إضافة سمة تعبّر عن سلوك جديد.
تنبيه: استخدم دائما addEventListener
في المعالجات التي على مستوى المستند
عند إسناد معالج حدثٍ إلى الكائن document
، يجب أن نستخدم دائما addEventListener
، وليس document.on<event>
، لأن هذا الأخير سيؤدي إلى تعارضات: تستبدل المعالجاتُ الجديدة المعالجات القديمة.
في المشاريع الواقعية، من الطبيعي أن تكون هناك عدة معالجات قد أُسندت بواسطة أجزاء مختلفة من الشيفرة.
مثال عن السلوك: القالِب (toggler)
لنرى مثالا آخر عن السلوك. يؤدي النقر على عنصرٍ له السمة data-toggle-id
إلى إخفاء وإظهار العنصر الذي له ذاك الـ id
.
<button data-toggle-id="subscribe-mail"> Show the subscription form </button> <form id="subscribe-mail" hidden> Your mail: <input type="email"> </form> <script> document.addEventListener('click', function(event) { let id = event.target.dataset.toggleId; if (!id) return; let elem = document.getElementById(id); elem.hidden = !elem.hidden; }); </script>
لنلاحظ مرة أخرى ما قمنا به. لإضافة وظيفة القلب إلى عنصرٍ ما من الآن فصاعدًا، لا حاجة لمعرفة جافاسكربت، يكفي استخدام السمة data-toggle-id
.
قد يصير هذا ملائما بالفعل -- لا حاجة لكتابة جافاسكربت لكل واحد من هذه العناصر. يكفي استخدام السلوك فقط. يجعل المعالج الذي على مستوى المستند ذلك يعمل مع أي عنصر على الصفحة.
يمكننا أن نجمع بين عدّة معالجات في نفس العنصر أيضا.
قد يشكل نمط "السلوك" بديلا عن الأجزاء المصغرة (mini-fragments) في جافاسكربت.
الملخص
تفويض الأحداث رائع حقًّا! إذ يُعدّ واحدا من أنفع الأنماط المتعلّقة بأحداث DOM.
كثيرا ما يُستخدم تفويض اﻷحداث لإضافة نفس المعالج لعدّة عناصر متماثلة، لكن لا يقتصر اﻷمر على ذلك.
الخوارزمية:
- أسند معالجًا وحيدًا إلى العنصر الحاوي.
-
في المعالج -- افحص العنصر المصدري
event.target
. - إذا وقع الحدث داخل عنصر يهمّنا، عالج الحدث.
المزايا:
- يبسّط التهيئة ويوفّر الذاكرة: لا حاجة لإضافة عدة معالجات.
- أقلّ شيفرة: عند إضافة أو إزالة عناصر، لا داعي لإضافة أو إزالة المعالجات.
-
التعديلات على DOM: يمكننا إضافة أو إزالة العناصر جماعيا بواسطة
innerHTML
وما إلى ذلك.
للتفويض حدود أيضا بالطبع:
-
أولًا، يجب أن يكون الحدث منتشرًا نحو اﻷعلى. بعض الأحداث لا تنتشر نحو اﻷعلى. يجب كذلك أن لا تَستخدم المعالجاتُ التي في الأسفل
event.stopPropagation()
. - ثانيًا، قد يضيف التفويض عبئًا على وحدة المعالجة المركزية (CPU)، لأنّ المعالج الذي على مستوى الحاوي يستجيب للأحداث في أي مكان في الحاوي، بغضّ النظر عن كونها مهمّة لنا أو لا. لكن العبئ عادةً طفيف، فلا نأخذه بالحسبان.
التمارين
أخفي الرسائل باستخدام التفويض
الأهمية: 5
هناك قائمة من الرسائل لها أزرار لإزالتها [x]
. اجعل الأزرار تعمل.
كما هو مبيّن هنا.
ملاحظة: يجب أن يكون هناك منصت واحد للأحداث على الحاوي. استخدم تفويض الأحداث.
أنجز التمرين في البيئة التجريبية
الحل
قائمة شجرية
الأهمية: 5
أنشئ شجرة يمكن فيها إظهار وإخفاء العقد الأبناء بواسطة النقر:
كما هو مبيّن هنا.
المتطلبات:
- معالج واحد للأحداث فقط (استخدم التفويض).
- يجب ألا يفعل النقر خارج عقدة العنوان (في مساحة فارغة) أي شيء.
أنجز التمرين في البيئة التجريبية
الحل
ينقسم الحل إلى جزئين:
-
ضع كلّ عقدة عنوان في الشجرة داخل
<span>
. بهذا يمكننا إضافة تنسيقات CSS إلى:hover
و معالجة النقرات على النص بالضبط، لأن عُرض<span>
هو نفس عُرض النص بالضبط (بخلاف ما لو كان بدونه). -
عيّن معالجًا على العقدة الجذر
tree
، وعالج النقرات على العنوانين<span>
تلك.
جدول قابل للترتيب
الأهمية: 4
اجعل الجدول قابلًا للترتيب: يجب أن يؤدي النقر على العناصر <th>
إلى ترتيبه حسب العمود الموافق.
لكلّ <th>
نوع معين موجود بداخل السمة، هكذا:
<table id="grid"> <thead> <tr> <th data-type="number">Age</th> <th data-type="string">Name</th> </tr> </thead> <tbody> <tr> <td>5</td> <td>John</td> </tr> <tr> <td>10</td> <td>Ann</td> </tr> ... </tbody> </table>
في المثال أعلاه، يحتوي العمود الأول على الأرقام، و العمود الثاني على الحروف. يجب أن تقوم دالة الترتيب بمعالجة الترتيب حسب النوع.
يستلزم فقط أن يُدعم النوعان "string"
و "number"
.
يمكن مشاهدة المثال يعمل من هنا.
ملاحظة: يمكن أن يكون الجدول كبيرا، بأي عدد من الأسطر والأعمدة.
أنجز التمرين في البيئة التجريبية
الحل
سلوك التلميحات
الأهمية: 5
أنشئ شفرة جافاسكربت لأجل سلوك التلميحات (tooltips).
عندما يحوم مؤشر الفأرة فوق عنصر له السمة data-tooltip
، فيجب أن تظهر التلميحة فوقه، وعندما يفارقه فإنها تختفي.
هذا مثال لشفرة HTML مع الشرح:
<button data-tooltip="the tooltip is longer than the element">Short button</button> <button data-tooltip="HTML<br>tooltip">One more button</button>
يجب أن تعمل كما هنا.
سنفترض في هذا التمرين أن جميع العناصر التي لها data-tooltip
تحتوي على نص فقط. لا وسوم متداخلة (بعد).
التفاصيل:
-
يجب أن تكون المسافة بين العنصر والتلميحة
5px
. - يجب أن تكون التلميحة في منتصف العنصر، إن أمكن ذلك.
- يجب ألا تقطع التلميحة حوافّ النافذة. من المفترض أن تكون التلميحة فوق العنصر، فإذا كان العنصر في أعلى الصفحة ولا مكان هناك للتلميحة، فإنها تكون تحته.
-
يُعطى محتوى التلميحة في السمة
data-tooltip
. يمكنها أن تحوي أي شفرة HTML.
ستحتاج إلى حدثين هنا:
-
mouseover
يحصل عندما يحوم المؤشر فوق العنصر. -
mouseout
يحصل عندما يفارق المؤشر العنصر.
يُرجى استخدام تفويض الأحداث: أسند اثنين من المعالجات إلى document
لتتبّع كلّ "الحومان" و "المفارقة" للعناصر التي لها data-tooltip
وقم بإدارة التلميحات من هناك.
بعد تطبيق السلوك، يمكن ولو لأناس غير متعودين على جافاسكربت إضافة عناصر بتلميحات.
ملاحظة: يجب أن تظهر تلميحة واحدة فقط في نفس الوقت.
أنجز التمرين في البيئة التجريبية
الحل
ترجمة -وبتصرف- للمقال Event delegation من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.