সরাসরি পর্বে যান
পর্ব ৩৮ · ৫৯-এর মধ্যে · মডিউল ৬

event — bubbling, delegation আর custom event

JavaScript Events — Listeners, the Event Object, Capture & Bubble, Delegation, Custom Events and EventTarget

This Bangla lesson explains browser events end to end: adding and removing listeners with options, the event object, the capture, target and bubble phases, stopPropagation versus preventDefault, event delegation with one listener for many elements, custom events with CustomEvent and EventTarget, and a map of common events with practical UI patterns.

এই পর্বে যা শিখবেন

  • addEventListener, removeEventListener আর চারটি option — once, passive, capture, signal
  • Event object পড়া: type, target বনাম currentTarget, key/code, clientX, isTrusted
  • Capture → target → bubble যাত্রা, আর stopPropagation, stopImmediatePropagation, preventDefault-এর সঠিক ব্যবহার
  • Event delegation দিয়ে একটি listener-এ অনেক element — নতুন যোগ হওয়া element-সহ
  • প্রচলিত event-এর মানচিত্র (pointer, keyboard, focus, scroll, lifecycle), CustomEvent, নিজের EventTarget class, আর accessibility ও memory-র সাবধানতা

১ · Event কী — ব্রাউজার বলছে “কিছু একটা ঘটেছে”

এখন পর্যন্ত লেখা বেশিরভাগ প্রোগ্রাম ওপর থেকে নিচে চলে, তারপর শেষ। কিন্তু একটি ওয়েব পাতা কখনো “শেষ” হয় না। পাতা খুলে থাকে, আর অপেক্ষা করে — কখন ব্যবহারকারী বোতাম চাপবেন, কখন কিছু লিখবেন, কখন স্ক্রল করবেন। কখন কী ঘটবে, প্রোগ্রাম আগে থেকে জানে না। এই ধরনের প্রোগ্রামকে বলে event-driven প্রোগ্রাম।

একটি event হলো ব্রাউজারের পাঠানো একটি বার্তা: “এই element-এ এই ঘটনা ঘটেছে”. ক্লিক, কি চাপা, মাউস সরানো, স্ক্রল, ফর্ম জমা, ছবি লোড শেষ, ট্যাব লুকানো — সবই event. আপনার কাজ হলো আগে থেকে বলে রাখা, “এই element-এ এই ধরনের event ঘটলে এই function ডেকো”. এই function-কে বলে event listener বা handler. পর্ব ৩১-এ দেখেছেন, ব্রাউজার event-কে একটি task হিসেবে queue-তে রাখে, আর call stack খালি হলে event loop আপনার listener চালায়। তাই একটি listener যতক্ষণ চলে, ততক্ষণ পাতা আর কোনো ক্লিকে সাড়া দেয় না — listener ছোট রাখুন।

একটি বড় অফিসের রিসেপশনের কথা ভাবুন। রিসেপশনিস্ট আগে থেকে একটি খাতা রাখেন: “পার্সেল এলে হিসাব বিভাগের রহিম সাহেবকে ডাকবেন”, “অতিথি এলে ম্যানেজারকে খবর দেবেন”. পার্সেল কখন আসবে কেউ জানে না, কিন্তু এলে সঠিক মানুষ খবর পান। এখানে পার্সেল আসা = event, খাতার প্রতিটি লাইন = listener রেজিস্টার করা, রহিম সাহেব = listener function. আর খবরটি ফ্লোর থেকে ফ্লোরে, বিভাগ থেকে বিভাগে যেতে পারে — এই পর্বের বড় অংশ সেই “যাত্রা” নিয়ে।

একটি event-এর যাত্রা বুঝতে DOM গাছ মনে করুন (পর্ব ৩৭)। আপনি একটি <span>-এ ক্লিক করলেন, যেটি একটি <li>-এর ভেতরে, যেটি একটি <ul>-এর ভেতরে। ক্লিকটি কি শুধু span-এর? না — span-এ ক্লিক মানে li-তেও ক্লিক, ul-এও, body-তেও। তাই ব্রাউজার event-টিকে গাছের ওপর থেকে নিচে span পর্যন্ত নামায়, তারপর আবার ওপরে তোলে, আর পথে প্রতিটি element-এর listener-কে সুযোগ দেয়। এই তিন ধাপের যাত্রা — capture, target আর bubble — এই পর্বের কেন্দ্র। এর জন্যই একটি parent-এ একটি listener দিয়ে তার সব সন্তানের ক্লিক সামলানো যায়, যাকে বলে event delegation.

👂 Listener — আগে থেকে বলে রাখা

btn.addEventListener("click", fn) — fn এখনই চলে না। ব্রাউজার শুধু মনে রাখে, পরে ক্লিক হলে ডাকবে। fn একটি callback.

📦 Event object — ঘটনার বিবরণ

Listener-এর প্রথম argument: কী ঘটল (type), কোথায় (target), কোন কি, মাউস কোথায় — সব একটি object-এ।

🌳 যাত্রা — শুধু এক element-এ নয়

ঘটনা গাছ বেয়ে নামে আর ওঠে। পথের প্রতিটি পূর্বপুরুষ element শুনতে পারে — চাইলে থামাতেও পারে।

C/C++/Python/Java জানলে

যেকোনো GUI জগতে একই ভাবনা: Java Swing-এ button.addActionListener(e -> …), JavaFX-এ setOnAction, Python Tkinter-এ widget.bind("<Button-1>", fn), Qt-তে signal আর slot. Java-তে listener আগে লেখা হতো anonymous class দিয়ে (Java-র nested ও anonymous class), আর পুরো নকশাটি Observer pattern (Java-র design pattern). ব্রাউজারের বিশেষত্ব হলো যাত্রা: Swing-এ ActionListener শুধু সেই বোতামের, কিন্তু DOM-এ event গাছ বেয়ে পূর্বপুরুষদের কাছেও যায়। এগুলোর কোনোটি না জানলে বাক্সটি বাদ দিন।

২ · addEventListener আর removeEventListener

Listener লাগানোর আধুনিক ও সঠিক উপায় একটিই: target.addEventListener(type, listener, options). type হলো event-এর নাম, “on” ছাড়া — "click", "keydown", "input". একই element-এ একই event-এ যত খুশি listener লাগানো যায়; সেগুলো রেজিস্টারের ক্রমে চলে। একই function একই option-এ দুবার দিলে দ্বিতীয়টি উপেক্ষিত হয় — দুবার চলে না।

Listener সরাতে removeEventListener, আর এখানে সবচেয়ে প্রচলিত ফাঁদ: সরাতে হলে হুবহু একই function reference দিতে হয়, আর capture option-ও মিলতে হয়। নিচের কোডে দ্বিতীয় লাইনটি কিছুই সরায় না:

btn.addEventListener("click", () => console.log("চাপা হলো"));
btn.removeEventListener("click", () => console.log("চাপা হলো")); // ✗ নতুন function!

const onClick = () => console.log("চাপা হলো");                      // ✓ নাম দিয়ে রাখুন
btn.addEventListener("click", onClick);
btn.removeEventListener("click", onClick);                         // সরল

দেখতে হুবহু এক হলেও দুটি arrow function দুটি আলাদা object — পর্ব ১৪-এর reference-এর নিয়ম। পর্ব ২৪-এ দেখেছেন, .bind(this)-ও প্রতিবার নতুন function বানায়, তাই একই ফাঁদ। Anonymous listener তখনই ঠিক আছে, যখন সেটি কখনো সরানোর দরকার নেই।

চারটি option

Optionকী করেকখন ব্যবহার
once: trueপ্রথমবার চলার পরে listener নিজে থেকে সরে যায়একবারের কাজ: প্রথম ক্লিকে audio চালু, একবারের স্বাগত বার্তা
passive: trueপ্রতিশ্রুতি: এই listener preventDefault ডাকবে নাscroll-সংশ্লিষ্ট touchstart, touchmove, wheel — ব্রাউজার অপেক্ষা না করে সাথে সাথে স্ক্রল করতে পারে
capture: trueListener চলে নামার পথে (capture ধাপ), ওঠার পথে নয়সন্তানদের আগে কিছু দেখা দরকার হলে; কদাচিৎ
signalএকটি AbortSignal; abort() হলে listener সরে যায়একসাথে অনেক listener সরানো — component বন্ধ হলে

signal option-টি পর্ব ৩৬-এর AbortController-এর সাথে চমৎকার মেলে। একটি modal খুললে আপনি document-এ keydown, window-এ resize, আর বোতামে click — তিনটি listener লাগালেন। বন্ধ করার সময় তিনটি function reference আলাদা করে মনে রাখার দরকার নেই:

function openModal(modal) {
  const ctrl = new AbortController();
  const opts = { signal: ctrl.signal };
  document.addEventListener("keydown", (e) => { if (e.key === "Escape") close(); }, opts);
  window.addEventListener("resize", () => position(modal), { ...opts, passive: true });
  modal.querySelector(".close").addEventListener("click", () => close(), opts);
  function close() { modal.hidden = true; ctrl.abort(); }   // এক ডাকে তিনটিই সরল
}

পুরোনো দুই উপায় — property আর inline attribute

btn.onclick = fn — এটিও কাজ করে, কিন্তু একটি element-এ একটি event-এর জন্য মাত্র একটি জায়গা। দ্বিতীয় কোড btn.onclick = অন্যFn লিখলে প্রথমটি চুপচাপ মুছে যায়। আর HTML-এ লেখা inline handler <button onclick="addToCart(5)"> আরও সমস্যার: কোডটি একটি string, তাই global function ছাড়া কিছু ডাকা কঠিন; HTML আর আচরণ মিশে যায় (separation of concerns ভাঙে); আর নিরাপত্তার জন্য যে Content Security Policy inline script বন্ধ করে, সেটি এই attribute-গুলোও বন্ধ করে দেয় (পর্ব ৪৩)। নতুন কোডে সবসময় addEventListener.

Handler-এর ভেতরে this

পর্ব ২৪-এর নিয়ম এখানে সরাসরি খাটে। সাধারণ function handler-কে ব্রাউজার ডাকে this = যে element-এ listener বসানো (অর্থাৎ e.currentTarget) দিয়ে। Arrow handler-এর নিজের this নেই — সে চারপাশের this নেয় (lexical this). class-এর method সরাসরি দিলে (btn.addEventListener("click", this.save)) this হারায় — this হয়ে যায় বোতাম। পরামর্শ: element দরকার হলে this-এর বদলে e.currentTarget লিখুন — স্পষ্ট, আর arrow-এও কাজ করে।

৩ · Event object — ঘটনার পুরো বিবরণ

প্রতিটি listener প্রথম argument হিসেবে একটি event object পায় — সাধারণত নাম দেওয়া হয় e বা event. Event-এর ধরন অনুযায়ী এটি আলাদা class-এর: MouseEvent, PointerEvent, KeyboardEvent, InputEvent, FocusEvent — সবাই মূল Event class থেকে আসে। সবচেয়ে দরকারি property-গুলো:

Propertyমানেউদাহরণ
typeevent-এর নাম"click"
targetযে element-এ ঘটনাটি আসলে ঘটেছে — সবচেয়ে ভেতরেরবোতামের ভেতরের <i> আইকন
currentTargetযে element-এর listener এই মুহূর্তে চলছেযে <ul>-এ listener বসানো
eventPhaseযাত্রার কোন ধাপ: ১ capture, ২ target, ৩ bubble3
key / codeকি-বোর্ড: কোন অক্ষর তৈরি হলো / কোন বোতাম চাপা হলো"/" / "Slash"
clientX, clientYমাউস বা আঙুল দেখা-অংশের কোন বিন্দুতে (পিক্সেল)214, 88
isTrustedসত্যিকারের ব্যবহারকারী-ঘটনা হলে true, কোড থেকে তৈরি হলে falsebtn.click() → false
timeStampপাতা খোলার পর থেকে কত মিলিসেকেন্ড — তারিখ নয়প্রতিবার আলাদা

target বনাম currentTarget — এই পর্বের সবচেয়ে গুরুত্বপূর্ণ পার্থক্য। ধরুন একটি বোতামের ভেতরে একটি আইকন আছে: <button><i class="icon"></i> কার্টে যোগ</button>, আর listener বোতামে। ব্যবহারকারী ঠিক আইকনের ওপর ক্লিক করলে e.target হবে <i>, কিন্তু e.currentTarget হবে <button>. যে কোড e.target.dataset.id পড়ে, সে আইকনে ক্লিক হলে undefined পায় — “মাঝে মাঝে কাজ করে না” ধরনের বাগের একটি বড় উৎস। currentTarget একটি চলমান মান: listener শেষ হলে এটি আবার null হয়ে যায়, তাই পরে ব্যবহারের জন্য রাখতে চাইলে একটি variable-এ কপি করুন।

key বনাম code: key বলে কোন অক্ষর বা কাজ তৈরি হলো — "a", "A", "Enter", "Escape", "ArrowDown". এটি কি-বোর্ড layout-এর ওপর নির্ভর করে — বাংলা layout চালু থাকলে একই বোতামে অন্য অক্ষর আসতে পারে। code বলে কোন বোতাম চাপা হলো, layout যা-ই হোক — "KeyA", "Slash". শর্টকাট বা অক্ষরের জন্য সাধারণত key; গেমের WASD-এর মতো “বোতামের অবস্থান” দরকার হলে code.

isTrusted: ব্যবহারকারী সত্যি ক্লিক করলে true; কোড থেকে btn.click() বা dispatchEvent করলে false. ব্রাউজার কিছু ক্ষমতা শুধু সত্যিকারের ব্যবহারকারী-ঘটনায় দেয় — যেমন popup খোলা, fullscreen, clipboard-এ লেখা। এই পর্বের runnable উদাহরণ event তৈরি করে কোড থেকে, তাই সেখানে isTrusted false.

timeStamp দিয়ে তারিখ বা স্থির ফল আশা করবেন না। এটি পাতা খোলার মুহূর্ত থেকে মাপা মিলিসেকেন্ড (performance.now()-এর মতো), আর প্রতিবার আলাদা। দুটি event-এর মাঝের ব্যবধান মাপতে কাজে লাগে, কিন্তু output-এ ছাপলে কোড কখনো একই ফল দেবে না।

৪ · তিন ধাপের যাত্রা — capture, target, bubble

আপনি একটি <span>-এ ক্লিক করলেন। ব্রাউজার প্রথমে পথ ঠিক করে: window → document → <html> → <body> → <ul> → <li> → <span>. পথটি event শুরুর আগেই স্থির হয়ে যায় — মাঝপথে DOM বদলালেও পথ বদলায় না (e.composedPath() দিয়ে এই পথ দেখা যায়)। তারপর তিন ধাপ, যাকে একসাথে বলে event propagation:

  1. Capture ধাপ: window থেকে নিচে নেমে target-এর parent পর্যন্ত — শুধু capture: true listener চলে।
  2. Target ধাপ: target element নিজে — তার সব listener চলে (আধুনিক ব্রাউজারে capture listener আগে)।
  3. Bubble ধাপ: target-এর parent থেকে আবার ওপরে window পর্যন্ত — সাধারণ (capture ছাড়া) listener চলে।

ধাপে ধাপে দেখুন, কোন listener কখন চলে, আর প্রতিটির ভেতরে target ও currentTarget কী:

1document.addEventListener("click", A, { capture: true });2ul.addEventListener("click", B, { capture: true });3span.addEventListener("click", C);4li.addEventListener("click", D);5ul.addEventListener("click", E);6document.addEventListener("click", F);7// ব্যবহারকারী span-এ ক্লিক করলেন
এই মুহূর্তে event-এর অবস্থা
eventPhase
currentTarget
target
  1. ক্লিক, তারপর পথ। ছয়টি listener বসানো আছে — দুটি capture, চারটি সাধারণ। ব্যবহারকারী span-এ ক্লিক করলেন। ব্রাউজার পথ ঠিক করল: window → document → html → body → ul → li → span. target পুরো যাত্রায় একই থাকবে — span.
  2. Capture: document. Event ওপর থেকে নামছে। document-এ একটি capture listener (A) আছে, সেটি চলল। currentTarget = document, কিন্তু target তখনো span — A আগেই জানে ক্লিকটি কোথায় যাচ্ছে।
  3. Capture: ul. html আর body-তে কোনো listener নেই, event নীরবে পার হলো। ul-এর capture listener B চলল। li-তে capture listener নেই — সেখানেও কিছু চলবে না। লক্ষ করুন, ul-এর দুটি listener-এর মধ্যে এখন শুধু capture-টি চলেছে।
  4. Target: span. Event পৌঁছাল। eventPhase = ২, আর target ও currentTarget একই element — শুধু এই ধাপে এমন হয়। C চলল।
  5. Bubble: li. এবার event ওপরে উঠছে। li-এর সাধারণ listener D চলল। currentTarget = li, target = span. এটিই delegation-এর মূল: li জানতে পারে তার কোন সন্তানে ক্লিক হয়েছে।
  6. Bubble: ul. ul-এর সাধারণ listener E চলল। একই element-এর দুটি listener (B আর E) দুই ভিন্ন ধাপে চলেছে — একটি নামার সময়, একটি ওঠার সময়।
  7. Bubble: document, তারপর window. F চলল, তারপর window (কোনো listener নেই), আর যাত্রা শেষ। সব listener শেষ হলে তবেই ব্রাউজার তার ডিফল্ট কাজ করে — লিংক হলে নতুন পাতায় যাওয়া, submit বোতাম হলে ফর্ম জমা — যদি না কেউ preventDefault ডেকে থাকে।

এবার নিজে চালিয়ে দেখুন। কোডটি window, document, ul, li আর span — পাঁচটিতেই একটি capture আর একটি সাধারণ listener বসায়, তারপর কোড থেকে span-এ ক্লিক পাঠায়। দ্বিতীয়বার li-তে একটি stopPropagation বসিয়ে আবার ক্লিক পাঠায়। ওপরের ফ্রেমে “অর্ডার” লেখাটিতে আপনি নিজেও ক্লিক করতে পারেন — নতুন log দেখবেন।

const ul = document.querySelector("#menu");
const li = ul.querySelector("li");
const span = li.querySelector("span");
const phase = ["-", "capture", "target", "bubble"];
const nodes = { window, document, ul, li, span };

for (const [label, node] of Object.entries(nodes)) {
  for (const capture of [true, false]) {
    node.addEventListener("click", (e) => {
      console.log(`${phase[e.eventPhase]} · ${label}`);
    }, { capture });
  }
}
span.click();                                   // কোড থেকে ক্লিক

console.log("--- li-তে stopPropagation ---");
li.addEventListener("click", (e) => {
  e.stopPropagation();
  console.log("li থামাল · isTrusted:", e.isTrusted);
}, { once: true });
span.dispatchEvent(new MouseEvent("click", { bubbles: true }));

প্রথম ক্লিকে দশটি লাইন: capture · window, capture · document, capture · ul, capture · li, তারপর target · span দুবার (span-এর দুটি listener-ই target ধাপে চলে, তাই দুবারই eventPhase = ২), তারপর bubble · li, bubble · ul, bubble · document, bubble · window. দ্বিতীয় ক্লিকে capture আর target একই, bubble · li-ও চলে (li-এর আগের listener আগে রেজিস্টার হয়েছিল), তারপর li থামাল · isTrusted: false — আর ul, document, window-এর bubble listener আর চলে না। দুটি জিনিস লক্ষ করুন: stopPropagation একই element-এর বাকি listener থামায় না, শুধু পরের element-এ যাওয়া থামায়; আর isTrusted false, কারণ event-টি কোড থেকে তৈরি। new MouseEvent("click")-এ bubbles: true না দিলে event bubble ধাপে যেতই না — নিজে তৈরি event-এ bubbling ডিফল্টে বন্ধ। span.click() নিজে থেকেই bubble করে।

সব event bubble করে না

বেশিরভাগ ব্যবহারকারী-ঘটনা (click, keydown, input, submit, pointerdown) bubble করে। কিন্তু কিছু event শুধু target-এ থাকে: focus ও blur, mouseenter ও mouseleave, ছবি বা script-এর load ও error, আর কোনো element-এর নিজের scroll. এদের প্রায় সবার একটি bubble করা জোড়া আছে — focusin/focusout, mouseover/mouseout — delegation-এর জন্য সেগুলো ব্যবহার করুন। আর একটি কৌশল: যে event bubble করে না, সেটিও capture ধাপে পূর্বপুরুষের কাছ দিয়ে যায়। তাই form.addEventListener("blur", fn, true) ফর্মের সব input-এর blur ধরতে পারে।

৫ · যাত্রা থামানো আর ডিফল্ট কাজ থামানো

তিনটি method প্রায়ই গুলিয়ে যায়, অথচ তারা সম্পূর্ণ আলাদা জিনিস থামায়:

🚫 preventDefault()

ব্রাউজারের নিজের কাজ থামায় — লিংকে যাওয়া, ফর্ম জমা দিয়ে পাতা reload, ডান-ক্লিকের মেনু, Space-এ পাতা স্ক্রল। যাত্রা চলতেই থাকে।

✋ stopPropagation()

পরের element-এ যাওয়া থামায়। এই element-এর বাকি listener তবু চলে। ব্রাউজারের ডিফল্ট কাজ থামায় না।

⛔ stopImmediatePropagation()

যাত্রা থামায় আর এই element-এর পরের listener-গুলোও থামায়। সবচেয়ে কড়া।

preventDefault-এর প্রচলিত ব্যবহার: JavaScript দিয়ে ফর্ম জমা দেওয়া (পর্ব ৩৯-এ বিস্তারিত), নিজের ডান-ক্লিক মেনু (contextmenu event), লিংক চাপলে পুরো পাতা না বদলে JavaScript দিয়ে content বদলানো। Listener ডাকলে e.defaultPrevented true হয়ে যায় — পরের listener এটি দেখে বুঝতে পারে। একটি সীমা মনে রাখুন: passive: true listener-এ preventDefault উপেক্ষিত হয় (Edge/Chrome console-এ একটি সতর্কবার্তা দেখায়), কারণ passive মানেই প্রতিশ্রুতি “আমি থামাব না”. আর event-টি cancelable না হলে (যেমন scroll) preventDefault-এর কোনো প্রভাবই নেই — স্ক্রল ঘটে যাওয়ার পরে scroll event আসে।

এবার যাত্রা থামানো। একটি পণ্যের card আছে, card-এ ক্লিক করলে পণ্যের বিস্তারিত পাতা খোলে; card-এর ভেতরে একটি “কার্টে যোগ” বোতাম। বোতাম চাপলে পাতা খোলা উচিত নয়। প্রথম যে সমাধান মাথায় আসে, বোতামে stopPropagation. দেখুন এতে কী ভাঙে:

1btn.addEventListener("click", A);      // কার্টে যোগ2btn.addEventListener("click", B);      // বোতাম অ্যানিমেশন3card.addEventListener("click", C);     // বিস্তারিত পাতা খোলা4document.addEventListener("click", G); // analytics, খোলা মেনু বন্ধ
বোতামে একটি ক্লিক
A-এর ভেতরে
C-এর ভেতরে
ফল
  1. কিছু না থামালে। বোতামের দুটি listener (A, B) চলে, তারপর event card-এ ওঠে — C বিস্তারিত পাতা খুলে ফেলে, যা আমরা চাই না। তারপর document-এর G চলে: analytics ক্লিক গুনল, আর খোলা কোনো dropdown মেনু থাকলে সেটি বন্ধ হলো।
  2. A-তে stopPropagation. B তবু চলে — একই element. C চলে না, পাতাও খোলে না। কিন্তু G-ও চলে না! Analytics এই ক্লিকটি কখনো দেখল না, আর অন্য কারও লেখা “বাইরে ক্লিক করলে মেনু বন্ধ” কোড এখন এই বোতামে কাজ করে না। কোনো error নেই — শুধু নীরব ভাঙন।
  3. A-তে stopImmediatePropagation. এবার B-ও বাদ, কারণ সে A-এর পরে রেজিস্টার হয়েছিল। এটি দরকার হয় খুব বিরল ক্ষেত্রে — যেমন অন্য লাইব্রেরির listener-কে নিশ্চিতভাবে আটকাতে। দৈনন্দিন কোডে প্রায় কখনো নয়।
  4. ভালো সমাধান: থামানো নয়, যাচাই। কেউ কিছু থামায় না। C নিজের শুরুতে দেখে নেয়: if (e.target.closest("button")) return; — ক্লিকটি বোতামের ভেতর থেকে এলে সে চুপ থাকে। সবাই সব event দেখে, প্রত্যেকে নিজের সিদ্ধান্ত নেয়। G চলল, analytics ঠিক।
মূল ধারণা

Event-এর যাত্রা একটি ভাগাভাগির সম্পদ: আপনার কোড ছাড়াও analytics, লাইব্রেরি, framework, browser extension — সবাই পথে শুনছে। stopPropagation সবার জন্য event গায়েব করে দেয়। তাই নিয়ম: “আমার জন্য নয়” বোঝাতে listener-এর ভেতরে target যাচাই করুন; যাত্রা থামানোকে শেষ উপায় রাখুন। আর ডিফল্ট কাজ থামাতে চাইলে সঠিক method preventDefault — এটি যাত্রায় কোনো বাধা দেয় না।

৬ · Event delegation — এক listener, অনেক element

একটি অনলাইন দোকানের পাতায় ৫০০টি পণ্য, প্রতিটিতে একটি “কার্টে যোগ” বোতাম। সহজ চিন্তা: loop চালিয়ে ৫০০টি বোতামে ৫০০টি listener. এতে তিনটি সমস্যা। (১) ৫০০টি function object আর রেজিস্ট্রেশন — memory আর শুরুর সময় খরচ। (২) পরে “আরও দেখুন” চাপলে নতুন ২০টি পণ্য এলে তাদের কোনো listener নেই — আবার লাগাতে মনে রাখতে হবে। (৩) পণ্য মুছলে তার listener-এর কথাও ভাবতে হবে।

Bubbling এর সুন্দর সমাধান দেয়: সব বোতামের সাধারণ পূর্বপুরুষে — পণ্যের grid-এ — একটিমাত্র listener. যেকোনো বোতামে ক্লিক bubble করে grid-এ পৌঁছায়। listener-এর ভেতরে e.target বলে দেয় ক্লিকটি ঠিক কোথায়, আর closest সেখান থেকে ওপরে উঠে প্রথম মেলানো পূর্বপুরুষ খুঁজে দেয়। পণ্যের পরিচয় রাখা হয় একটি data attribute-এ (data-id="tea"). ধাপে ধাপে:

1list.addEventListener("click", (e) => {2  const item = e.target.closest("[data-id]");3  if (!item || !list.contains(item)) return;4  console.log("বাছাই:", item.dataset.id);5});6list.insertAdjacentHTML("beforeend",7  '<li data-id="p4"><b>মিষ্টি দই</b> ৳১২০</li>');8// ব্যবহারকারী নতুন li-এর <b>-তে ক্লিক করলেন
listener-এর চোখে
listener সংখ্যা
e.target
e.currentTarget
item
  1. একটি listener, list-এ। ul-এ এখন তিনটি li (p1, p2, p3). কোনো li-তে listener নেই — একটিমাত্র listener ul-এ। list হলো সেই ul.
  2. নতুন item যোগ। কোড একটি চতুর্থ li যোগ করল। তার জন্য কোনো addEventListener লেখা হয়নি, লেখার দরকারও নেই। (এখানে HTML লেখাটি আমাদের নিজের স্থির লেখা; ব্যবহারকারীর দেওয়া লেখা কখনো এভাবে HTML-এ বসাবেন না — পর্ব ৩৭ আর ৪৩।)
  3. ক্লিক পড়ল <b>-তে। ব্যবহারকারী পণ্যের নামে, অর্থাৎ li-এর ভেতরের <b>-তে ক্লিক করলেন। target = b, li নয়! Event bubble করে: b → li (listener নেই) → ul.
  4. ul-এর listener চলল। currentTarget = ul (যেখানে listener বসানো), target = b (যেখানে ক্লিক পড়েছে)। শুধু e.target.dataset.id পড়লে undefined আসত — b-এর কোনো data-id নেই।
  5. closest ওপরে খোঁজে। e.target.closest("[data-id]") b থেকে শুরু করে (b নিজেও পরীক্ষা হয়), না মিললে parent-এ ওঠে: li-এ data-id আছে — পাওয়া গেল। li-এর ফাঁকা জায়গায় বা ul-এর padding-এ ক্লিক হলে closest null দিত।
  6. পাহারা, তারপর কাজ। !item হলে ফিরে যাও। list.contains(item) নিশ্চিত করে, পাওয়া element এই list-এরই — nested list বা বাইরের কোনো data-id-ধারী পূর্বপুরুষ যেন ভুল করে ধরা না পড়ে। তারপর item.dataset.id = "p4". নতুন item কাজ করল, এক লাইন বাড়তি কোড ছাড়াই।

Delegation কোথায় সবচেয়ে কাজের: বড় বা বদলাতে থাকা list (todo, চ্যাট বার্তা, কার্টের item, টেবিলের সারি), আর যেখানে item JavaScript দিয়ে বারবার তৈরি-মোছা হয়। একটি listener-এ একাধিক কাজও সামলানো যায় — বোতামে data-action="add", data-action="remove" দিয়ে, আর listener-এ switch (btn.dataset.action). জনপ্রিয় framework-গুলোর ভেতরেও এই ভাবনা আছে — যেমন React নিজের event ব্যবস্থায় পাতার root-এ listener বসিয়ে delegation করে (পর্ব ৫৪)।

Delegation-এর সীমা। (১) যে event bubble করে না (focus, mouseenter), সেগুলোর জন্য bubble করা জোড়া (focusin, mouseover) বা capture ব্যবহার করুন। (২) mousemove, pointermove, scroll-এর মতো খুব ঘন event পুরো পাতার root-এ delegate করলে প্রতিটি নড়াচড়ায় listener চলে — যতটা সম্ভব কাছের পূর্বপুরুষে বসান। (৩) কোনো ভেতরের কোড stopPropagation করলে event আপনার listener পর্যন্ত পৌঁছাবেই না — আরেকটি কারণ, কেন সেটি এড়ানো উচিত।

পরীক্ষা ও ইন্টারভিউতে

Front-end ইন্টারভিউ আর লিখিত পরীক্ষায় bubbling আর delegation ক্লাসিক প্রশ্ন:

  • Event bubbling আর capturing কী? — event target পর্যন্ত নামে (capture), তারপর ওপরে ওঠে (bubble); addEventListener ডিফল্টে bubble ধাপে শোনে।
  • Event delegation কী, কেন? — parent-এ একটি listener, e.target.closest() দিয়ে সন্তান চেনা; কম listener, আর পরে যোগ হওয়া element-ও কাজ করে।
  • target বনাম currentTarget? — যেখানে ঘটনা ঘটেছে বনাম যার listener চলছে।
  • stopPropagation, stopImmediatePropagation, preventDefault? — যাত্রা থামানো; যাত্রা ও একই element-এর বাকি listener থামানো; ব্রাউজারের ডিফল্ট কাজ থামানো।
  • Listener কেন সরে না? — removeEventListener-এ আলাদা function reference বা ভিন্ন capture মান দেওয়া হয়েছে।

৭ · নিজের event — CustomEvent আর EventTarget

এখন পর্যন্ত event পাঠিয়েছে ব্রাউজার। কিন্তু আপনিও পাঠাতে পারেন — এবং বড় অ্যাপে এটি খুব শক্তিশালী নকশা। ধরুন পাতায় তিনটি আলাদা অংশ: পণ্যের grid, হেডারের কার্ট-ব্যাজ (লাল বৃত্তে সংখ্যা), আর পাশের “মোট দাম” প্যানেল। grid কি ব্যাজ আর প্যানেলের function সরাসরি ডাকবে? তাহলে grid-কে সবার নাম জানতে হবে, আর নতুন কোনো অংশ যোগ করলে grid-এর কোড বদলাতে হবে। ভালো নকশা: grid শুধু ঘোষণা করে “কার্ট বদলেছে”, আর যার দরকার সে শোনে। এটিই observer pattern.

DOM-এ নিজের event বানাতে CustomEvent, আর পাঠাতে dispatchEvent:

// পাঠানো দিক — কে শুনছে তা জানে না
grid.dispatchEvent(new CustomEvent("cart:updated", {
  detail: { id: "tea", count: 3 },   // যা খুশি ডেটা
  bubbles: true,                     // ডিফল্টে false — document পর্যন্ত যেতে true দিন
}));

// শোনা দিক — কে পাঠাল তা জানে না
document.addEventListener("cart:updated", (e) => {
  badge.textContent = e.detail.count;
});

তিনটি খুঁটিনাটি। (১) নাম ছোট হাতের আর একটি উপসর্গসহ রাখুন (cart:updated, chat:message) — ব্রাউজারের ভবিষ্যৎ কোনো built-in event-এর সাথে নামের সংঘাত এড়াতে। (২) bubbles আর cancelable নিজে তৈরি event-এ ডিফল্টে false. (৩) dispatchEvent synchronous: সব listener সাথে সাথে, একই call stack-এ চলে, তারপর dispatchEvent ফেরত আসে — ব্রাউজারের ক্লিকের মতো queue-তে অপেক্ষা করে না। Event-টি cancelable: true হলে আর কোনো listener preventDefault ডাকলে dispatchEvent false ফেরত দেয় — পাঠানো দিক এভাবে জিজ্ঞেস করতে পারে “কেউ আপত্তি করেছে?”.

DOM ছাড়াই event — নিজের class-এ EventTarget

addEventListener আর dispatchEvent আসলে DOM element-এর নিজের নয়; এগুলো আসে EventTarget নামের একটি class থেকে, যা থেকে Element, document, window, এমনকি WebSocket আর AbortSignal-ও তৈরি। আধুনিক ব্রাউজারে আর Node.js-এ (Node 15 থেকে global) আপনি নিজের class-ও EventTarget থেকে বানাতে পারেন — DOM-এর কোনো element ছাড়াই। পর্ব ৩২-এর Node EventEmitter-এর মতো, কিন্তু ব্রাউজার আর Node দুই জায়গায় একই API.

নিচের উদাহরণে দুটি ভাবনা একসাথে: একটি Cart class (EventTarget থেকে, private field-সহ, পর্ব ২৬) প্রতিটি বদলে change event পাঠায়, আর ব্যাজ সেটি শুনে নিজেকে বদলায়। পণ্যের grid-এ একটিমাত্র delegated listener. কোড একটি নতুন পণ্য যোগ করে, তারপর কোড থেকে তিনটি ক্লিক পাঠায়। ফ্রেমের বোতামগুলোতে আপনি নিজেও ক্লিক করে ব্যাজ বাড়তে দেখতে পারেন।

class Cart extends EventTarget {
  #items = new Map();
  add(id) {
    this.#items.set(id, (this.#items.get(id) ?? 0) + 1);
    const count = [...this.#items.values()].reduce((a, b) => a + b, 0);
    this.dispatchEvent(new CustomEvent("change", { detail: { id, count } }));
  }
}
const cart = new Cart();
const grid = document.querySelector("#grid");

cart.addEventListener("change", (e) => {           // ব্যাজ শুধু শোনে
  document.querySelector("#badge").textContent = e.detail.count;
  console.log(`কার্টে ${e.detail.id} · মোট ${e.detail.count}`);
});
grid.addEventListener("click", (e) => {            // এক listener, সব বোতাম
  const card = e.target.closest("button")?.closest("[data-id]");
  if (!card) return;                               // বোতামের বাইরে ক্লিক
  console.log(`target=${e.target.tagName}, currentTarget=#${e.currentTarget.id}`);
  cart.add(card.dataset.id);
});
grid.insertAdjacentHTML("beforeend", '<div data-id="gamchha"><b>গামছা</b><br><button type="button"><i>+</i> কার্টে যোগ</button></div>');
grid.querySelector('[data-id="tea"] button').click();
grid.querySelector('[data-id="gamchha"] i').click();   // নতুন পণ্য, ভেতরের <i>
grid.querySelector('[data-id="tea"] b').click();       // বোতাম নয় → কিছুই না

ফলাফল চার লাইন: target=BUTTON, currentTarget=#grid, কার্টে tea · মোট 1, target=I, currentTarget=#grid, কার্টে gamchha · মোট 2 — আর ফ্রেমে ব্যাজ দেখায় 2. তিনটি জিনিস লক্ষ করুন। (১) গামছার কার্ড listener লাগানোর পরে যোগ হয়েছে, আর ক্লিকটি পড়েছে বোতামের ভেতরের <i>-এ — তবু closest("button") বোতাম খুঁজে পেয়েছে, আর তারপর closest("[data-id]") কার্ড। (২) শেষ ক্লিকটি পণ্যের নামে, বোতামে নয় — closest("button") null দিয়েছে, ?. সেখানেই থেমেছে, আর কিছুই ছাপা হয়নি। (৩) “কার্টে …” লাইনটি ঠিক “target=…” লাইনের পরেই — dispatchEvent synchronous, তাই cart.add ফেরত আসার আগেই ব্যাজের listener চলে গেছে। Cart কখনো ব্যাজের নাম জানে না; কাল একটি “মোট দাম” প্যানেল যোগ করতে Cart-এর এক লাইনও বদলাতে হবে না।

৮ · প্রচলিত event-এর মানচিত্র আর বাস্তব নকশা

দলEventমনে রাখার কথা
Pointer / mouseclick, pointerdown, pointermove, pointerup, contextmenu, dblclickPointer event মাউস, আঙুল আর stylus — তিনটিকে এক API-তে আনে (e.pointerType). নতুন কোডে mouse/touch আলাদা না লিখে pointer লিখুন।
Keyboardkeydown, keyupkeypress deprecated — ব্যবহার করবেন না। কি চেপে ধরে রাখলে e.repeat true.
Focusfocus/blur, focusin/focusoutপ্রথম জোড়া bubble করে না, দ্বিতীয় জোড়া করে — delegation-এর জন্য দ্বিতীয়টি।
ফর্মinput, change, submitinput প্রতিটি অক্ষরে, change মান চূড়ান্ত হলে — পর্ব ৩৯-এ গভীরে।
স্ক্রল / আকারscroll, resize, wheelসেকেন্ডে বহুবার আসে — throttle বা debounce (পর্ব ৩৬), আর passive: true. কিছু দৃশ্যমান হলো কি না জানতে IntersectionObserver ভালো (পর্ব ৪১)।
পাতার জীবনচক্রDOMContentLoaded, load, visibilitychange, pagehide, beforeunloadনিচে আলাদা করে।

পাতার জীবনচক্র

  • DOMContentLoaded — HTML পুরো পড়া আর DOM গাছ তৈরি শেষ (আর defer script চলে গেছে); ছবি তখনো আসছে থাকতে পারে। DOM নিয়ে কাজ শুরুর জন্য এটিই যথেষ্ট। <script defer> বা module script ব্যবহার করলে সাধারণত এর অপেক্ষাও লাগে না।
  • load (window-এ) — ছবি, stylesheet, iframe সব আসা শেষ। অনেক পরে আসে; শুধু ছবির মাপের মতো জিনিস দরকার হলে। একটি <img>-এর নিজেরও load আর error event আছে।
  • visibilitychange — ব্যবহারকারী অন্য ট্যাবে গেলেন বা অ্যাপ লুকালেন (document.visibilityState === "hidden"). ভিডিও থামানো, polling কমানো, অসমাপ্ত লেখা সেভ করার সঠিক জায়গা — মোবাইলে অনেক সময় এরপর পাতা আর কোনো event ছাড়াই বন্ধ হয়ে যায়। পুরোনো unload event নির্ভরযোগ্য নয় আর ধীরে ধীরে বাদ দেওয়া হচ্ছে।
  • beforeunload — পাতা ছাড়ার আগে “আপনি কি নিশ্চিত?” জিজ্ঞাসা। আধুনিক ব্রাউজার নিজের সাধারণ বার্তা দেখায়, আপনার লেখা বার্তা নয়। সাবধানে: শুধু অসংরক্ষিত পরিবর্তন থাকলে listener লাগান, সেভ হলেই সরান — সবসময় লাগিয়ে রাখলে ব্যবহারকারী বিরক্ত হন, আর কিছু ব্রাউজারে পাতাটি দ্রুত “পেছনে যাওয়া” cache-এ রাখা যায় না।

বাস্তব নকশা: modal বন্ধ করা আর “/” শর্টকাট

একটি modal (পপআপ বাক্স) দুইভাবে বন্ধ হওয়া উচিত: Escape চাপলে, আর বাক্সের বাইরের কালো অংশে (backdrop) ক্লিক করলে — কিন্তু বাক্সের ভেতরে ক্লিক করলে নয়। ভেতরের ক্লিকও bubble করে backdrop-এ পৌঁছায়, তাই পার্থক্য করতে হয় target দিয়ে। আর অনেক সাইটে “/” চাপলে সার্চ বক্সে cursor চলে যায় — কিন্তু ব্যবহারকারী কোনো ঘরে লিখতে থাকলে নয়:

// backdrop-এ ক্লিক: target যদি backdrop নিজেই হয় (ভেতরের বাক্স নয়)
backdrop.addEventListener("click", (e) => {
  if (e.target === e.currentTarget) closeModal();
});

document.addEventListener("keydown", (e) => {
  if (e.key === "Escape" && !backdrop.hidden) closeModal();

  const typing = e.target.closest("input, textarea, select, [contenteditable]");
  if (e.key === "/" && !typing && !e.ctrlKey && !e.metaKey && !e.altKey) {
    e.preventDefault();                         // "/" অক্ষরটি বক্সে যেন না বসে
    document.querySelector("#search").focus();
  }
});

ব্রাউজারের নিজের <dialog> element (showModal()) Escape-এ বন্ধ হওয়া, focus আটকে রাখা আর backdrop নিজেই দেয় — নিজে modal বানানোর আগে সেটি ভাবুন।

Keyboard আর accessibility — <button> বিনামূল্যে যা দেয়

একটি <button>-এ click listener লাগালে সেটি শুধু মাউসে নয়, Tab দিয়ে focus করে Enter বা Space চাপলেও চলে — ব্রাউজার নিজেই কি-চাপাকে click বানিয়ে দেয়। screen reader-ও এটিকে “বোতাম” বলে ঘোষণা করে। কিন্তু <div class="btn">-এ click listener দিলে এর কিছুই হয় না: Tab-এ focus আসে না, Enter-এ কিছু হয় না, screen reader জানে না এটি বোতাম। ঠিক করতে tabindex="0", role="button" আর keydown-এ Enter/Space সামলানো — সবই হাতে। সহজ নিয়ম: ক্লিক করার জিনিস হলে <button>, অন্য পাতায় যাওয়ার জিনিস হলে <a href>. এটি accessibility-এর সবচেয়ে সস্তা জয়।

Touch, gesture আর ভুলে যাওয়া listener

মোবাইলের জন্য আলাদা touchstart/touchmove event আছে (একাধিক আঙুলের তথ্যসহ), কিন্তু বেশিরভাগ কাজে pointer event যথেষ্ট — ড্র্যাগ করতে pointerdown-এ setPointerCapture, আর ব্রাউজারের নিজের স্ক্রল/zoom আটকাতে CSS-এর touch-action. Pinch বা swipe-এর মতো gesture নিজে চিনতে হয় একাধিক pointer-এর অবস্থান মেপে; এটি জটিল, তাই সাধারণত লাইব্রেরি নেওয়া হয়।

শেষ সাবধানতা memory leak. একটি element DOM থেকে মুছে গেলে আর কোথাও তার reference না থাকলে তার listener-সহ সেটি garbage collection-এ চলে যায়। বিপদ হয় দীর্ঘজীবী target-এ: একটি single-page অ্যাপের component খোলার সময় window বা document-এ listener লাগাল, বন্ধের সময় সরাল না। listener-এর closure component-এর পুরো ডেটা ধরে রাখে, আর প্রতিবার খোলা-বন্ধে আরেকটি জমে — পাতা ধীরে ধীরে ভারী হয়, আর একই কাজ বারবার চলে। সমাধান: যে লাগায়, সে-ই সরায় — signal option এর সবচেয়ে সহজ উপায়। DevTools দিয়ে এমন leak খোঁজা পর্ব ৫৩-এ।

৯ · AI-এর যুগে — AI-এর লেখা event-কোড যাচাই করা

Event-এর কোড প্রায় সবসময় “কাজ করে” — অন্তত ডেমোতে, মাউসে, প্রথমবার। সমস্যা ধরা পড়ে পরে: নতুন item-এ ক্লিক কাজ করে না, কি-বোর্ডে চলে না, পাতা ধীরে ধীরে ভারী হয়, analytics অদ্ভুতভাবে কম ক্লিক দেখায়। AI টুল এই ভুলগুলো নিয়মিত করে, কারণ তার শেখার নমুনায় পুরোনো jQuery আর inline onclick-এর কোড প্রচুর।

AI-এর ভুলকেন সমস্যাকী চাইবেন
querySelectorAll(".btn").forEach(b => b.addEventListener(…))পরে যোগ হওয়া item-এ কাজ করে না; অনেক listenerparent-এ একটি delegated listener + closest
HTML-এ onclick="…"global function লাগে, CSP-তে বন্ধ, HTML-JS মেশানোaddEventListener
removeEventListener-এ নতুন anonymous function বা নতুন .bind(this)কিছুই সরে না — leak আর দ্বিগুণ কাজএকই reference, বা signal
সমস্যা হলেই stopPropagationanalytics, “বাইরে ক্লিকে বন্ধ” আর delegation নীরবে ভাঙেlistener-এ target যাচাই
<div onclick> বা <span>-কে বোতাম বানানোকি-বোর্ড আর screen reader-এ অচল<button type="button">
e.target.dataset.id সরাসরিবোতামের ভেতরের আইকনে ক্লিক হলে undefinede.target.closest("[data-id]") বা currentTarget
scroll/touchmove listener-এ ভারী কাজ, passive ছাড়াস্ক্রল আটকে যায় (jank)passive: true, throttle বা IntersectionObserver

AI-কে event-কোড লেখাতে চাইলে নিয়মগুলো আগেই বলে দিন:

একটি অনলাইন দোকানের পণ্যের grid-এর জন্য vanilla JavaScript লিখে দাও। প্রতিটি পণ্য-কার্ডে data-id আছে আর একটি "কার্টে যোগ" বোতাম। "আরও দেখুন" চাপলে নতুন কার্ড যোগ হয়। হেডারে একটি কার্ট-ব্যাজ আছে, আর একটি modal আছে যা Escape বা backdrop-এ ক্লিক করলে বন্ধ হবে। নিয়ম: ১. প্রতিটি বোতামে আলাদা listener দেবে না; grid-এ একটি delegated listener দেবে, আর e.target.closest("button") ও closest("[data-id]") দিয়ে কার্ড খুঁজবে। ২. HTML-এ কোনো inline onclick থাকবে না; সব addEventListener দিয়ে। ৩. ক্লিক করার সব জিনিস হবে <button type="button">, div বা span নয়। ৪. Cart হবে একটি class যা EventTarget থেকে আসে, আর বদলালে CustomEvent("change", { detail }) পাঠাবে; ব্যাজ সেটি শুনবে। Cart-এর কোডে ব্যাজের কোনো উল্লেখ থাকবে না। ৫. stopPropagation ব্যবহার করবে না; modal-এর ভেতরের ক্লিক আলাদা করবে e.target === e.currentTarget দিয়ে। ৬. modal খোলার সময় লাগানো সব listener একটি AbortController-এর signal দিয়ে লাগাবে, আর বন্ধের সময় abort করবে। ৭. শেষে ব্যাখ্যা করো: আইকনে ক্লিক হলে e.target আর e.currentTarget কী হয়, আর নতুন কার্ড কেন বাড়তি কোড ছাড়াই কাজ করে।

AI-এর লেখা event-কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Event কেন একবার নিচে নেমে আবার ওপরে ওঠে — দুই ধাপ কেন? এর উত্তর লুকিয়ে আছে ১৯৯০-এর দশকের “ব্রাউজার যুদ্ধে”.

১৯৯৭ সালে দুটি প্রধান ব্রাউজার — Netscape Navigator 4 আর Internet Explorer 4 — প্রায় একই সময়ে event-এর একটি মডেল আনে, কিন্তু একে অপরের ঠিক উল্টো। দুই কোম্পানিই একই প্রশ্নের মুখোমুখি হয়েছিল: একটি লিংক একটি paragraph-এর ভেতরে, paragraph একটি div-এর ভেতরে। লিংকে ক্লিক হলে তিনজনেরই listener থাকলে কার listener আগে চলবে?

Netscape-এর উত্তর ছিল capturing: বাইরের element আগে। window বা document আগে ঘটনাটি “ধরে” নিতে পারত (captureEvents), তারপর চাইলে ভেতরে পাঠাত। যুক্তিটি নিয়ন্ত্রণের: পাতার বাইরের স্তর সবকিছুর ওপর নজর রাখতে পারে। Microsoft-এর উত্তর ছিল bubbling: ঘটনা যেখানে ঘটেছে সেখান থেকে শুরু, তারপর বুদবুদের মতো ওপরে ওঠা। IE-তে event object পাওয়া যেত global window.event থেকে, আর ওঠা থামাতে লিখতে হতো window.event.cancelBubble = true. যুক্তিটি স্বাভাবিকতার: সবচেয়ে নির্দিষ্ট element-ই সবচেয়ে আগে জানে কী করতে হবে।

ফল হলো ওয়েব ডেভেলপারদের জন্য দুঃস্বপ্ন। একই পাতার জন্য দুই রকম কোড, ব্রাউজার চিনে নেওয়ার কৌশল, আর অসংখ্য বাগ। Peter-Paul Koch-এর quirksmode.org-এর মতো সাইট বছরের পর বছর এই পার্থক্যগুলোর তালিকা রাখত।

W3C-এর সমাধান ছিল আপসের: নভেম্বর ২০০০-এ প্রকাশিত DOM Level 2 Events মানদণ্ড দুটি মডেলকেই রাখল — আগে capture ধাপ (window থেকে নিচে), তারপর target, তারপর bubble ধাপ (ওপরে)। addEventListener-এর তৃতীয় argument useCapture বেছে দিত কোন ধাপে শুনবেন, আর ডিফল্ট রাখা হলো false — অর্থাৎ bubble. বাস্তবে প্রায় সবাই bubble ব্যবহার করে, কারণ এটিই স্বজ্ঞাত, আর IE-র বিশাল বাজার অংশের কারণে ডেভেলপাররা এতেই অভ্যস্ত ছিলেন। Capture টিকে রইল বিশেষ কাজের জন্য — যেমন বাইরের স্তরে আগে থেকে event আটকানো, বা যে event bubble করে না সেটি পূর্বপুরুষে ধরা।

মূল উপলব্ধি: আজকের তিন ধাপের যাত্রা কোনো একক নকশাকারের পরিকল্পনা নয় — দুই প্রতিদ্বন্দ্বী কোম্পানির দুই মডেলের আপস। মানদণ্ড অনেক সময় “সবচেয়ে ভালো” নকশা নয়, বরং “সবার কোড যাতে চলে” এমন নকশা বেছে নেয়।

প্র ০২মানদণ্ড ২০০০ সালে এলেও Internet Explorer বহু বছর addEventListener সমর্থন করেনি — তার নিজের ছিল attachEvent. এতে কী সমস্যা হতো, আর jQuery-এর জনপ্রিয়তায় এর ভূমিকা কী?

IE5 থেকে Microsoft নিজের একটি API দিয়েছিল: el.attachEvent("onclick", fn) আর সরাতে detachEvent. W3C-এর addEventListener-এর সাথে এর পার্থক্যগুলো ছোট মনে হলেও বাস্তবে বড়। Event-এর নামে “on” লাগত। শুধু bubble ছিল, capture ধাপ ছিল না। Handler-এর ভেতরে this হতো window, element নয় — পর্ব ২৪-এর ভাষায়, element কখনো this হিসেবে আসত না। একই function দুবার লাগালে দুবার চলত। আর একাধিক handler কোন ক্রমে চলবে, তার নিশ্চয়তা ছিল না। Event-এর তথ্য পেতে অনেকে global window.event ব্যবহার করতেন; preventDefault-এর জায়গায় returnValue = false, stopPropagation-এর জায়গায় cancelBubble = true, target-এর জায়গায় srcElement.

অর্থাৎ প্রতিটি listener-এর জন্য ডেভেলপারদের দুই রকম কোড লিখতে হতো। প্রায় প্রতিটি প্রজেক্টে একটি নিজস্ব addEvent(el, type, fn) function থাকত, যা ব্রাউজার দেখে সঠিক API ডাকত আর this ঠিক করত। এই function সঠিকভাবে লেখা এত কঠিন ছিল যে ২০০৫ সালে quirksmode-এ এর সেরা সংস্করণ লেখার একটি প্রকাশ্য প্রতিযোগিতাও হয়েছিল।

এই বিশৃঙ্খলাই ২০০৬ সালে আসা jQuery-এর সাফল্যের বড় কারণ। $(el).click(fn) বা পরে .on("click", fn) সব ব্রাউজারে একইভাবে কাজ করত, this সবসময় element, আর event object-কে মানদণ্ডের মতো করে মেরামত করে দিত। jQuery-এর .on("click", ".item", fn) delegation-কেও খুব সহজ করে দিয়েছিল, আর অনেক ডেভেলপার delegation শিখেছেন এর মাধ্যমেই।

অবশেষে IE9 (২০১১) addEventListener সমর্থন করে, আর IE11-এর আধুনিক মোডে attachEvent সরিয়ে দেওয়া হয়। আজ মানদণ্ডের API সব ব্রাউজারে এক — তাই শুধু event-এর জন্য jQuery নেওয়ার আর দরকার নেই। তবু পুরোনো কোডবেসে আর AI-এর উত্তরে এই যুগের ছাপ — window.event, srcElement, return false — এখনো দেখা যায়।

মূল উপলব্ধি: একটি মানদণ্ড লেখা হলেই সমস্যা শেষ হয় না; সবচেয়ে বড় ব্রাউজার না মানলে এক দশক ধরে দুই রকম কোড লিখতে হয়। লাইব্রেরি সেই ফাঁক ভরায় — আর ফাঁক বন্ধ হলে লাইব্রেরির প্রয়োজনও কমে যায়।

প্র ০৩২০১৬ সালে passive option কেন যোগ করা হলো? একটি ছোট boolean কীভাবে মোবাইলে স্ক্রলের মসৃণতা বদলে দিল?

সমস্যাটি বুঝতে ব্রাউজারের দৃষ্টিকোণ থেকে ভাবুন। ব্যবহারকারী ফোনের পর্দায় আঙুল রাখলেন আর টানতে শুরু করলেন। ব্রাউজার চায় সাথে সাথে পাতা স্ক্রল করতে। কিন্তু পাতায় একটি touchstart বা touchmove listener আছে, আর সেই listener preventDefault() ডেকে স্ক্রল বাতিল করতে পারে — যেমন একটি ম্যাপ বা ছবির carousel নিজে ড্র্যাগ সামলাতে চায়। তাই ব্রাউজারকে অপেক্ষা করতে হয়: listener চলুক, দেখি সে বাতিল করে কি না। আর listener চলে main thread-এ, যেটি হয়তো তখন অন্য কোনো ভারী JavaScript নিয়ে ব্যস্ত।

ফল: আঙুল নড়ছে, পাতা নড়ছে না — কয়েকশো মিলিসেকেন্ডের আটকে থাকা, যাকে বলে jank. মজার ব্যাপার, Chrome দলের পরিমাপে দেখা গিয়েছিল, এমন touch listener-এর বিশাল অংশ কখনো preventDefault ডাকেই না — অনেকগুলো analytics বা লাইব্রেরির কোড, যারা শুধু “দেখে”. অথচ ব্রাউজার তা আগে থেকে জানতে পারে না।

সমাধান হিসেবে Google-এর প্রকৌশলীরা প্রস্তাব করেন একটি প্রতিশ্রুতি: { passive: true } — “আমি preventDefault ডাকব না”. তখন ব্রাউজার অপেক্ষা না করে compositor thread-এ সাথে সাথে স্ক্রল করে, আর listener পাশাপাশি চলে। এটি WHATWG DOM মানদণ্ডে যুক্ত হয়, আর Chrome 51 (২০১৬)-এ আসে; শীঘ্রই Firefox আর Safari-ও যোগ করে। এর জন্যই addEventListener-এর তৃতীয় argument boolean থেকে options object হয়ে ওঠে।

কিন্তু পুরোনো কোড তো বদলাবে না। তাই ২০১৭ সালে Chrome একটি “intervention” করে: window, document বা body-তে লাগানো touchstart/touchmove listener ডিফল্টে passive ধরে নেওয়া হয়, যদি না কোড স্পষ্টভাবে passive: false বলে। পরে wheel-এর জন্যও একই কাজ হয়। এতে কিছু পুরোনো সাইট ভেঙেছিল, আর বিতর্কও হয়েছিল — ব্রাউজার কি মানদণ্ডের ডিফল্ট নিজের ইচ্ছায় বদলাতে পারে? Chrome-এর যুক্তি ছিল, লাখো ব্যবহারকারীর মসৃণ স্ক্রল কয়েকটি সাইটের সুবিধার চেয়ে বড়।

মূল উপলব্ধি: পারফরম্যান্সের অনেক সমস্যা ধীর কোডের নয়, অনিশ্চয়তার — ব্রাউজার জানে না আপনি কী করবেন, তাই অপেক্ষা করে। একটি স্পষ্ট প্রতিশ্রুতি (passive) সেই অপেক্ষা সরিয়ে দেয়।

প্র ০৪মাউস, আঙুল আর stylus — তিনটির জন্য আলাদা event না লিখে এক API: Pointer Events. এটি কোথা থেকে এল, আর সব ব্রাউজারে আসতে এত সময় লাগল কেন?

শুরুতে ওয়েবে ছিল শুধু মাউস — mousedown, mousemove, mouseup. ২০০৮ সালে iPhone-এর Safari-তে (iPhone OS 2) Apple আনে touch event (touchstart, touchmove, touchend), যা একাধিক আঙুলের তথ্য দেয়। পরে এটি W3C-তে Touch Events মানদণ্ড হয়। কিন্তু এতে ডেভেলপারদের দুবার কোড লিখতে হতো — একবার মাউসের জন্য, একবার touch-এর জন্য; আর মোবাইল ব্রাউজার পুরোনো সাইট চালাতে touch-এর পরে নকল mouse event-ও পাঠাত, যা থেকে “দুবার চলা” বাগ তৈরি হতো।

Microsoft তখন Windows 8 নিয়ে আসছিল — touch-screen ল্যাপটপ আর stylus-সহ ট্যাবলেট, যেখানে একই যন্ত্রে মাউস, আঙুল আর কলম তিনটিই। তারা ভাবল, তিনটির জন্য তিন API না রেখে একটি সাধারণ “pointer” ধারণা হোক। IE10 (২০১২) prefix-সহ এই API আনে, আর Microsoft প্রস্তাবটি W3C-তে জমা দেয়। ফেব্রুয়ারি ২০১৫-এ Pointer Events Level 1 W3C Recommendation হয়: pointerdown, pointermove, pointerup, সাথে pointerType (mouse/touch/pen), চাপের মাত্রা (pressure) আর কলমের কাত হওয়ার কোণ।

Chrome আর Firefox কয়েক বছরের মধ্যে যোগ করে। কিন্তু Apple দীর্ঘ সময় যোগ করেনি — তাদের touch event মডেল iOS-এ ইতিমধ্যে প্রতিষ্ঠিত ছিল, আর তারা নতুন একটি সমান্তরাল মডেলের প্রয়োজন নিয়ে সংশয় প্রকাশ করেছিল। এ নিয়ে বিভিন্ন ব্যাখ্যা প্রচলিত আছে — প্রযুক্তিগত মতভেদ থেকে শুরু করে touch event নিয়ে পেটেন্ট-সংক্রান্ত আলোচনা পর্যন্ত — তবে নিশ্চিত কারণ Apple প্রকাশ্যে বিস্তারিত বলেনি। অবশেষে Safari 13 (২০১৯)-এ Pointer Events আসে, iPad-এ Apple Pencil-এর সমর্থনসহ।

এই দীর্ঘ অপেক্ষার মানে ছিল, প্রায় সাত বছর ধরে “সবখানে চলে” এমন কোডের জন্য ডেভেলপারদের touch আর mouse দুটোই লিখতে হয়েছে, বা polyfill ব্যবহার করতে হয়েছে। আজ pointer event সব আধুনিক ব্রাউজারে আছে, তাই নতুন কোডে এটিই প্রথম পছন্দ; touch event লাগে শুধু multi-touch gesture-এর মতো বিশেষ কাজে।

মূল উপলব্ধি: ওয়েব মানদণ্ড শুধু প্রযুক্তির প্রশ্ন নয় — কোম্পানির প্ল্যাটফর্ম-কৌশল, পুরোনো বিনিয়োগ আর আইনি বিবেচনাও এর গতি ঠিক করে। একটি ভালো নকশা লেখা থেকে সবখানে চলা পর্যন্ত কখনো কখনো সাত বছর লাগে।

প্র ০৫DOM event আসলে Observer pattern — ১৯৯৪ সালের “Gang of Four” বইয়ের একটি নকশা। Front-end architecture-এ observer আর pub/sub কতটা কাজের, আর এর বিপদ কোথায়?

১৯৯৪ সালে Erich Gamma, Richard Helm, Ralph Johnson আর John Vlissides — যাঁদের “Gang of Four” বলা হয় — “Design Patterns” বইটি প্রকাশ করেন। তার একটি নকশা Observer: একটি object (subject) নিজের পরিবর্তনের খবর একাধিক নির্ভরশীল object-কে (observer) দেয়, অথচ তাদের নির্দিষ্ট class জানে না — শুধু জানে “এদের একটি update ডাকতে হবে”. ধারণাটি নতুন ছিল না; Smalltalk-80-এর Model-View-Controller-এ model নিজের “dependents”-দের এভাবেই খবর দিত। বইটি শুধু নাম দিয়ে আর নিয়ম গুছিয়ে এটিকে সবার ভাষা বানিয়ে দেয়।

DOM event এই নকশার সরাসরি রূপ: element = subject, listener = observer, addEventListener = subscribe. class Cart extends EventTarget লিখলে আপনি নিজের subject বানাচ্ছেন। এর একটি ঘনিষ্ঠ আত্মীয় publish/subscribe: এখানে প্রকাশক আর শ্রোতা একে অপরকে একেবারেই চেনে না — মাঝে একটি “event bus” বা channel থাকে, যেমন document-এ পাঠানো cart:updated. Node.js-এর EventEmitter, Redux-এর store-এর subscribe, WebSocket-এর message — সবখানে একই পরিবারের ভাবনা।

লাভ বড়: অংশগুলো আলাদা থাকে (loose coupling), নতুন শ্রোতা যোগ করতে প্রকাশকের কোড বদলাতে হয় না, আর একটি ঘটনায় অনেকে সাড়া দিতে পারে। কার্টের উদাহরণে দেখেছেন, ব্যাজ বা দামের প্যানেল যোগ করতে Cart-কে ছুঁতে হয় না।

কিন্তু দামও আছে। (১) প্রবাহ খোঁজা কঠিন: “এই বোতাম চাপলে কী কী হয়?” — কোড পড়ে বলা যায় না, কারণ শ্রোতারা যেকোনো ফাইলে থাকতে পারে। (২) ক্রমের ওপর নির্ভরতা: শ্রোতারা রেজিস্টারের ক্রমে চলে, আর কেউ অজান্তে সেই ক্রমের ওপর নির্ভর করলে নতুন একটি ফাইল যোগে বাগ আসে। (৩) ভুলে যাওয়া শ্রোতা: unsubscribe না করলে memory leak (lapsed listener সমস্যা)। (৪) event-এর ঝড়: এক event-এর শ্রোতা আরেক event পাঠায়, সেটি আরেকটি — শেষে একটি ক্লিকে অসংখ্য কাজ, কখনো অসীম loop. এজন্যই React-এর মতো framework একমুখী ডেটা-প্রবাহ বেছে নিয়েছিল (ভাবনার প্রশ্নটি পর্ব ৫৪-এ আরও)।

মূল উপলব্ধি: Observer আর pub/sub অংশগুলোকে আলাদা রাখার শক্তিশালী হাতিয়ার, কিন্তু “কে কাকে ডাকে” সেই স্পষ্টতা কেড়ে নেয়। Event নামগুলোর তালিকা আর প্রতিটির detail-এর আকার লিখে রাখুন — একটি ছোট চুক্তি পরে অনেক রহস্য বাঁচায়।

অনুশীলন

আগে মাথায় যাত্রাপথ এঁকে উত্তর অনুমান করুন, তারপর browser console-এ চালিয়ে মিলিয়ে নিন।

  1. ক্রম অনুমান করুন: <div id="d"><button id="b">OK</button></div>. Listener: div-এ সাধারণ (“d-bubble”), div-এ capture (“d-capture”), button-এ সাধারণ (“b”), document-এ সাধারণ (“doc”). button-এ ক্লিক করলে কোন ক্রমে ছাপা হবে? এবার button-এর listener-এ e.stopPropagation() যোগ করলে?

    প্রথমে: d-capture, b, d-bubble, doc — capture ধাপে div আগে, তারপর target, তারপর bubble-এ div আর document. কোন লাইনে listener লেখা হয়েছে তাতে কিছু যায় আসে না; ধাপই ক্রম ঠিক করে। stopPropagation যোগ করলে: d-capture, b — capture ধাপ আগেই শেষ, তাই d-capture চলেছে; bubble ধাপের বাকি দুটি বাদ।

  2. বাগ খুঁজুন: একটি search বক্সে input.addEventListener("input", debounce(search, 300)) লাগানো হয়েছে। পাতা ছাড়ার সময় কোড লেখে input.removeEventListener("input", debounce(search, 300)). Listener কি সরবে? কেন? ঠিক করুন — দুইভাবে।

    সরবে না। debounce(search, 300) প্রতিবার ডাকলে একটি নতুন function ফেরত দেয় (পর্ব ৩৬), তাই remove-এর function আর add-এর function আলাদা reference. ঠিক করার এক উপায়: const onInput = debounce(search, 300); একবার বানিয়ে add আর remove দুই জায়গায় onInput. দ্বিতীয় উপায়: const ctrl = new AbortController(); input.addEventListener("input", debounce(search, 300), { signal: ctrl.signal }); আর পরে ctrl.abort() — reference মনে রাখারই দরকার নেই।

  3. Accessibility মেরামত: একজন লিখেছেন <div class="like" onclick="like(7)">❤ পছন্দ</div>. কি-বোর্ড ও screen reader ব্যবহারকারীর জন্য এতে কী কী সমস্যা, আর ঠিক করা HTML ও JavaScript কেমন হবে (inline handler ছাড়া, অনেকগুলো পোস্টের জন্য)?

    সমস্যা: div-এ Tab দিয়ে focus আসে না, Enter/Space-এ কিছু হয় না, screen reader এটিকে বোতাম বলে না, আর inline onclick CSP-তে বন্ধ হতে পারে ও global like function চায়। ঠিক করা: <button type="button" class="like" data-id="7" aria-pressed="false">❤ পছন্দ</button>, আর সব পোস্টের জন্য একটি delegated listener: feed.addEventListener("click", (e) => { const btn = e.target.closest("button.like"); if (!btn) return; like(btn.dataset.id); btn.setAttribute("aria-pressed", "true"); }); — Enter আর Space এখন বিনামূল্যে কাজ করে।

  4. ছোট কাজ: একটি todo list বানান (<ul id="todos">), যেখানে প্রতিটি item-এ “✓ শেষ” আর “✕ মুছুন” দুটি বোতাম, আর ওপরে একটি “যোগ করুন” বোতাম যা নতুন item বানায়। শর্ত: ul-এ মাত্র একটি click listener, বোতাম চেনা হবে data-action দিয়ে। যোগ, শেষ আর মোছার পরে একটি todos:changed CustomEvent পাঠান, আর document-এ সেটি শুনে বাকি কাজের সংখ্যা দেখান।

    মূল অংশ: todos.addEventListener("click", (e) => { const btn = e.target.closest("button[data-action]"); const li = btn?.closest("li"); if (!li) return; if (btn.dataset.action === "done") li.classList.toggle("done"); else if (btn.dataset.action === "remove") li.remove(); todos.dispatchEvent(new CustomEvent("todos:changed", { bubbles: true })); }); আর document.addEventListener("todos:changed", () => { counter.textContent = todos.querySelectorAll("li:not(.done)").length; });. নতুন item বানাতে createElement আর textContent ব্যবহার করুন (ব্যবহারকারীর লেখা innerHTML-এ নয়), আর যোগের পরেও একই event পাঠান। লক্ষ করুন, li মুছে ফেলার পরেও event পাঠানো যায়, কারণ পাঠাচ্ছে todos — মোছা li নয়; মোছা element থেকে পাঠালে সেটি আর document পর্যন্ত bubble করত না।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রম<ul><li><span>-এর span-এ ক্লিক হলে নিচের listener-গুলো কোন ক্রমে চলে, সাজান (▲▼ চেপে বা টেনে)।

  1. ⬇️ window-এর capture listener
  2. ⬇️ document-এর capture listener
  3. ⬇️ ul-এর capture listener
  4. 🎯 span-এর listener (target ধাপ)
  5. ⬆️ li-এর সাধারণ listener
  6. ⬆️ document-এর সাধারণ listener

JavaScript-এ একটি <ul>-এ click listener বসানো, আর ব্যবহারকারী একটি <li>-এর ভেতরের <b>-তে ক্লিক করলেন। listener-এর ভেতরে e.target আর e.currentTarget কী?

  • target = li, currentTarget = ul
  • target = b, currentTarget = ul
  • target = ul, currentTarget = b
  • দুটিই b

ব্রাউজারে btn.addEventListener("click", () => go()) লেখার পরে btn.removeEventListener("click", () => go()) লিখলে কী হয়?

  • listener সরে যায়
  • TypeError হয়
  • বোতামের সব click listener সরে যায়
  • কিছুই সরে না, listener থেকে যায়

JavaScript-এ stopPropagation() আর preventDefault()-এর পার্থক্য কী?

  • দুটি একই কাজ করে, শুধু নাম আলাদা
  • stopPropagation লিংকে যাওয়া থামায়, preventDefault bubbling থামায়
  • stopPropagation যাত্রা থামায়, preventDefault ব্রাউজারের ডিফল্ট কাজ থামায়
  • preventDefault শুধু ফর্মে কাজ করে

একটি list-এ JavaScript দিয়ে বারবার নতুন item যোগ হয়। প্রতিটি item-এর ক্লিক সামলানোর সবচেয়ে ভালো উপায় কোনটি?

  • list-এ একটি listener বসিয়ে e.target.closest দিয়ে item বের করা
  • শুরুতে loop চালিয়ে প্রতিটি item-এ listener বসানো
  • প্রতিটি item-এ inline onclick attribute লেখা
  • window-এ capture listener বসানো

JavaScript-এ new CustomEvent("cart:updated", { detail: 5 }) একটি element থেকে dispatchEvent করা হলো। document-এর listener কি এটি পাবে?

  • হ্যাঁ, সব event document পর্যন্ত bubble করে
  • না, কারণ bubbles: true দেওয়া হয়নি
  • না, কারণ detail-এ object দিতে হয়
  • হ্যাঁ, তবে পরের task-এ

ব্রাউজারে একটি <div>-এ click listener দিয়ে সেটিকে বোতামের মতো ব্যবহার করলে প্রধান সমস্যা কী?

  • div-এ click event একেবারেই কাজ করে না
  • div-এর click bubble করে না
  • div-এর click বোতামের চেয়ে ধীর
  • কি-বোর্ডে focus আর Enter/Space কাজ করে না, screen reader বোতাম চেনে না

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখতে পাবেন।

মনে রাখুন

  1. Event = ব্রাউজারের বার্তা “কিছু ঘটেছে”. addEventListener দিয়ে listener লাগান; সরাতে হুবহু একই function reference বা signal. Inline onclick নয়।
  2. যাত্রা তিন ধাপে: capture (নিচে) → target → bubble (ওপরে). ডিফল্ট listener bubble ধাপে; focus, blur, mouseenter, load bubble করে না।
  3. target = যেখানে ঘটেছে, currentTarget = যার listener চলছে। Delegation: parent-এ একটি listener + e.target.closest("[data-id]").
  4. preventDefault ব্রাউজারের কাজ থামায় (passive-এ নয়); stopPropagation যাত্রা থামায় — অন্যের কোড ভাঙে, তাই target যাচাই আগে।
  5. নিজের event: CustomEvent + detail (bubbles ডিফল্টে false), dispatchEvent synchronous, class … extends EventTarget. ক্লিকের জিনিস <button>; দীর্ঘজীবী target-এর listener সরাতে ভুলবেন না।

পরের পর্বে: ওয়েবে ব্যবহারকারীর কাছ থেকে তথ্য নেওয়ার প্রধান পথ ফর্ম। পর্ব ৩৯-এ form আর validation: submit, input আর change event গভীরে, FormData, ব্রাউজারের নিজের validation (required, pattern, :user-invalid), নিজের ভুল-বার্তা — আর কেন সার্ভারেও আবার যাচাই করতেই হবে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান