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

DOM — HTML থেকে জীবন্ত গাছ: খোঁজা, পড়া আর বদলানো

The JavaScript DOM — Nodes & the Tree, Selecting, Traversing, Changing Content Safely, Creating Elements, and Rendering Performance

Taught in Bangla, this lesson explains how the browser turns HTML into a live DOM tree and how JavaScript changes it: node types, selecting with querySelector and closest, live versus static lists and traversal, textContent versus innerHTML and XSS safety, attributes versus properties, classList and dataset, creating elements from data with template, and avoiding layout thrashing.

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

  • HTML লেখা থেকে DOM tree কীভাবে তৈরি হয় — node type, parent, child, sibling আর ফাঁকা জায়গার text node
  • element খোঁজা আর গাছে চলাফেরা — getElementById, querySelector(All), closest, matches, children বনাম childNodes
  • textContent, innerText আর innerHTML-এর পার্থক্য, আর ব্যবহারকারীর লেখা নিরাপদে দেখানো
  • attribute বনাম property, dataset, classList, নতুন element বানানো, <template> আর data থেকে তালিকা আঁকা
  • reflow, repaint, layout thrashing, script লোডের সময় (defer, DOMContentLoaded) আর accessibility-র মূল নিয়ম

১ · HTML লেখা থেকে জীবন্ত গাছ — DOM আসলে কী

মডিউল ৫ পর্যন্ত আমাদের বেশিরভাগ কোড চলেছে console-এ — হিসাব করেছে, ডেটা এনেছে, ফল লিখেছে। কিন্তু JavaScript জন্মেছিল একটি কাজের জন্য: ওয়েবপাতাকে জীবন্ত করা। “কার্টে যোগ করুন” চাপলে কোণের সংখ্যা ১ বাড়বে, পণ্য শেষ হলে নামের ওপর দাগ পড়বে, খোঁজার শব্দটি হলুদ হবে। এই মডিউলে আমরা ব্রাউজারের ভেতরে ঢুকব, আর প্রথম প্রশ্নটি সবচেয়ে মৌলিক: JavaScript পাতার সাথে কথা বলে কোন পথে?

পর্ব ০২-এ এর উত্তরের একটি ঝলক দেখেছেন। ব্রাউজার HTML ফাইলটি লেখা হিসেবে রেখে দেয় না। ফাইলের বাইট পড়ে, ট্যাগ চেনে, আর প্রতিটি ট্যাগ ও প্রতিটি লেখার টুকরোকে মেমরিতে একটি object বানায়। কে কার ভেতরে, সেই অনুযায়ী object-গুলো একটি গাছে সাজানো হয়। এই গাছের নাম Document Object Model — সংক্ষেপে DOM. নামটিই সংজ্ঞা: ডকুমেন্টকে (পাতাকে) object দিয়ে প্রকাশ করার একটি নকশা (model).

একটি বিয়েবাড়ির আমন্ত্রণপত্র আর আসল অনুষ্ঠানের পার্থক্য ভাবুন। কার্ডে লেখা আছে কোন টেবিলে কারা বসবেন — এটি HTML ফাইল, একটি পরিকল্পনা। অনুষ্ঠানের দিন মানুষ সত্যিই টেবিলে বসেন — এটি DOM, জীবন্ত বাস্তবতা। কেউ টেবিল বদলালে কার্ড বদলায় না, বদলায় হলের ভেতরের অবস্থা। JavaScript হলো সেই ব্যবস্থাপক, যিনি অনুষ্ঠান চলাকালীন চেয়ার সরান, নতুন অতিথি বসান। আর ফটোগ্রাফার — ব্রাউজারের রেন্ডারিং — শুধু যেখানে বদল হয়েছে, সেখানকার নতুন ছবি তোলেন।

প্রতিটি বিন্দু একটি node — আর node-এর ধরন আছে

গাছের প্রতিটি বিন্দুকে বলে node. সব node এক রকম নয়; প্রত্যেকের একটি nodeType সংখ্যা আছে। দৈনন্দিন কাজে চার ধরন চিনলেই চলে:

ধরনnodeTypeকী থেকে আসেউদাহরণ
Document9পুরো পাতা — গাছের শিকড়document
Element1প্রতিটি ট্যাগ<li>, <button>
Text3ট্যাগের মাঝের লেখা — ফাঁকা জায়গা আর নতুন লাইনও"চা", "\n "
Comment8<!-- … -->ডেভেলপারের মন্তব্য

তৃতীয় সারিটি নতুনদের সবচেয়ে বেশি অবাক করে। আপনি HTML সুন্দর করে সাজাতে প্রতিটি <li> নতুন লাইনে, দুই ঘর ভেতরে লেখেন। ব্রাউজারের কাছে সেই নতুন লাইন আর ফাঁকা ঘরও লেখা — তাই সেগুলোও একটি করে text node হয়ে গাছে ঢোকে। পর্দায় এগুলো চোখে পড়ে না (CSS সাধারণত পরপর ফাঁকা জায়গা এক করে দেখায়), কিন্তু JavaScript-এর কাছে এগুলো আসল node. নিচের ছোট একটি কার্ট-তালিকা ধাপে ধাপে গাছ হয়ে উঠতে দেখুন:

1<ul id="cart">2  <li>চা</li>3  <!-- অফার -->4  <li class="hot">শিঙাড়া</li>5</ul>
DOMএকটি element node
ul#cart   nodeType 1 (element)
DOMফাঁকা জায়গা + প্রথম li
ul#cart
├─ #text "\n  "   nodeType 3
└─ li ─ #text "চা"
DOMমন্তব্যও node
ul#cart
├─ #text "\n  "
├─ li ─ #text "চা"
├─ #text "\n  "
└─ #comment " অফার "   nodeType 8
DOMসম্পূর্ণ: ৭টি child node
ul#cart
├─ #text "\n  "
├─ li ─ #text "চা"
├─ #text "\n  "
├─ #comment " অফার "
├─ #text "\n  "
├─ li.hot ─ #text "শিঙাড়া"
└─ #text "\n"
JavaScript-এর চোখেদুই ধরনের পথ
ul.childNodes.length       → 7  (সব node)
ul.children.length         → 2  (শুধু element)
ul.firstChild              → #text "\n  "
ul.firstElementChild       → li ("চা")
li.nextElementSibling      → li.hot
li.hot.parentElement       → ul#cart
  1. শুরুর ট্যাগ → element node. Parser <ul id="cart"> পড়ে একটি element object বানায়। id attribute-টি সেই object-এর সাথে জমা থাকে। এখনো এর কোনো সন্তান নেই।
  2. নতুন লাইন আর দুই ঘর ফাঁকা — একটি text node. <ul>-এর পরে লাইন ভাঙা আর দুটি space এল। এগুলোও লেখা, তাই ul-এর প্রথম সন্তান একটি text node, li নয়! তারপর li element, যার ভেতরে নিজের text node “চা”।
  3. আবার ফাঁকা জায়গা, তারপর comment. মন্তব্যও গাছে থাকে (nodeType 8). পর্দায় দেখা যায় না, কিন্তু childNodes গুনলে আসে।
  4. গাছ সম্পূর্ণ। দ্বিতীয় li আর শেষে </ul>-এর আগের নতুন লাইন যোগ হলো। ul-এর মোট সন্তান ৭টি node: ৪টি text, ১টি comment, ২টি element. আপনি লিখেছিলেন “দুটি li”, ব্রাউজার রেখেছে সাতটি জিনিস।
  5. দুই ধরনের পথ। DOM-এ চলাফেরার প্রায় প্রতিটি property-র দুটি রূপ আছে: সব node গোনে এমন (childNodes, firstChild, nextSibling) আর শুধু element গোনে এমন (children, firstElementChild, nextElementSibling). দৈনন্দিন কাজে প্রায় সবসময় দ্বিতীয়টি চান।

তিনটি আলাদা জিনিস: HTML ফাইল, DOM আর DevTools

“View Page Source” (Ctrl+U) দেখায় সার্ভার থেকে আসা আসল HTML লেখা — যা কখনো বদলায় না। DevTools-এর Elements ট্যাব দেখায় এই মুহূর্তের DOM — JavaScript যা যোগ বা বদল করেছে, সবসহ। আর এই দুইয়ের মধ্যে প্রথম পার্থক্য তৈরি হয় JavaScript চলার আগেই: ব্রাউজার ভুল বা অসম্পূর্ণ HTML নিজে মেরামত করে। যেমন <table><tr>… লিখলে DOM-এ মাঝখানে একটি <tbody> বসে যায় — ফলে table > tr selector কিছুই খুঁজে পায় না। <p>-এর ভেতরে <div> লিখলে parser আগেই <p> বন্ধ করে দেয়। তাই নিয়মটি মনে রাখুন: JavaScript যা দেখে, তা DevTools-এর Elements ট্যাব — Page Source নয়।

document — গাছের মাথা আর প্রবেশপথ

ব্রাউজার প্রতিটি পাতার জন্য একটি document object দেয় — একটি global variable, যা গাছের শিকড়। document.documentElement হলো <html>, document.head আর document.body নামেই পাওয়া যায়, document.title পাতার শিরোনাম। মনে রাখুন, DOM JavaScript ভাষার অংশ নয় — এটি ব্রাউজারের দেওয়া একটি Web API. তাই Node.js-এ document নেই; সার্ভারে HTML পড়তে হলে jsdom-এর মতো library লাগে।

২ · element খোঁজা — id, CSS selector, closest আর matches

কিছু বদলানোর আগে সেটিকে খুঁজে পেতে হবে। পুরোনো আর নতুন মিলিয়ে কয়েকটি উপায় আছে, তবে আজকের কোডে তিনটিই প্রায় সব কাজ সারে:

Methodকী দেয়না পেলেতালিকা কেমন
document.getElementById('cart')ওই id-এর elementnull—
el.querySelector('.card .price')প্রথম মিলnull—
el.querySelectorAll('li.hot')সব মিলখালি NodeListstatic — সেই মুহূর্তের ছবি
el.getElementsByClassName('item')সব মিলখালি HTMLCollectionlive — নিজে থেকে বদলায়
el.getElementsByTagName('li')সব মিলখালি HTMLCollectionlive

querySelector আর querySelectorAll যেকোনো CSS selector বোঝে — #id, .class, [data-id="3"], ul > li:first-child, এমনকি :has(). তাই CSS জানলে খোঁজার নতুন ভাষা শিখতে হয় না। দুটি ব্যবহারিক বিষয় খেয়াল করুন। প্রথমত, এগুলো যেকোনো element-এর ওপর ডাকা যায় — card.querySelector('.price') শুধু ওই কার্ডের ভেতরে খোঁজে, পুরো পাতায় নয়; এতে ভুল element ধরার ঝুঁকি কমে, খোঁজাও দ্রুত হয়। দ্বিতীয়ত, না পেলে null আসে — তাই document.querySelector('#badge').textContent = 3 লাইনটি id-তে বানান ভুল থাকলে TypeError দেবে। যেখানে element না থাকাও স্বাভাবিক, সেখানে ?. (পর্ব ০৮) বা একটি if ব্যবহার করুন।

querySelectorAll যা দেয়, সেটি একটি NodeList — array নয়, কিন্তু length, index, forEach আছে আর for…of চলে। map বা filter চাইলে [...list] বা Array.from(list) করে নিন (পর্ব ১৫, ১৭)।

closest আর matches — ওপরের দিকে খোঁজা আর পরীক্ষা করা

el.closest('.card') element থেকে শুরু করে ওপরের দিকে (নিজে, তারপর parent, তারপর তার parent…) প্রথম যেটি selector মেলে, সেটি দেয়; না পেলে null. একটি কার্ডের ভেতরের বোতাম চাপা হলে “কোন কার্ড?” জানার এটিই সবচেয়ে পরিষ্কার উপায় — পর্ব ৩৮-এর event delegation-এর ভিত্তিও এটি। el.matches('.sold-out') শুধু হ্যাঁ/না বলে: এই element কি selector-টি মেলে? দুটোই একই CSS selector-এর ভাষায় কথা বলে।

getElementById কি এখনো দরকার? হ্যাঁ, ছোট একটি জায়গায়। এটি সবচেয়ে দ্রুত (ব্রাউজার id-এর একটি সূচি রাখে) আর selector-এর বিশেষ অক্ষরের ঝামেলা নেই: id যদি "1st-item" হয়, querySelector('#1st-item') SyntaxError দেয় (CSS-এ id সংখ্যা দিয়ে শুরু করা যায় না, escape লাগে), কিন্তু getElementById('1st-item') ঠিকই কাজ করে। ব্যবহারকারীর দেওয়া মান দিয়ে selector বানাতে হলে CSS.escape(value) ব্যবহার করুন।

৩ · live আর static তালিকা, আর গাছে চলাফেরা

টেবিলের শেষ কলামটি নিরীহ দেখায়, কিন্তু এর পেছনে DOM-এর সবচেয়ে বিখ্যাত বাগগুলোর একটি লুকিয়ে আছে। querySelectorAll যে তালিকা দেয়, সেটি ডাকার মুহূর্তের একটি ছবি — পরে DOM বদলালেও তালিকা বদলায় না। কিন্তু getElementsByClassName আর getElementsByTagName যে HTMLCollection দেয়, সেটি live: প্রতিবার পড়ার সময় গাছ থেকে নতুন করে হিসাব হয়। এখন ধরুন, একটি ফিল্টার সব পণ্য থেকে item class সরাতে চায়:

1const live = document.getElementsByClassName('item');2for (let i = 0; i < live.length; i++) {3  live[i].classList.remove('item');4}5// ঠিক: const snap = document.querySelectorAll('.item');6//       for (const el of snap) el.classList.remove('item');
ভ্যারিয়েবলের মান
i
live (এখন যারা .item)
live.length
এই ধাপে বদলাল
  1. তালিকা নেওয়া। live-এ তিনটি element: A, B, C. এটি কোনো array-তে কপি নয় — এটি একটি জানালা, যা দিয়ে প্রতিবার গাছের দিকে তাকিয়ে “এখন কারা .item” দেখা হয়।
  2. i = 0. live[0] হলো A; তার class সরল। সাথে সাথে A আর শর্ত মেলে না, তাই জানালা থেকে উধাও। এখন live = [B, C], দৈর্ঘ্য ২ — সবাই এক ঘর বাঁয়ে সরে গেছে।
  3. i = 1. live[1] এখন B নয়, C! C থেকে class সরল। এখন live = [B]. B-কে লাফিয়ে পেরিয়ে গেছি — ঠিক যেমন একটি array থেকে loop চলাকালীন splice করলে হয় (পর্ব ১২)।
  4. i = 2. শর্ত 2 < 1 মিথ্যা — loop শেষ। ফল: তিনটির মধ্যে একটি (B) এখনো .item. কোনো error নেই, শুধু ভুল ফল — এজন্যই বাগটি ধরা কঠিন। জোড় সংখ্যক element থাকলে অর্ধেক বাদ পড়ে।
  5. সমাধান। querySelectorAll-এর static তালিকা নিন — ডাকার মুহূর্তের ছবি, তাই loop-এর মাঝে বদলায় না। অন্য উপায়: [...live] দিয়ে আগে কপি, বা শেষ থেকে উল্টো দিকে loop. মনে রাখবেন, childNodes-ও একটি live NodeList; শুধু querySelectorAll-এর NodeList static.

গাছে চলাফেরা — parent, children আর sibling

একটি element হাতে থাকলে তার আশেপাশে যাওয়াকে বলে DOM traversal. element-কেন্দ্রিক property-গুলো মনে রাখুন: parentElement (বাবা-মা), children (সন্তানেরা, live HTMLCollection), firstElementChild/lastElementChild, আর nextElementSibling/previousElementSibling (ভাই-বোন)। এদের “Element”-হীন যমজ (childNodes, firstChild, nextSibling) text আর comment node-ও গোনে — ধাপ ১-এর স্টেপারে দেখেছেন, সেখানে প্রথম সন্তানই ছিল ফাঁকা জায়গার একটি text node. পুরোনো টিউটোরিয়াল বা AI-এর কোডে el.firstChild.textContent দেখলে সতর্ক হোন — HTML-এ একটি নতুন লাইন যোগ হলেই এটি ভেঙে যায়।

এবার সব এক জায়গায় চালিয়ে দেখুন। নিচের কোড চালালে ওপরে তিনটি পণ্যের তালিকা আর একটি ইনপুট বক্স দেখা যাবে; কোডটি node গোনে, গাছে চলাফেরা করে, live তালিকার ফাঁদটি নিজের চোখে দেখায়, আর শেষে attribute ও property-র পার্থক্যের একটি ঝলক দেয় (অংশ ৫-এ বিস্তারিত):

const ul = document.getElementById('products');
console.log('childNodes:', ul.childNodes.length, '| children:', ul.children.length);
const first = ul.firstChild;                            // প্রথম node — কোনো li নয়!
console.log('firstChild:', first.nodeType, JSON.stringify(first.textContent));
const second = ul.firstElementChild.nextElementSibling;
console.log(second.textContent, second.dataset.price, second.closest('ul').id, second.matches('.item'));

const live = document.getElementsByClassName('item');   // live HTMLCollection
const snap = document.querySelectorAll('.item');        // static NodeList
for (let i = 0; i < live.length; i++) live[i].classList.remove('item');
console.log('ভুল loop-এর পরে live:', live.length, '| snap:', snap.length);
for (const li of snap) li.classList.add('item');        // স্থির ছবি ধরে চললে নিরাপদ
console.log('ঠিক loop-এর পরে live:', live.length);

const qty = document.querySelector('#qty');
qty.value = '3';                                        // যেন ব্যবহারকারী 3 লিখলেন
console.log('property:', qty.value, '| attribute:', qty.getAttribute('value'));

প্রথম লাইন childNodes: 7 | children: 3 — তিনটি li আর তাদের আগে-পরে চারটি ফাঁকা জায়গার text node. দ্বিতীয় লাইন firstChild: 3 "\n ": প্রথম node-এর nodeType 3 (text), আর তার লেখা একটি নতুন লাইন ও দুটি space — JSON.stringify দিয়ে ছাপায় অদৃশ্য অক্ষরগুলো দেখা যাচ্ছে। তৃতীয় লাইন খাদি পাঞ্জাবি 1800 products true: দ্বিতীয় element-এ পৌঁছে তার লেখা, data-price (dataset দিয়ে, string হিসেবে), ওপরের ul-এর id আর selector মেলার উত্তর। তারপর ফাঁদ: ভুল loop-এর পরে live: 1 | snap: 3 — তিনটির একটি বাদ পড়েছে, অথচ স্থির তালিকায় তিনটিই আছে। স্থির তালিকা ধরে আবার class যোগ করলে ঠিক loop-এর পরে live: 3 — live তালিকা নিজে থেকেই আবার তিনটি দেখায়, ওপরের পাতায় তিনটি নামই সবুজ। শেষ লাইন property: 3 | attribute: 1 — বক্সে এখন ৩, কিন্তু HTML-এ লেখা value="1" বদলায়নি। কেন, তা অংশ ৫-এ।

৪ · লেখা পড়া ও বদলানো — textContent, innerText আর innerHTML

element-এর ভেতরের জিনিস বদলানোর তিনটি জনপ্রিয় property আছে। নাম কাছাকাছি, কিন্তু আচরণ — আর ঝুঁকি — একেবারে আলাদা।

📝 textContent — নিছক লেখা

পড়লে: ভেতরের সব text node-এর লেখা জোড়া লাগিয়ে দেয় — লুকানো element-এরও। লিখলে: সব সন্তান মুছে একটি text node বসায়। < এখানে শুধু একটি অক্ষর। দ্রুত, নিরাপদ — ডিফল্ট পছন্দ।

👁️ innerText — যা চোখে দেখা যায়

CSS মেনে চলে: display: none অংশ বাদ দেয়, <br> আর block-কে নতুন লাইন বানায়। এ জন্য পড়ার সময় ব্রাউজারকে layout হিসাব করতে হয় — তাই innerText ধীর, আর loop-এ পড়লে বিপদ (অংশ ৭)।

🧩 innerHTML — HTML হিসেবে পড়া

লিখলে: দেওয়া string-কে HTML parser দিয়ে পড়ে নতুন element বানায়। নিজের লেখা, নিশ্চিত markup-এ সুবিধাজনক; কিন্তু অন্যের দেওয়া লেখায় কখনো নয়। পুরোনো সন্তান আর তাদের listener-ও মুছে যায়।

শেষ কার্ডের সতর্কবার্তাটি কেন এত জোরালো, ধাপে ধাপে দেখুন। ধরুন একটি পণ্যের রিভিউ বক্সে কেউ নাম হিসেবে লিখলেন <img src=x onerror=…> (“…”-এর জায়গায় কোনো কোড)। আপনার কোড সেই নাম পাতায় দেখায়:

  1. ইনপুটnameঅন্যের দেওয়া string
  2. innerHTMLHTML parserনতুন element তৈরি
  3. onerrorattribute → codeপাতার ক্ষমতায় চলে
  4. textContentএকটি text nodeশুধু লেখা
  5. নিয়মtext আগেHTML লাগলে sanitize
ধাপ ১ডেটা আসে বাইরে থেকে
const name = '<img src=x onerror=…>';   // রিভিউ ফর্ম, URL, API — যেকোনো জায়গা
ধাপ ২string হয়ে গেল element
box.innerHTML = `<p>${name}</p>`;   // ✘
DOM: p
     └─ img  src="x"  onerror="…"   ← আসল element!
ধাপ ৩কেন বিপজ্জনক
ব্রাউজার "x" ছবি নামাতে চায় → ব্যর্থ → error event
→ onerror-এর লেখা কোড হিসেবে চলে
  আপনার পাতার সব ক্ষমতা নিয়ে: DOM, ব্যবহারকারীর session, API কল
ধাপ ৪একই ইনপুট, নিরাপদ পথ
p.textContent = name;   // ✔
DOM: p
     └─ #text "<img src=x onerror=…>"
পর্দায় হুবহু লেখাটি দেখা যায়; কোনো element নেই, কিছু চলে না
ধাপ ৫প্রতিরক্ষার ক্রম
১. লেখা দেখাতে: textContent, createElement + append
২. সত্যিই HTML দরকার (যেমন rich text): আগে sanitize
   (DOMPurify, বা যেখানে সমর্থিত — setHTML / Sanitizer API)
৩. সাথে CSP — পর্ব ৪৩
  1. ডেটা বাইরে থেকে আসে। নাম, মন্তব্য, URL-এর ?q=, API-র উত্তর — এগুলো আপনার লেখা নয়। নিয়ম: এগুলোকে সবসময় লেখা হিসেবে ভাবুন, কখনো markup হিসেবে নয়।
  2. innerHTML string-কে HTML parser-এ পাঠায়। Template literal দিয়ে জোড়া লাগানো string-এ ব্যবহারকারীর লেখা ঢুকেছে, আর parser তার ভেতরের <img …>-কে একটি আসল element বানিয়েছে — সাথে তার সব attribute.
  3. event handler attribute-ও কোড। src="x" কোনো ছবি নয়, তাই নামানো ব্যর্থ হয়, আর onerror-এর লেখা আপনার পাতার কোড হিসেবে চলে — আপনার ব্যবহারকারীর পাতায়, তাঁর লগইন অবস্থায়। এটিই XSS. (মজার তথ্য: innerHTML দিয়ে ঢোকানো <script> ট্যাগ চলে না — তাই আক্রমণে এমন attribute-ই বেশি দেখা যায়।) এই পাতায় আমরা কিছুই চালাচ্ছি না — শুধু বোঝার জন্য দেখানো।
  4. textContent কোনো parser ডাকে না। পুরো string একটি text node-এ যায়; < একটি সাধারণ অক্ষর। পাঠক পর্দায় হুবহু লেখাটি দেখেন — দেখতে অদ্ভুত, কিন্তু সম্পূর্ণ নিরীহ। কোনো escape function লিখতে হয়নি; DOM API নিজেই নিরাপদ।
  5. প্রতিরক্ষার ক্রম। প্রথমে text-ভিত্তিক API. সত্যিই HTML দেখাতে হলে (যেমন একটি blog editor-এর লেখা) একটি পরীক্ষিত sanitizer দিয়ে বিপজ্জনক অংশ ছেঁটে নিন — নিজে regex লিখে নয়। পুরো নিরাপত্তা-চিত্র — CSP, Trusted Types — পর্ব ৪৩-এ।

setHTML আর Sanitizer API সম্পর্কে সাবধানে বলি। ব্রাউজারে একটি নিজস্ব sanitizer আনার কাজ চলছে — element.setHTML(string), যা HTML বসানোর আগে নিজেই বিপজ্জনক অংশ বাদ দেবে। মানদণ্ডটি কয়েক বছরে কয়েকবার নতুন করে লেখা হয়েছে, আর সব ব্রাউজারে এখনো সমানভাবে আসেনি। তাই ব্যবহারের আগে সমর্থন যাচাই করুন (MDN বা caniuse), আর ততদিন পরীক্ষিত library DOMPurify সবচেয়ে প্রচলিত পথ। সবচেয়ে ভালো sanitizer হলো যেটি দরকারই হয় না — অর্থাৎ textContent.

একটি বাস্তব উদাহরণ: খোঁজার শব্দটি ফলাফলে হলুদ করে দেখানো। সহজ (আর ভুল) পথ হলো name.replace(q, `<mark>${q}</mark>`) করে innerHTML-এ বসানো — তাহলে পণ্যের নাম আর খোঁজার শব্দ, দুটোই HTML হিসেবে পড়া হয়, অথচ শব্দটি ব্যবহারকারী টাইপ করেছেন। নিরাপদ পথ: নামটিকে তিন টুকরো করুন — মিলের আগে, মিল, মিলের পরে — আর প্রথম ও শেষ টুকরো text হিসেবে, মাঝেরটি একটি <mark> element-এর textContent হিসেবে বসান। অংশ ৬-এর কোডে ঠিক এটিই করা হয়েছে।

৫ · attribute বনাম property, dataset, classList আর style

HTML-এর attribute আর object-এর property — দুটি আলাদা জগৎ

HTML-এ লেখা <input id="qty" value="1">-এর value="1" হলো একটি attribute — markup-এর অংশ, সবসময় string. Parser যখন এখান থেকে element object বানায়, তখন object-এ DOM property তৈরি হয় — যেমন input.value, input.id, input.checked. বেশিরভাগ ক্ষেত্রে দুটি একে অপরকে প্রতিফলিত করে: el.id = 'x' করলে attribute-ও বদলায়। কিন্তু কিছু ক্ষেত্রে নয়, আর সবচেয়ে গুরুত্বপূর্ণ ব্যতিক্রম ফর্মের ঘর:

  • value attribute = শুরুর (default) মান। getAttribute('value') আর input.defaultValue এটি দেয়।
  • value property = এই মুহূর্তের মান। ব্যবহারকারী টাইপ করলে বা কোড input.value = '3' লিখলে শুধু এটি বদলায় — ওপরের কোডের শেষ লাইন ঠিক এটিই দেখিয়েছে। ফর্ম reset করলে property আবার attribute-এর মানে ফেরে।
  • টাইপও আলাদা হতে পারে। checkbox.checked একটি boolean, কিন্তু attribute checked-এর শুধু থাকা-না-থাকা আছে। a.href property পূর্ণ URL দেয় (https://…/cart), getAttribute('href') দেয় যা লেখা ছিল (/cart).

নিয়মটি সরল: অবস্থা পড়তে-লিখতে property, markup-এ যা লেখা বা নিজের বানানো attribute-এ getAttribute/setAttribute. ফর্মের পুরো গল্প পর্ব ৩৯-এ।

dataset — নিজের ডেটা element-এ বেঁধে রাখা

HTML নিজের তথ্য রাখার জন্য data- দিয়ে শুরু হওয়া attribute অনুমোদন করে: <li data-id="7" data-unit-price="450">. JavaScript-এ এগুলো পাওয়া যায় el.dataset দিয়ে — li.dataset.id, আর হাইফেন-নাম camelCase হয়ে li.dataset.unitPrice. মান সবসময় string; হিসাবের আগে Number() করুন। কোন বোতাম কোন পণ্যের, কোন সারি কোন id-এর — এমন ছোট পরিচয় রাখার জন্য dataset আদর্শ। তবে পুরো অ্যাপের state DOM-এ রাখবেন না; state থাকুক JavaScript-এর data-তে, DOM শুধু তার প্রতিচ্ছবি (পর্ব ২১-এর “পাতা = f(state)”).

classList বনাম style — চেহারা বদলানোর দুই পথ

একটি পণ্য স্টক শেষ হলে ধূসর আর কাটা দাগসহ দেখাতে চান। দুই উপায়:

// ✘ inline style: চেহারার নিয়ম JavaScript-এ ছড়িয়ে পড়ে
li.style.opacity = '0.5';
li.style.textDecoration = 'line-through';

// ✔ class: চেহারা CSS-এ, JavaScript শুধু অবস্থা জানায়
li.classList.toggle('sold-out', product.stock === 0);   // শর্ত সত্য হলে যোগ, মিথ্যা হলে বাদ

classList-এর চারটি method মুখস্থ রাখুন: add, remove, toggle (দ্বিতীয় argument দিলে জোর করে যোগ বা বাদ), আর contains. class-এর পথ ভালো কেন? চেহারার সব নিয়ম এক জায়গায় (CSS ফাইলে) থাকে, ডিজাইনার JavaScript না ছুঁয়েই রং বদলাতে পারেন, dark mode বা media query স্বাভাবিকভাবে কাজ করে, আর .sold-out নামটি নিজেই বলে দেয় অবস্থাটি কী। inline style-এর জায়গা আছে শুধু সত্যিকারের গতিশীল মানে — একটি progress bar-এর width: 37%, বা টেনে আনা জিনিসের অবস্থান। সেখানেও CSS custom property ভালো বিকল্প: bar.style.setProperty('--done', '37%'), আর বাকি নিয়ম CSS-এ।

৬ · নতুন element বানানো, বসানো আর data থেকে তালিকা আঁকা

তৈরি আর বসানোর টুলবক্স

document.createElement('li') একটি নতুন element বানায় — কিন্তু এটি তখনো গাছের বাইরে, পর্দায় নেই। গাছে বসানোর আধুনিক method-গুলো একটি নির্দিষ্ট element-এর সাপেক্ষে কাজ করে, আর সবগুলো একসাথে একাধিক node বা string নেয় (string গেলে সেটি text node হয় — HTML নয়):

Methodকোথায় বসায়মনে রাখার উপায়
parent.append(a, b)ভেতরে, শেষেতালিকার নিচে নতুন সারি
parent.prepend(a)ভেতরে, শুরুতেনোটিশ বোর্ডের সবার ওপরে
el.before(a) / el.after(a)বাইরে, আগে / পরে (sibling হিসেবে)লাইনে কারও আগে-পরে দাঁড়ানো
el.replaceWith(a)নিজের জায়গায় অন্য nodeখেলোয়াড় বদল
el.remove()গাছ থেকে সরিয়ে দেয়—
parent.replaceChildren(...nodes)সব সন্তান মুছে নতুনগুলোপুরো তালিকা নতুন করে আঁকা

পুরোনো কোডে appendChild, insertBefore, removeChild দেখবেন — এগুলো একবারে একটিমাত্র node নেয় আর string নেয় না; নতুন কোডে ওপরের method-গুলোই পরিষ্কার। একটি সূক্ষ্ম নিয়ম: একটি node গাছে একবারই থাকতে পারে। ইতিমধ্যে গাছে থাকা node-কে append করলে সেটি কপি হয় না, সরে যায়।

কপি দরকার হলে node.cloneNode(true) — true মানে পুরো শাখাসহ (deep) কপি। খেয়াল রাখুন, addEventListener দিয়ে লাগানো listener কপি হয় না, আর id থাকলে সেটিও কপি হয় — একই id দুবার থাকলে পাতা অবৈধ হয়ে যায়।

insertAdjacentHTML(position, html) চার জায়গার একটিতে ('beforebegin', 'afterbegin', 'beforeend', 'afterend') HTML string বসায়। innerHTML += …-এর চেয়ে এটি ভালো, কারণ বিদ্যমান সন্তানদের মুছে নতুন করে বানায় না (+= পুরো ভেতরটা আবার parse করে, পুরোনো listener আর ইনপুটের লেখা হারায়)। কিন্তু এটিও HTML parse করে — তাই innerHTML-এর সব নিরাপত্তা-সতর্কতা এখানেও খাটে।

<template> — বারবার আসা markup-এর ছাঁচ

একটি নোটিশ বোর্ডের প্রতিটি নোটিশে শিরোনাম, তারিখ আর একটি “পড়েছি” বোতাম আছে। প্রতিবার createElement দিয়ে পাঁচটি element বানানো ক্লান্তিকর, আর innerHTML-এর string-এ ব্যবহারকারীর লেখা ঢোকানো বিপজ্জনক। মাঝামাঝি সমাধান <template>:

<template id="notice-tpl">
  <article class="notice">
    <h3 class="title"></h3>
    <time class="date"></time>
    <button type="button">পড়েছি</button>
  </article>
</template>

<template>-এর ভেতরের জিনিস ব্রাউজার parse করে, কিন্তু পর্দায় দেখায় না, ভেতরের ছবি নামায় না, script চালায় না — একটি নিষ্ক্রিয় ছাঁচ। tpl.content একটি DocumentFragment; প্রতিটি নোটিশের জন্য tpl.content.firstElementChild.cloneNode(true) করে কপি নিন, তারপর querySelector('.title').textContent = notice.title দিয়ে ফাঁকা ঘরগুলো ভরুন। Markup থাকল HTML-এ (যেখানে তা পড়া সহজ), ব্যবহারকারীর ডেটা গেল textContent দিয়ে (যেখানে তা নিরাপদ) — দুই দিকেরই সুবিধা।

DocumentFragment — অনেক node একবারে

DocumentFragment হলো একটি হালকা, গাছের বাইরের পাত্র। এতে ১০০টি <li> জমিয়ে তারপর একবার list.append(frag) করলে fragment নিজে গাছে ঢোকে না — তার সব সন্তান একসাথে সরে যায়, আর fragment খালি হয়ে যায়। এতে live DOM-এ একটিমাত্র পরিবর্তন হয়। (আধুনিক ব্রাউজারে এর গতি-সুবিধা কতটা, তা নিয়ে একটি সূক্ষ্ম সত্য আছে — অংশ ৭-এ।)

data থেকে তালিকা — পর্ব ২১-এর render নকশা, এবার পূর্ণ রূপে

পর্ব ২১-এর খরচের খাতায় নিয়ম ছিল “পাতা = f(state)” — data বদলালে পুরো তালিকা নতুন করে আঁকা। এবার সেই নকশায় এই পর্বের সব টুল এক জায়গায়: template থেকে কপি, dataset-এ id, textContent-এ নাম আর দাম, স্টক শেষ হলে class আর disabled বোতাম, খোঁজার শব্দে নিরাপদ <mark>, fragment-এ জমিয়ে একবারে বসানো, আর কার্টের ব্যাজে সংখ্যা। চালানোর পরে ওপরের পাতায় “কার্টে যোগ” বোতামগুলো নিজে চেপে দেখুন — ব্যাজ বাড়বে:

const products = [
  { id: 1, name: 'জামদানি শাড়ি', price: 4500, stock: 3 },
  { id: 2, name: 'খাদি পাঞ্জাবি', price: 1800, stock: 0 },
  { id: 3, name: 'নকশি কাঁথা', price: 3200, stock: 5 },
];
const [list, badge, row] = ['#list', '#badge', '#row'].map((s) => document.querySelector(s));
function withMark(text, q) {                  // শুধু text আর <mark> — HTML parse নেই
  const i = q ? text.indexOf(q) : -1;
  if (i < 0) return [text];
  const mark = Object.assign(document.createElement('mark'), { textContent: q });
  return [text.slice(0, i), mark, text.slice(i + q.length)];
}
function render(q = '') {
  const frag = document.createDocumentFragment();
  for (const p of products) {
    const li = row.content.firstElementChild.cloneNode(true);
    li.dataset.id = p.id;
    li.querySelector('.name').append(...withMark(p.name, q));
    li.querySelector('.price').textContent = `৳${p.price}`;
    li.classList.toggle('sold-out', p.stock === 0);
    li.querySelector('button').disabled = p.stock === 0;
    frag.append(li);
  }
  list.replaceChildren(frag);                 // live DOM-এ একবারই হাত
}
list.addEventListener('click', (e) => { if (e.target.closest('button')) badge.textContent = +badge.textContent + 1; });
render('কা');
for (const id of [1, 3]) list.querySelector(`[data-id="${id}"] button`).click();
console.log('li:', list.children.length, '| স্টক শেষ:', list.querySelectorAll('.sold-out').length, '| mark:', list.querySelector('mark').textContent, '| কার্ট:', badge.textContent);

আউটপুট li: 3 | স্টক শেষ: 1 | mark: কা | কার্ট: 2. তিনটি পণ্যই আঁকা হয়েছে; খাদি পাঞ্জাবির stock ০, তাই classList.toggle('sold-out', true) তাকে ধূসর আর কাটা দাগসহ করেছে, আর তার বোতাম disabled — কিবোর্ড আর screen reader-ও জানে যে এটি চাপা যায় না। খোঁজার শব্দ “কা” মিলেছে “নকশি কাঁথা”-য়; withMark নামটিকে তিন টুকরো করে মাঝেরটি একটি <mark> element-এ বসিয়েছে — কোথাও HTML string নেই, তাই শব্দটি <img …> হলেও শুধু লেখা হিসেবেই দেখা যেত। শেষে কোড নিজেই দুটি বোতামে click() চাপে, তাই ব্যাজ ২. লক্ষ করুন, তিনটি বোতামের প্রতিটিতে আলাদা listener নেই — পুরো তালিকায় একটি, আর closest('button') দিয়ে চেনা; এই কৌশলের (event delegation) নাম আর পুরো ব্যাখ্যা পর্ব ৩৮-এ। ব্যাজের aria-live="polite" screen reader-কে সংখ্যা বদলের খবর পড়ে শোনাতে বলে (অংশ ৮)।

৭ · পারফরম্যান্স — reflow, repaint আর layout thrashing

পর্ব ০২-এ দেখেছেন, DOM থেকে পর্দার পিক্সেল পর্যন্ত কয়েকটি ধাপ: style হিসাব → layout (প্রতিটি বাক্সের মাপ আর অবস্থান; একে reflow-ও বলে) → paint (পিক্সেলে রং, লেখা; বদল হলে বলে repaint) → composite. JavaScript DOM বদলালে কোন ধাপ আবার চলবে, তা নির্ভর করে কী বদলেছে তার ওপর: লেখা, মাপ বা font বদলালে layout থেকে শুরু; শুধু রং বদলালে layout বাদ, শুধু paint; আর transform বা opacity প্রায়ই শুধু composite.

ব্রাউজার এখানে বুদ্ধিমান: আপনার প্রতিটি লেখার সাথে সাথে layout করে না। সে পরিবর্তনগুলো “ময়লা” (dirty) চিহ্ন দিয়ে জমিয়ে রাখে, আর পরের frame আঁকার আগে একবার সব হিসাব করে। তাই একটি loop-এ ১০০টি append করলেও সাধারণত layout হয় একবার। সমস্যা শুরু হয় যখন আপনি লেখার পরেই মাপ পড়েন — offsetHeight, offsetWidth, getBoundingClientRect(), scrollTop, এমনকি innerText. সঠিক উত্তর দিতে ব্রাউজারকে তখনই, সাথে সাথে, জমানো সব layout করতে হয় — একে বলে forced synchronous layout. loop-এর প্রতিটি ঘুরে লেখা-পড়া-লেখা-পড়া হলে প্রতিবারই জোর করে layout — এটিই layout thrashing:

1// ✘ প্রতিটি ঘুরে পড়া, তারপর লেখা2for (const card of cards) {3  card.style.height = box.offsetHeight / 4 + 'px';4}5// ✔ আগে একবার পড়া, তারপর সব লেখা6const h = box.offsetHeight / 4;7for (const card of cards) card.style.height = h + 'px';
ব্রাউজারের খাতা
কোন কার্ড
layout-এর অবস্থা
জোর করে layout (মোট)
frame-এর শেষে layout
  1. প্রথম কার্ড। layout পরিষ্কার, তাই box.offsetHeight পড়া সস্তা — জমা উত্তর থেকেই আসে। তারপর কার্ডের height লেখা হলো; ব্রাউজার শুধু “layout ময়লা” চিহ্ন দেয়, এখনই কিছু হিসাব করে না।
  2. দ্বিতীয় কার্ড। আবার offsetHeight পড়া। কিন্তু layout ময়লা — হয়তো আগের কার্ডের উচ্চতা box-এর মাপ বদলেছে। ঠিক উত্তর দিতে ব্রাউজার তখনই, JavaScript থামিয়ে, পুরো layout করে। তারপর আবার লেখা, আবার ময়লা।
  3. ১০০ কার্ড শেষে। প্রায় প্রতিটি ঘুরে একবার জোর করা layout — বড় পাতায় প্রতিটি কয়েক মিলিসেকেন্ড হলে মোট শত মিলিসেকেন্ড। এ সময় main thread আটকে, ক্লিক সাড়া দেয় না, স্ক্রল ঝাঁকুনি দেয়। কোডে কোনো ভুল চোখে পড়ে না — শুধু ধীর।
  4. সমাধান — আগে পড়া। যা পড়ার, loop-এর আগে একবার পড়ে variable-এ রাখুন। layout পরিষ্কার থাকায় এটিও সস্তা।
  5. তারপর সব লেখা। loop-এ শুধু লেখা, কোনো পড়া নেই — ব্রাউজার শুধু ময়লা চিহ্ন জমায়, আর frame আঁকার আগে একবার layout করে। নিয়মটি: একসাথে সব পড়া, তারপর একসাথে সব লেখা (batch reads, then writes). বড় অ্যাপে পড়া আর লেখা আলাদা requestAnimationFrame পর্যায়ে ভাগ করা হয় — পর্ব ৩৬-এর rAF মনে করুন।

DocumentFragment নিয়ে সৎ কথা। পুরোনো লেখায় পড়বেন “fragment ছাড়া ১০০টি append মানে ১০০টি reflow”. আধুনিক ব্রাউজারে তা সত্য নয় — যতক্ষণ মাঝে কোনো মাপ পড়ছেন না, ১০০টি append-এর পরেও layout সাধারণত একবারই হয়। fragment-এর আসল সুবিধা অন্যখানে: live গাছে একবারই হাত দেওয়া (তাই MutationObserver বা framework-এর কাছে একটি পরিবর্তন), আর কোড পরিষ্কার রাখা — “আগে বানাও, পরে বসাও”। পারফরম্যান্সের প্রশ্নে অনুমান নয়, মাপুন: DevTools-এর Performance প্যানেলে বেগুনি “Layout” বার আর “Forced reflow” সতর্কবার্তা খুঁজুন (পর্ব ৫৩)।

আরও তিনটি সস্তা অভ্যাস: loop-এর ভেতরে বারবার document.querySelector না করে loop-এর আগে একবার খুঁজে রাখুন; হাজার হাজার সারির তালিকায় শুধু পর্দায় দেখা অংশ আঁকুন (virtualization) বা CSS content-visibility: auto দিন; আর অ্যানিমেশনে top/left-এর বদলে transform ব্যবহার করুন, যাতে layout ধাপটি বাদ যায়।

৮ · script কখন চলে, accessibility আর framework কখন

script যদি element খুঁজে না পায়

নতুনদের একটি চেনা কষ্ট: document.querySelector('#cart') দেয় null, অথচ HTML-এ #cart স্পষ্ট আছে। কারণ প্রায় সবসময় সময়: <head>-এ রাখা সাধারণ <script> parser থামিয়ে তখনই চলে, যখন <body> এখনো পড়াই হয়নি — গাছে তখনো #cart নেই। পর্ব ০২-এ দেখা সমাধানগুলো এখন DOM-এর চোখে দেখুন:

লেখার ধরনকখন চলেDOM তৈরি?
<script src> (head-এ)পৌঁছানোমাত্র, parser থামিয়েনা — শুধু ওপরের অংশ
<script src defer>HTML পড়া শেষে, লেখার ক্রমে, DOMContentLoaded-এর আগেহ্যাঁ
<script type="module">ডিফল্টেই defer-এর মতোহ্যাঁ
<script src async>নামা শেষ হলেই, ক্রম নিশ্চিত নয়নিশ্চিত নয়

দুটি event-ও চিনে রাখুন। DOMContentLoaded ঘটে যখন HTML পুরো পড়া শেষ, DOM তৈরি, আর defer script চলে গেছে — ছবি বা iframe নামা শেষ হওয়ার অপেক্ষা ছাড়াই। load (window-এ) ঘটে আরও পরে, যখন ছবি, stylesheet, iframe — সব নামা শেষ। DOM নিয়ে কাজের জন্য প্রথমটিই যথেষ্ট; ছবির আসল মাপ জানতে হলে দ্বিতীয়টি। আজকের সহজ পরামর্শ: script-এ defer বা type="module" দিন, তাহলে আলাদা করে অপেক্ষা করতে হয় না।

accessibility — DOM বদলালে সবার কাছে পৌঁছায় কি?

আপনার DOM শুধু চোখ দিয়ে দেখা হয় না। Screen reader একই গাছ থেকে একটি accessibility tree বানিয়ে পড়ে শোনায়; কিবোর্ড ব্যবহারকারী Tab চেপে চলেন। তাই তিনটি নিয়ম:

  • চাপার জিনিস হলে <button>, <div> নয়। button নিজে থেকেই Tab-এ আসে, Enter আর Space-এ চাপা যায়, screen reader বলে “বোতাম”, disabled বোঝে। <div onclick>-এ এর কিছুই নেই — সব হাতে বানাতে হয়, আর প্রায়ই অর্ধেক বানানো হয়। এটিই semantic HTML-এর শক্তি।
  • নিজে থেকে বদলানো লেখা জানিয়ে দিন। কার্টের ব্যাজ বা “সেভ হয়েছে” বার্তা নিঃশব্দে বদলালে screen reader ব্যবহারকারী জানতেই পারেন না। পাত্রটিতে aria-live="polite" দিন — পাত্রটি শুরু থেকেই DOM-এ থাকুক, শুধু তার লেখা বদলান।
  • ARIA শেষ উপায়, প্রথম নয়। ARIA attribute (role, aria-label, aria-expanded) নিজস্ব widget-এর অর্থ জানায়, কিন্তু আচরণ যোগ করে না। প্রথম নিয়ম: যে HTML element কাজটি নিজে পারে, সেটিই ব্যবহার করুন।

framework কখন সাহায্য করে

এই পর্বের render নকশা ছোট পাতায় চমৎকার। কিন্তু অ্যাপ বড় হলে কয়েকটি সমস্যা দেখা দেয়: প্রতিবার পুরো তালিকা নতুন করে আঁকলে ইনপুটে লেখা অর্ধেক কথা, focus আর scroll-এর অবস্থান হারায়; শুধু যা বদলেছে তা হাতে হাতে খুঁজে বদলানো (একে বলে imperative update) কোডকে জটিল করে; আর state ও DOM আলাদা হয়ে যাওয়ার বাগ বাড়ে। React, Vue, Svelte-এর মতো declarative UI framework ঠিক এই কাজটি নেয়: আপনি বলেন “এই state-এ পাতা এমন দেখাবে”, framework হিসাব করে DOM-এ ন্যূনতম কী বদলাতে হবে। কিন্তু তারা সবাই শেষ পর্যন্ত এই পর্বের createElement, textContent, classList-ই ডাকে — DOM বোঝা ছাড়া framework-এর বাগ বোঝা যায় না। কোনটি কখন, তা পর্ব ৫৪-এ।

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

Desktop GUI-তেও একই ধারণা: Java Swing-এ একটি JFrame-এর ভেতরে JPanel, তার ভেতরে JButton — একটি component tree; JavaFX একে সরাসরি “scene graph” বলে, আর Python-এর Tkinter-এ প্রতিটি widget-এর একটি parent থাকে। এই গঠন Composite design pattern-এর উদাহরণ (Java-র design pattern পর্ব)। বড় পার্থক্য হলো, DOM শুরু হয় একটি লেখা (HTML) থেকে, আর যেকোনো সময় selector দিয়ে খোঁজা যায়। Python-এ সার্ভার থেকে নামানো HTML পড়ার BeautifulSoup-ও একই কাজ করে — HTML থেকে গাছ বানিয়ে select()-এ CSS selector দিয়ে খোঁজে (Python-এর HTTP ও web scraping পর্ব); তবে সেটি একটি স্থির ছবি, ব্রাউজারের মতো জীবন্ত নয়।

পরীক্ষা ও ইন্টারভিউতে
  • DOM কী, আর HTML-এর সাথে পার্থক্য? — HTML লেখা; DOM হলো ব্রাউজারের বানানো সেই লেখার জীবন্ত object-tree, যা JavaScript পড়ে ও বদলায়।
  • textContent বনাম innerText বনাম innerHTML? — নিছক লেখা; CSS মেনে দৃশ্যমান লেখা (layout লাগে, ধীর); HTML হিসেবে parse (অবিশ্বস্ত ডেটায় XSS).
  • NodeList বনাম HTMLCollection? — querySelectorAll-এর NodeList static, যেকোনো node রাখতে পারে; getElementsBy…-এর HTMLCollection live, শুধু element.
  • children আর childNodes-এর length আলাদা কেন? — childNodes-এ ফাঁকা জায়গার text node আর comment-ও থাকে।
  • Reflow আর repaint-এর পার্থক্য, layout thrashing কী? — মাপ-অবস্থান হিসাব বনাম শুধু আঁকা; লেখার পরে মাপ পড়লে জোর করা layout, loop-এ বারবার — সমাধান আগে পড়া, পরে লেখা।

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

“একটি product list বানিয়ে দাও” বললে AI কয়েক সেকেন্ডে কাজ করা কোড দেয় — আর সেই কোড ডেমোতে প্রায় সবসময় ঠিক দেখায়। কারণ DOM কোডের সবচেয়ে বড় ভুলগুলো ডেমোতে ধরা পড়ে না: নিরাপত্তার ভুল ধরা পড়ে যখন কেউ ইচ্ছা করে অদ্ভুত লেখা পাঠায়, accessibility-র ভুল ধরা পড়ে যখন একজন screen reader ব্যবহারকারী আসেন, আর গতির ভুল ধরা পড়ে হাজার সারিতে, দুর্বল ফোনে। AI-এর কোডে যে পাঁচটি ভুল বারবার দেখা যায়:

AI-এর সাধারণ ভুলফলঠিক রূপ
ব্যবহারকারী বা API-র ডেটা template literal-এ জুড়ে innerHTML-এXSS — অন্যের কোড আপনার পাতায় চলেtextContent, template + textContent; HTML লাগলে sanitizer
<div onclick> বা <span>-কে বোতাম বানানোকিবোর্ড ও screen reader-এ অচল<button type="button">
el.style.color = … দিয়ে অবস্থা দেখানোচেহারার নিয়ম JS-এ ছড়ানো, theme ভাঙেclassList.toggle('sold-out', cond), নিয়ম CSS-এ
loop-এর ভেতরে বারবার document.querySelector, innerHTML +=বৃথা খোঁজা, প্রতিবার পুরো ভেতর আবার parseআগে একবার খোঁজা, fragment-এ জমিয়ে একবার বসানো
loop-এ লেখার পরপর offsetHeight পড়াlayout thrashing, ঝাঁকুনিআগে সব পড়া, পরে সব লেখা

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

ভ্যানিলা JavaScript-এ (কোনো framework নয়) একটি পণ্যের তালিকা আঁকার কোড লিখে দাও। ডেটা: [{ id, name, price, stock }] array. নিয়ম: ১. কোথাও innerHTML, insertAdjacentHTML বা outerHTML-এ ডেটা ঢোকাবে না — নাম, দাম সব textContent দিয়ে। markup-এর জন্য একটি <template> ব্যবহার করো। ২. প্রতিটি সারিতে <button type="button"> থাকবে; div বা span-কে বোতাম বানাবে না। stock 0 হলে বোতাম disabled আর li-তে classList.toggle দিয়ে "sold-out" class — কোনো inline style নয়। ৩. সব li একটি DocumentFragment-এ জমিয়ে replaceChildren দিয়ে একবারে বসাবে; loop-এর ভেতরে document.querySelector ডাকবে না। ৪. কোথাও লেখার পরে offsetHeight বা getBoundingClientRect পড়বে না; মাপ লাগলে আগে পড়ো। ৫. কার্টের সংখ্যা দেখানোর span-এ aria-live="polite" দাও; একটি listener দিয়ে (closest ব্যবহার করে) সব বোতাম সামলাও। ৬. শেষে বলো: নামে "<img src=x onerror=…>" থাকলে তোমার কোড কী দেখাবে, আর কেন সেটি নিরাপদ।

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

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

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

প্র ০১১৯৯০-এর দশকের “browser war”-এ একই পাতা দুই ব্রাউজারে দুইভাবে বদলাতে হতো কেন, আর ১৯৯৮ সালের W3C DOM Level 1 কী সমাধান দিল?

১৯৯৫ সালে Netscape Navigator-এ JavaScript আসে, আর পরের বছর Microsoft Internet Explorer 3-এ নিজের সংস্করণ JScript আনে। প্রথম দিকে পাতার সাথে কথা বলার উপায় ছিল খুব সীমিত — ফর্ম, ছবি আর লিংকের কয়েকটি তালিকা (document.forms, document.images). পরে এই অনানুষ্ঠানিক ব্যবস্থাকে “DOM Level 0” বলা হয়। ১৯৯৭ সালে দুই কোম্পানি চতুর্থ সংস্করণে “Dynamic HTML” (DHTML) নামে পাতা বদলানোর ক্ষমতা আনে — কিন্তু সম্পূর্ণ আলাদা নকশায়। Netscape 4-এ ছিল document.layers আর একটি বিশেষ <layer> ট্যাগ; Internet Explorer 4-এ ছিল document.all, যা দিয়ে পাতার প্রায় যেকোনো element ধরা যেত, আর innerHTML-এর মতো property.

ফল ছিল ডেভেলপারদের জন্য দুঃস্বপ্ন। প্রায় প্রতিটি স্ক্রিপ্টের শুরুতে লেখা থাকত “if (document.layers) { … } else if (document.all) { … }” — একই কাজ দুইবার, দুই ভাষায়। অনেক সাইট লিখত “Best viewed in Internet Explorer”. এই বিভক্তি ওয়েবকে কয়েকটি কোম্পানির নিয়ন্ত্রণে চলে যাওয়ার ঝুঁকিতে ফেলেছিল।

W3C (World Wide Web Consortium) এর জবাবে একটি নিরপেক্ষ, ভাষা-স্বাধীন মানদণ্ড লেখে। DOM Level 1 ১৯৯৮ সালের অক্টোবরে W3C Recommendation হয়। এর মূল ধারণাই আজকের পর্বের ভিত্তি: ডকুমেন্ট হলো node-এর একটি গাছ; প্রতিটি node-এর ধরন (nodeType), parent, child আর sibling আছে; getElementsByTagName, createElement, appendChild, getAttribute — সবার জন্য এক। লক্ষ করুন, মানদণ্ডটি JavaScript-এর জন্য আলাদা নয়; Java-র মতো অন্য ভাষার জন্যও একই interface লেখা হয়েছিল, তাই XML পড়ার Java কোডেও “DOM parser” কথাটি শোনেন। ২০০০ সালে DOM Level 2 আনে addEventListener আর CSS-এর সাথে সংযোগ, ২০০৪ সালে Level 3. পরে WHATWG মানদণ্ডটিকে একটি “living standard” হিসেবে নিয়মিত হালনাগাদ শুরু করে, যা আজও চলছে।

মানদণ্ড থাকলেই সবাই মানে না — Internet Explorer বহু বছর addEventListener-এর বদলে নিজের attachEvent রেখেছিল (IE9-এ এসে মানদণ্ড মানে)। তবু দিকটি ঠিক হয়ে গিয়েছিল: ব্রাউজার যা-ই বানাক, একটি সাধারণ চুক্তি আছে, যার বিরুদ্ধে তাকে মাপা যায়।

মূল উপলব্ধি: DOM কোনো একটি কোম্পানির আবিষ্কার নয়, একটি যুদ্ধবিরতির চুক্তি। আজ যে কোড সব ব্রাউজারে একইভাবে চলে, তা সম্ভব হয়েছে কারণ একটি নিরপেক্ষ সংস্থা “পাতা মানে node-এর গাছ” — এই একটি মডেলে সবাইকে রাজি করিয়েছিল।

প্র ০২২০০৬ সালের jQuery একসময় প্রায় প্রতিটি সাইটে ছিল। querySelectorAll আর আধুনিক DOM API কীভাবে তাকে কম প্রয়োজনীয় করে তুলল — আর এর মধ্যে মানদণ্ড তৈরির কোন শিক্ষা আছে?

DOM Level 1 মানদণ্ড দিল, কিন্তু ২০০০-এর দশকের মাঝামাঝি বাস্তব কোড লেখা তবুও কঠিন ছিল। মানদণ্ডের API লম্বা আর কষ্টকর (document.getElementsByTagName('div'), তারপর loop, তারপর class পরীক্ষা), class দিয়ে খোঁজার কোনো native উপায় ছিল না, আর প্রতিটি ব্রাউজারের ভুল ও ব্যতিক্রম আলাদা — বিশেষ করে Internet Explorer 6-এর। ২০০৬ সালের জানুয়ারিতে John Resig নিউ ইয়র্কের একটি BarCamp-এ jQuery উপস্থাপন করেন। মূল ধারণা ছিল এক লাইনে: CSS selector দিয়ে element খুঁজে তাদের ওপর একসাথে কাজ করা — $('.item').addClass('sold-out'). ভেতরে jQuery প্রতিটি ব্রাউজারের পার্থক্য লুকিয়ে রাখত, আর তার selector engine (পরে আলাদা হয়ে Sizzle নামে) CSS selector নিজেই parse করত।

জনপ্রিয়তা ছিল বিস্ময়কর — এক দশকের বেশি সময় ধরে এটি ওয়েবের সবচেয়ে বেশি ব্যবহৃত JavaScript library-গুলোর একটি ছিল। কিন্তু একই সময়ে ব্রাউজাররা jQuery থেকে শিখছিল। W3C-র Selectors API প্রস্তাব করে querySelector আর querySelectorAll, আর ২০০৮–২০০৯ সালের মধ্যে প্রধান ব্রাউজারগুলো (Safari 3.1, Chrome-এর প্রথম সংস্করণ, Firefox 3.5, Internet Explorer 8 — শেষেরটি সীমিত selector-সহ) এটি চালু করে। এর পরে একে একে এল classList, closest, matches, append/before/after, fetch — প্রায় প্রতিটিই jQuery-র কোনো জনপ্রিয় method-এর native রূপ। Internet Explorer বিদায় নেওয়ার পরে ব্রাউজারের পার্থক্যও অনেক কমে যায়।

ফলে নতুন প্রজেক্টে jQuery আর অপরিহার্য নয় — “You Might Not Need jQuery”-এর মতো ওয়েবসাইট প্রতিটি jQuery কাজের native বিকল্প দেখাতে শুরু করে, আর GitHub ২০১৮ সালে নিজের সাইট থেকে jQuery সরানোর অভিজ্ঞতা প্রকাশ করে। তবু jQuery মরেনি; বহু পুরোনো সাইট আর WordPress-এর মতো প্ল্যাটফর্মে এটি আজও চলে, আর নিয়মিত নতুন সংস্করণ আসে।

এখানে একটি বড় শিক্ষা আছে: ওয়েব প্ল্যাটফর্ম প্রায়ই library থেকে শেখে। ডেভেলপাররা আগে library-তে একটি সমাধান হাজার সাইটে পরীক্ষা করেন; যেটি টিকে যায়, মানদণ্ড সেটিকে native করে নেয় (Promise, fetch, এমনকি debounce নিয়েও আলোচনা চলে)। একে কখনো “paving the cowpaths” বলা হয় — মানুষ যে পথে হাঁটে, সেখানেই রাস্তা পাকা করা।

মূল উপলব্ধি: jQuery হেরে যায়নি — সে জিতে গিয়ে নিজেকে অপ্রয়োজনীয় করেছে। আজ আপনি যে querySelectorAll লেখেন, সেটি একটি library-র সাফল্যের স্মৃতিস্তম্ভ।

প্র ০৩ভুলে ভরা HTML (“tag soup”) এক ব্রাউজারে এক রকম, আরেকটিতে আরেক রকম গাছ হতো। HTML5-এর parsing algorithm এই সমস্যা কীভাবে মেটাল, আর কেন এটি XML-এর কঠোর পথের চেয়ে সফল হলো?

প্রথম দিন থেকেই ওয়েবের HTML ছিল অগোছালো। মানুষ হাতে লিখত, ট্যাগ বন্ধ করতে ভুলত, <b><i>…</b></i> লিখত — একে বলা হয় tag soup. ব্রাউজাররা এমন পাতাও দেখাত, কারণ যে ব্রাউজার ভুল পাতা দেখাতে পারত না, ব্যবহারকারী সেটি ছেড়ে দিতেন। কিন্তু কীভাবে মেরামত হবে, তা কোথাও লেখা ছিল না। প্রত্যেক ব্রাউজার অন্যটির আচরণ উল্টো দিক থেকে খুঁজে নকল করার চেষ্টা করত। ফল: একই ভুল HTML থেকে Internet Explorer একটি গাছ বানাত, Firefox আরেকটি। DOM মানদণ্ড ছিল, কিন্তু DOM-এ কী ঢুকবে তা নির্ভর করত parser-এর ওপর — তাই childNodes বা parentNode-এর উত্তরও ব্রাউজারভেদে বদলাত।

W3C-র উত্তর ছিল XHTML: HTML-কে XML-এর কঠোর নিয়মে লেখা, যেখানে একটি ভুলেই পুরো পাতা দেখানো বন্ধ (“draconian error handling”). তাত্ত্বিকভাবে পরিষ্কার, কিন্তু বাস্তবে অচল — কোটি কোটি পুরোনো পাতা কেউ ঠিক করবে না, আর একটি ভুল ট্যাগে সাদা পাতা দেখানো ব্যবসার জন্য অগ্রহণযোগ্য।

২০০৪ সালে Apple, Mozilla আর Opera-র লোকজন WHATWG গঠন করেন, আর Ian Hickson-এর সম্পাদনায় HTML5 মানদণ্ড এক অভূতপূর্ব কাজ করে: বিদ্যমান ব্রাউজারের আচরণ আর লাখ লাখ বাস্তব পাতা বিশ্লেষণ করে প্রতিটি সম্ভাব্য ভুলের জন্য সঠিক মেরামত লিখে ফেলে — tokenizer-এর প্রতিটি অবস্থা, প্রতিটি “insertion mode”, এমনকি কুখ্যাত “adoption agency algorithm”, যা ভুল ক্রমে বন্ধ করা formatting ট্যাগ সামলায়। ২০০৮ সালের জানুয়ারিতে HTML5-এর প্রথম public working draft প্রকাশিত হয়। এই পর্বে দেখা <tbody> নিজে থেকে বসা বা <p>-এর আগাম বন্ধ হওয়া এই algorithm-এরই অংশ। ২০১০ থেকে ২০১২ সালের মধ্যে প্রধান ব্রাউজারগুলো নতুন parser চালু করে — যেমন Firefox 4-এ Henri Sivonen-এর লেখা parser.

ফল: আজ একই HTML, যত ভুলই থাকুক, সব আধুনিক ব্রাউজারে একই DOM দেয়। Python-এর html5lib-এর মতো library-ও একই algorithm মানে, তাই সার্ভারে পড়া গাছও ব্রাউজারের সাথে মেলে।

মূল উপলব্ধি: নিখুঁত ইনপুট দাবি করার বদলে HTML5 “অনিখুঁত ইনপুটের সাথে নির্ধারিত আচরণ” বেছে নিয়েছিল। ক্ষমাশীলতা আর পূর্বানুমেয়তা — দুটো একসাথে পাওয়া যায়, যদি ক্ষমার নিয়মগুলোও লিখে রাখা হয়।

প্র ০৪React (২০১৩) virtual DOM জনপ্রিয় করল এই দাবিতে যে “DOM ধীর”। তাহলে Svelte আর Solid-এর মতো framework virtual DOM ছাড়াই সরাসরি DOM বদলে কীভাবে প্রায়ই আরও দ্রুত হয়?

২০১৩ সালের মে মাসে Facebook React-কে open source করে। তখনকার বড় সমস্যা ছিল state আর DOM-কে মিলিয়ে রাখা: ডেটা বদলালে হাতে হাতে খুঁজে বের করতে হতো কোন element বদলাতে হবে, আর ভুল হলে পাতা আর ডেটা আলাদা কথা বলত। React-এর প্রস্তাব ছিল সাহসী: প্রতিবার state বদলালে পুরো UI নতুন করে “আঁকুন” — তবে আসল DOM-এ নয়, একটি হালকা JavaScript object-এর গাছে, যাকে বলা হলো virtual DOM. তারপর নতুন আর পুরোনো virtual গাছ তুলনা করে (diffing, বা reconciliation) শুধু পার্থক্যগুলো আসল DOM-এ বসানো। এতে পর্ব ২১-এর সরল “পাতা = f(state)” মডেল বড় অ্যাপেও চলে।

এই প্রচারে একটি বাক্য খুব ছড়িয়েছিল — “DOM ধীর, virtual DOM দ্রুত”। কিন্তু এটি অর্ধসত্য। DOM-এর একটি textContent বদল নিজে খুব সস্তা; ধীর হয় অপ্রয়োজনীয় পরিবর্তন, layout thrashing, আর বড় অংশ বারবার মুছে নতুন করে বানানো। Virtual DOM এগুলো এড়ায় — কিন্তু নিজের দাম দিয়ে: প্রতিটি state বদলে component function আবার চলে, নতুন object-গাছ তৈরি হয়, আর পুরো গাছ তুলনা হয়, যদিও হয়তো একটিমাত্র সংখ্যা বদলেছে। ২০১৮ সালে Svelte-এর স্রষ্টা Rich Harris “Virtual DOM is pure overhead” শিরোনামে এই যুক্তিটি জনপ্রিয় করেন।

Svelte (২০১৬ সালে প্রথম, ২০১৯ সালে Svelte 3-এ বড় পরিবর্তন) একটি compiler: আপনার component আগেই বিশ্লেষণ করে এমন কোড বানায়, যা জানে কোন state বদলালে ঠিক কোন DOM node-এর কোন property বদলাতে হবে — ঠিক এই পর্বের মতো হাতে লেখা badge.textContent = count. Ryan Carniato-র SolidJS (১.০ এসেছে ২০২১ সালে) runtime-এ “signal” দিয়ে একই কাজ করে: প্রতিটি signal মনে রাখে কোন DOM আপডেট তার ওপর নির্ভর করে, আর বদলালে শুধু সেটিই চলে — একে বলা হয় fine-grained reactivity. কোনো তুলনা নেই, পুরো component আবার চলে না। পরে Angular (signals) আর Svelte 5 (runes)-ও signal-ধাঁচের reactivity গ্রহণ করেছে; Vue-র reactivity শুরু থেকেই এমন নির্ভরতা মনে রাখে, যদিও সে virtual DOM-ও ব্যবহার করে।

তাহলে কে জিতল? প্রশ্নটিই ভুল। Virtual DOM সরল মানসিক মডেল আর বিশাল ecosystem দেয়; fine-grained পদ্ধতি কম কাজ করে, তবে নিজের নিয়ম শিখতে হয়। বাস্তব অ্যাপে পার্থক্য প্রায়ই নির্ভর করে ডেভেলপার কতটা অপ্রয়োজনীয় render এড়ান তার ওপর।

মূল উপলব্ধি: DOM নিজে ধীর নয় — অপ্রয়োজনীয় কাজ ধীর। যে পদ্ধতি সবচেয়ে কম DOM ছোঁয় আর সবচেয়ে কম হিসাব করে, সে-ই দ্রুত; এই পর্বের “শুধু যা বদলেছে, একবারে বসাও” নিয়মই সব framework-এর লক্ষ্য।

প্র ০৫Shadow DOM আর Web Components কী সমস্যার সমাধান করতে এল? ২০১১ সালের প্রস্তাব থেকে ২০১৬ সালের v1 পর্যন্ত যাত্রায় কী বদলাল?

একটি বড় পাতায় অনেক দলের কোড একসাথে থাকে। আপনি একটি সুন্দর তারিখ-বাছাইয়ের widget বানালেন, কিন্তু সাইটের কোনো stylesheet-এ button { color: red } লেখা — আপনার widget-এর বোতামও লাল হয়ে গেল। আবার আপনার widget-এর .title class সাইটের অন্য .title-কে বদলে দিল, বা কারও document.querySelectorAll('li') আপনার widget-এর ভেতরের li-ও তুলে নিল। DOM একটিমাত্র বিশাল global জায়গা — CSS আর selector-এর কোনো সীমানা নেই। মজার ব্যাপার, ব্রাউজার নিজে এই সমস্যা অনেক আগেই সমাধান করেছিল: <video>-এর play বোতাম বা <input type="date">-এর ক্যালেন্ডার আসলে ভেতরে লুকানো DOM দিয়ে বানানো, যা পাতার CSS ছুঁতে পারে না।

২০১১ সালে Google-এর Alex Russell Fronteers সম্মেলনে প্রস্তাব করেন, এই ক্ষমতা ডেভেলপারদেরও দেওয়া হোক — নাম Web Components. চারটি অংশ নিয়ে শুরু: Custom Elements (নিজের ট্যাগ, যেমন <bkash-button>), Shadow DOM (element-এর সাথে যুক্ত একটি আলাদা, লুকানো গাছ, যার CSS বাইরে যায় না আর বাইরের selector ভেতরে ঢোকে না), HTML Templates (এই পর্বের <template> — এটিই সবার আগে সব ব্রাউজারে আসে), আর HTML Imports. প্রথম সংস্করণ (“v0”) শুধু Chrome-এ চালু হয়; অন্য ব্রাউজার নির্মাতারা নকশার কিছু অংশে আপত্তি তোলেন, আর HTML Imports বাদ পড়ে — তার জায়গা নেয় ES module (পর্ব ৪৫)।

দীর্ঘ আলোচনার পরে সব প্রধান ব্রাউজার নির্মাতা একমত হয়ে নতুন নকশা দাঁড় করান — Custom Elements v1 আর Shadow DOM v1, ২০১৬ সালে। Chrome সেই বছরই দুটো চালু করে, Safari ২০১৬–২০১৭ সালে, Firefox ২০১৮ সালে। নতুন নকশায় custom element বানানো হয় class extends HTMLElement দিয়ে (পর্ব ২৬-এর class), আর attachShadow({ mode: 'open' }) দিয়ে shadow root. পরে Declarative Shadow DOM এসে HTML-এই shadow root লেখার সুযোগ দেয়, যাতে সার্ভারে আঁকা পাতাতেও এটি কাজ করে।

আজ বড় ডিজাইন সিস্টেম আর অনেক কোম্পানির UI library Web Components দিয়ে বানানো, কারণ এগুলো যেকোনো framework-এর সাথে চলে। তবে সীমাবদ্ধতাও আছে: ফর্মের সাথে সংযোগ, accessibility (shadow সীমানা পেরিয়ে aria সম্পর্ক) আর server rendering নিয়ে কাজ এখনো চলছে।

মূল উপলব্ধি: DOM-এর global প্রকৃতি ছোট পাতায় সুবিধা, বড় পাতায় বিপদ। Shadow DOM সেই গাছে দেয়াল তোলে — ঠিক যেমন প্রোগ্রামিং ভাষা function আর module দিয়ে scope তৈরি করে (পর্ব ০৭)। Encapsulation-এর প্রয়োজন সব জায়গায় একই।

অনুশীলন

প্রথম তিনটিতে কোড লিখতে হবে না — আগে কাগজে উত্তর লিখুন, তারপর যেকোনো পাতার DevTools Console-এ মিলিয়ে নিন।

  1. node গুনুন: HTML হলো <div id="box">, তারপর নতুন লাইনে দুই ঘর ভেতরে <h3>নোটিশ</h3>, নতুন লাইনে <p>কাল ক্লাস নেই</p>, তারপর নতুন লাইনে </div>. box.childNodes.length, box.children.length, box.firstChild.nodeType আর box.lastElementChild.textContent কত?

    childNodes.length = ৫: ফাঁকা জায়গার text, h3, ফাঁকা জায়গার text, p, শেষের নতুন লাইনের text. children.length = ২ (h3 আর p). firstChild.nodeType = 3 — প্রথম সন্তান text node. lastElementChild.textContent = "কাল ক্লাস নেই". পুরো HTML এক লাইনে, ফাঁকা ছাড়া লিখলে childNodes-ও ২ হতো — এজন্যই firstChild-নির্ভর কোড ভঙ্গুর।

  2. ফল অনুমান করুন: একটি তালিকায় ৪টি <li class="new">. কোড: const items = document.getElementsByClassName('new'); for (let i = 0; i < items.length; i++) items[i].classList.remove('new');. শেষে কয়টি li-তে new থাকবে, কোনগুলোতে? getElementsByClassName-এর জায়গায় querySelectorAll('.new') হলে?

    li-গুলো A, B, C, D ধরুন। i=0: A থেকে সরল → live = [B, C, D]. i=1: live[1] = C → সরল → [B, D]. i=2: 2 < 2 মিথ্যা — শেষ। B আর D-তে new থাকবে — ঠিক অর্ধেক বাদ। querySelectorAll হলে তালিকা স্থির [A, B, C, D], তাই চারটি থেকেই সরবে। live তালিকা রাখতেই চাইলে while (items.length) items[0].classList.remove('new'); বা শেষ থেকে উল্টো loop-ও কাজ করে।

  3. নিরাপত্তার বাগ খুঁজুন: একটি সহকর্মী মন্তব্য দেখানোর কোড লিখেছেন: list.innerHTML += `<li><b>${c.user}</b>: ${c.text}</li>`; — প্রতিটি মন্তব্যের জন্য একবার। কয়টি সমস্যা দেখছেন? ঠিক করুন।

    তিনটি সমস্যা। (১) নিরাপত্তা: c.user আর c.text অন্যের লেখা, innerHTML-এ HTML হিসেবে parse হয় — XSS. (২) গতি: += প্রতিবার পুরো তালিকার HTML পড়ে, নতুন string বানিয়ে, সব আবার parse করে — n মন্তব্যে মোট কাজ প্রায় n² অনুপাতে বাড়ে। (৩) অবস্থা হারানো: প্রতিবার পুরোনো li-গুলো মুছে নতুন বানানো হয়, তাই তাদের listener বা যেকোনো অবস্থা হারায়। ঠিক রূপ:

    const frag = document.createDocumentFragment();
    for (const c of comments) {
      const li = document.createElement('li');
      const b = document.createElement('b');
      b.textContent = c.user;
      li.append(b, ': ', c.text);       // string → text node, HTML নয়
      frag.append(li);
    }
    list.append(frag);
  4. কোড লিখুন — নোটিশ বোর্ড: একটি <template id="notice-tpl"> (অংশ ৬-এর মতো: .title, .date, একটি বোতাম) আর <section id="board"> আছে। renderNotices(notices) লিখুন, যা [{ id, title, date, urgent }] থেকে সব নোটিশ আঁকবে: নতুনটি সবার ওপরে (date অনুযায়ী উল্টো ক্রমে), urgent হলে urgent class, প্রতিটি article-এ data-id, সব একবারে বসানো। Template-এর <time>-এ datetime attribute-ও দিন।

    একটি সম্ভাব্য সমাধান:

    function renderNotices(notices) {
      const tpl = document.querySelector('#notice-tpl');
      const frag = document.createDocumentFragment();
      for (const n of notices.toSorted((a, b) => b.date.localeCompare(a.date))) {
        const art = tpl.content.firstElementChild.cloneNode(true);
        art.dataset.id = n.id;
        art.classList.toggle('urgent', n.urgent);
        art.querySelector('.title').textContent = n.title;
        const time = art.querySelector('.date');
        time.textContent = n.date;
        time.setAttribute('datetime', n.date);   // নিজের attribute → setAttribute
        frag.append(art);
      }
      document.querySelector('#board').replaceChildren(frag);
    }

    লক্ষ করুন: toSorted মূল array বদলায় না (পর্ব ১৬); template আর board loop-এর আগে একবার খোঁজা; শিরোনাম textContent-এ, তাই নোটিশে যা-ই লেখা থাক, নিরাপদ; আর replaceChildren পুরোনো নোটিশ মুছে নতুনগুলো এক ধাপে বসায়, তাই একই function বারবার ডাকলেও নোটিশ দ্বিগুণ হয় না।

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

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

ক্রমএকটি পাতা খোলা থেকে JavaScript-এর একটি classList.add পর্দায় দেখা পর্যন্ত ঘটনাগুলো সাজান (▲▼ চেপে বা টেনে)।

  1. 📄 ব্রাউজার সার্ভার থেকে HTML-এর বাইট পায়
  2. 🌳 HTML parser token থেকে node বানিয়ে DOM tree গড়ে
  3. ⚙️ defer script চলে, তারপর DOMContentLoaded
  4. 🏷️ script একটি li-তে classList.add('sold-out') করে
  5. 📐 ব্রাউজার style আর layout আবার হিসাব করে
  6. 🎨 paint আর composite — পর্দায় নতুন চেহারা

JavaScript-এ document.querySelectorAll('.item') আর document.getElementsByClassName('item')-এর মূল পার্থক্য কী?

  • প্রথমটি শুধু প্রথম মিল দেয়, দ্বিতীয়টি সব
  • প্রথমটির তালিকা স্থির (static), দ্বিতীয়টির তালিকা DOM-এর সাথে বদলায় (live)
  • প্রথমটি আসল array দেয়, তাই map চলে
  • দ্বিতীয়টি শুধু document-এ ডাকা যায়

HTML-এ <ul>-এর ভেতরে তিনটি <li> আলাদা আলাদা লাইনে, ফাঁকা ঘরসহ লেখা। ul.childNodes.length কত হবে?

  • ৩
  • ৬
  • ৭
  • ১০

ব্যবহারকারীর লেখা একটি মন্তব্য পাতায় দেখানোর সবচেয়ে নিরাপদ উপায় কোনটি?

  • el.textContent = comment
  • el.innerHTML = comment
  • el.insertAdjacentHTML('beforeend', comment)
  • el.innerHTML = comment.replace('<script>', '')

HTML-এ <input id="qty" value="1">. ব্যবহারকারী বক্সে ৫ লিখলেন। qty.value আর qty.getAttribute('value') কী দেবে?

  • "5" আর "5"
  • "1" আর "1"
  • "1" আর "5"
  • "5" আর "1"

একটি loop প্রতিটি কার্ডে card.style.height = box.offsetHeight / 4 + 'px' লিখছে, আর পাতা ধীর হয়ে যাচ্ছে। সবচেয়ে সম্ভাব্য কারণ আর সমাধান কী?

  • style-এ লেখা ধীর; innerHTML দিয়ে লিখলে দ্রুত হবে
  • লেখা আর পড়া মেশানোয় প্রতিটি ঘুরে জোর করা layout (layout thrashing) — মাপ loop-এর আগে একবার পড়ুন
  • for…of ধীর; forEach ব্যবহার করুন
  • 'px' string জোড়া লাগানো ধীর; সংখ্যা সরাসরি দিন

<head>-এ রাখা একটি সাধারণ <script src="app.js">-এ document.querySelector('#cart') null দিচ্ছে, যদিও body-তে id="cart" আছে। সবচেয়ে ভালো সমাধান কোনটি?

  • querySelector-এর বদলে getElementById ব্যবহার করা
  • script-এ async যোগ করা
  • script-এ defer যোগ করা
  • কোডটি setTimeout(…, 1000)-এর ভেতরে রাখা

শব্দকোষ

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

মনে রাখুন

  1. DOM = HTML থেকে বানানো জীবন্ত object-tree. JavaScript ফাইল নয়, গাছ বদলায়; DevTools-এর Elements ট্যাব সেই গাছ দেখায়। node-এর ধরন: document (9), element (1), text (3 — ফাঁকা জায়গাও), comment (8).
  2. খোঁজা: querySelector(All) CSS selector বোঝে, static তালিকা দেয়; getElementsBy… live — loop-এর মাঝে বদলালে element বাদ পড়ে। চলাফেরায় element-কেন্দ্রিক property (children, nextElementSibling), উপরে খুঁজতে closest.
  3. অন্যের লেখা মানেই textContent. innerHTML/insertAdjacentHTML HTML parse করে — বাইরের ডেটায় XSS. HTML সত্যিই লাগলে পরীক্ষিত sanitizer; বিস্তারিত পর্ব ৪৩।
  4. attribute = markup-এর শুরুর মান, property = এখনকার অবস্থা (input.value). নিজের ডেটা dataset-এ, চেহারা classList-এ, inline style শুধু সত্যিকারের গতিশীল মানে।
  5. বানাও, তারপর একবারে বসাও: template + cloneNode + textContent, DocumentFragment, replaceChildren. loop-এ আগে সব পড়া, পরে সব লেখা; script-এ defer; চাপার জিনিস <button>, বদলানো বার্তায় aria-live.

পরের পর্বে: এই পর্বে পাতা বদলেছি কোডের ইচ্ছায়। এবার ব্যবহারকারীর ইচ্ছায় — event. একটি ক্লিক কীভাবে গাছ বেয়ে নিচে নেমে আবার ওপরে ওঠে (capturing আর bubbling), কেন একটি listener দিয়ে হাজার বোতাম সামলানো যায় (delegation — এই পর্বের closest সেখানেই আসল কাজে নামবে), আর নিজের custom event বানানো।

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

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