Lesson 8 / الدرس 8
Bubbling and delegation / الانتشار والتفويض
An event does not stop where it happened — it travels up the tree. That one behaviour lets you attach a single listener instead of two hundred, and it solves the problem lesson 3 warned about.
الحدث لا يتوقف حيث وقع — بل يسافر صاعدًا في الشجرة. وذلك السلوك وحده يتيح لك ربط مستمع واحد بدل مئتين، ويحل المشكلة التي حذّر منها الدرس 3.
An event travels up
You click the button:
<body> 4. and finally here
<ul> 3. then here
<li> 2. then here
<button> 1. the event starts here
A listener on the <ul> hears clicks on every button inside it.
Why this is the standard pattern
| A listener per item | One listener on the parent | |
|---|---|---|
| Two hundred rows | Two hundred listeners | One |
| Rows added later | Need attaching individually | Already handled |
| After replaceChildren | All listeners lost | Still there |
| Removing a row | Remember to detach | Nothing to do |
| Reading the code | Attachment scattered | One place |
The third row is the fix for lesson 3's warning. Rebuilding a list destroys every listener inside it, so a page that re-renders needs delegation or it silently stops responding after the first update.
The two lines that make it work are worth reading closely. event.target.closest("[data-action='remove']") starts at whatever was actually clicked and walks up until it finds a remove button — so a click on an icon inside the button still finds it. And the if (!button) return is the guard: without it, a click anywhere else in the list would fall through into code expecting a button.
event.target.closest("[data-action='remove']") يبدأ من الشيء المنقور فعلًا ويمشي صاعدًا حتى يجد زر إزالة — فالنقر على أيقونة داخل الزر يجده أيضًا. وif (!button) return هو الحارس: فبدونه يسقط نقر في أي موضع آخر من القائمة إلى شيفرة تتوقع زرًا.event.target.matches(...) حين يحوي الزر أي شيء. فإن حمل الزر أيقونة أو <span>، كان هدف النقر ذلك العنصر الداخلي وفشل الفحص — فيعمل الزر حين تنقر حافته ولا يفعل شيئًا حين تنقر وسطه. وهو عيب مجنِّن لأنه يعمل نصف عمل بالضبط. وclosest هو الإصلاح، ولهذا يستخدمه هذا الدرس.
data- attributes are the usual way to mark what an element does, and they were in the HTML course for this reason. data-action="remove" reads clearly, is available as button.dataset.action in JavaScript, and does not collide with styling the way keying off a class does — someone renaming a CSS class should not break your listeners.
data- هي الطريقة المعتادة لتعليم ما يفعله عنصر، وقد كانت في دورة HTML لهذا السبب. فـdata-action="remove" تُقرأ بوضوح، ومتاحة كـbutton.dataset.action في JavaScript، ولا تتصادم مع التنسيق كما يفعل الاعتماد على صنف — فمن يعيد تسمية صنف CSS لا ينبغي أن يكسر مستمعيك.focus وblur لا ينتشران، ولهذا لا يفعل تفويضهما شيئًا بصمت — استخدم focusin وfocusout، وهما الصيغتان المنتشرتان وموجودتان لهذا بالضبط. وتلك الحقيقة وحدها توفّر بعد ظهر أول مرة تحتاجها.Try it live / جرّب بنفسك
Check yourself / اختبر نفسك
1. Why does one listener on a list handle items added later?
Nothing is attached to the new item at all. The event simply travels up through it to the listener that was always there.
2. A delegated button works at its edge and not in the middle. Why?
Maddening precisely because it half works. closest walks up from the target until it finds the button.
closest يمشي صاعدًا من الهدف حتى يجد الزر.
3.
Delegating focus does nothing. What is the fix?
focus لا يفعل شيئًا. ما الإصلاح؟Not every event bubbles. focusin and focusout exist for exactly this case.
focusin وfocusout موجودان لهذه الحالة بالضبط.Score / النتيجة: 0 / 3
Your task / مهمتك
Build a to-do list with add, remove and a "done" toggle, using exactly one click listener on the container for all three actions — distinguished by a data-action attribute. Put an icon or a span inside one of the buttons to prove closest is doing its job. Then rebuild the whole list with replaceChildren and show that everything still works afterwards.
ابنِ قائمة مهام بإضافة وإزالة ومبدّل "منجَز"، مستخدمًا مستمع نقر واحدًا بالضبط على الحاوية للأفعال الثلاثة — مميَّزة بسمة data-action. وضع أيقونة أو span داخل أحد الأزرار لتثبت أن closest يؤدي عمله. ثم أعد بناء القائمة كلها بـreplaceChildren وأظهر أن كل شيء ما يزال يعمل بعدها.
- Exactly one click listener for three actions مستمع نقر واحد بالضبط لثلاثة أفعال
- data-action used, not CSS classes data-action مستخدمة لا أصناف CSS
- A nested element inside a button, still working عنصر متداخل داخل زر وما يزال يعمل
- Everything still works after a full re-render كل شيء ما يزال يعمل بعد إعادة عرض كاملة