Internet Programming Laboratory

Unit 6: JavaScript DOM & BOM

DOM Tree, Selection, Manipulation, classList, Event Bubbling & Capturing, DOM Traversal, BOM (window, history, location, navigator), setTimeout, setInterval, IntersectionObserver, MutationObserver — bridging JavaScript to the visible page.

🏢 Industry-Aligned  |  📝 15 MCQs (Bloom's Taxonomy)  |  🔬 5 Lab Exercises  |  💼 Interview Prep

Section 1

Why This Chapter Matters in 2025

You already know JavaScript syntax — variables, functions, loops, promises. But here's the honest truth: JavaScript without DOM access is a brain without a body. It can think, but it can't do anything the user can see. The DOM (Document Object Model) is the bridge between your JavaScript code and the actual pixels on screen.

Every single time you've seen a website update without a full page reload — a notification badge appearing on Flipkart, a cart total recalculating on Swiggy, a stock price flashing green on Zerodha — that's JavaScript talking to the DOM. And the BOM (Browser Object Model) is what gives you access to the browser itself: the URL bar, navigation history, screen dimensions, and timers.

🏢 Industry Connection — DOM Powers Every Indian Super-App

Flipkart — When you add an item to cart, JavaScript calls createElement() to build a new cart-item card, appendChild() to inject it into the sidebar, and classList.add('slide-in') to animate it. Zero page reloads.

Swiggy — Infinite scroll? That's IntersectionObserver watching a sentinel element. When it enters the viewport, fetch() loads the next batch of restaurants and insertAdjacentHTML() injects the cards.

Zerodha (Kite) — Stock prices update 5 times per second via WebSocket. Each update uses querySelector() to find the price cell, textContent to write the new value, and classList.toggle() to flash green or red.

CRED — Those buttery smooth card flip animations? classList.add('flipped') triggers CSS transitions. The classList API is the secret weapon behind every modern animation library.

🇮🇳 Flipkart🇮🇳 Swiggy🇮🇳 Zerodha🇮🇳 CRED🇮🇳 PhonePe🇮🇳 Razorpay

Prerequisite Checklist ✅

  • ✅ Completed Unit 4 (JavaScript Core — variables, functions, objects, async/await)
  • ✅ Completed Unit 5 (or at least know basic HTML structure — you need a page to manipulate)
  • ✅ Chrome DevTools — you'll live in the Elements and Console tabs (F12)
  • ✅ Understand that HTML is a tree of nested elements — the DOM formalises this
The DOM specification was created by the W3C in 1998 to provide a language-neutral interface to HTML and XML documents. Before the DOM, Netscape and Internet Explorer had their own incompatible object models — leading to the infamous "This site is best viewed in..." banners. The DOM standardised everything. Today, every browser implements the same DOM API, which is why your JavaScript works identically on Chrome, Firefox, Safari, and Edge.
Section 2

Learning Outcomes — Bloom's Taxonomy

Bloom's LevelLearning Outcome
L1 — RememberRecall DOM selection methods (getElementById, querySelector, querySelectorAll) and BOM objects (window, history, location, navigator)
L2 — UnderstandExplain the difference between event bubbling and capturing, between innerHTML and textContent, and between NodeList and HTMLCollection
L3 — ApplyUse DOM manipulation methods (createElement, appendChild, removeChild, insertAdjacentHTML) to dynamically build UI components without page reload
L4 — AnalyzeDebug event propagation issues using DevTools, trace DOM traversal paths, and diagnose performance problems caused by excessive DOM mutations
L5 — EvaluateCompare IntersectionObserver vs scroll-event-based approaches for lazy loading and justify the performance benefits of the Observer pattern
L6 — CreateBuild a complete interactive product listing page with dynamic card generation, infinite scroll, classList-driven animations, and SPA-style navigation using the History API
Section 3

Concept Explanations — Theory, Earned

3.1 The DOM Tree — Document, Element, and Text Nodes

When the browser loads an HTML page, it doesn't work with the raw text. It parses the HTML and builds an in-memory tree structure called the DOM (Document Object Model). Every HTML tag becomes a node in this tree. JavaScript interacts with this tree — not with the HTML file directly.

DOM (Document Object Model) — A programming interface that represents an HTML/XML document as a tree of objects. Each node in the tree corresponds to a part of the document (element, text, comment, attribute). JavaScript reads and modifies this tree, and the browser re-renders accordingly.
Three Types of Nodes You Must Know
Node TypenodeType ValueExampleDescription
Document9documentThe root of the tree — entry point for all DOM access
Element1<div>, <p>, <h1>Every HTML tag becomes an Element node
Text3"Hello World"The actual text inside elements — it's a separate node!
ASCII Art: The DOM Tree
HTML Source DOM Tree (in memory) ┌──────────────────────┐ ┌─────────────────────────────────────┐ │ <html> │ │ document │ │ <head> │ │ │ │ │ <title>Shop</title>│ ──► │ <html> │ │ </head> │ │ ┌───┴───┐ │ │ <body> │ │ <head> <body> │ │ <h1>Welcome</h1> │ │ │ ┌──┴──┐ │ │ <p>Shop now</p> │ │ <title> <h1> <p> │ │ </body> │ │ │ │ │ │ │ </html> │ │ "Shop" "Welcome" "Shop now" │ └──────────────────────┘ │ (text) (text) (text) │ └─────────────────────────────────────┘ Element nodes: <html>, <head>, etc. Text nodes: "Shop", "Welcome", etc.

Text is a separate node, not a property of its parent element. When you write <p>Hello</p>, the DOM creates TWO nodes: an Element node (<p>) and a child Text node ("Hello"). This is why element.childNodes can include text nodes and whitespace — it trips up beginners constantly.

JavaScript
// Explore the DOM tree in your console
console.log(document.nodeType);          // 9 (Document node)
console.log(document.documentElement);    // <html> element
console.log(document.head);               // <head> element
console.log(document.body);               // <body> element

// Every node has these properties:
const heading = document.querySelector('h1');
console.log(heading.nodeName);    // "H1"
console.log(heading.nodeType);    // 1 (Element)
console.log(heading.nodeValue);   // null (elements have no value; text nodes do)
console.log(heading.textContent); // "Welcome" (text inside)

3.2 DOM Selection — Finding Elements

Before you can manipulate any element, you need to select it. JavaScript gives you several methods — but in 2025, only two matter in production code.

MethodReturnsSelectorIndustry Use?
getElementById('id')Single Element / nullID only✅ Fast for unique elements
querySelector('css')First matching Element / nullAny CSS selectorIndustry default
querySelectorAll('css')Static NodeListAny CSS selector✅ For multiple elements
getElementsByClassName('cls')Live HTMLCollectionClass name only⚠️ Rarely used
getElementsByTagName('tag')Live HTMLCollectionTag name only⚠️ Legacy
JavaScript
// ═══ getElementById — fastest for unique elements ═══
const navbar = document.getElementById('main-nav');
// No # needed — it only searches IDs

// ═══ querySelector — INDUSTRY STANDARD ═══
// Uses CSS selector syntax — the most flexible method
const hero = document.querySelector('.hero-section');         // First .hero-section
const btn  = document.querySelector('button.primary');        // First button with .primary
const link = document.querySelector('nav a[href="/cart"]');   // Attribute selector
const item = document.querySelector('#products .card:first-child'); // Complex selector

// ═══ querySelectorAll — returns a NodeList (array-like) ═══
const cards = document.querySelectorAll('.product-card');
console.log(cards.length);     // e.g., 24
cards.forEach(card => {         // NodeList supports forEach ✅
  console.log(card.textContent);
});

// Convert NodeList to Array for full array methods
const cardArray = [...cards];   // or Array.from(cards)
const vegCards = cardArray.filter(c => c.dataset.veg === 'true');
Always use querySelector / querySelectorAll as your default. They accept any CSS selector — classes, IDs, attributes, pseudo-classes, combinators. Reserve getElementById only for ultra-performance-sensitive cases (it's slightly faster). At Flipkart, Swiggy, and CRED, querySelector is the standard across all codebases.
NodeList vs HTMLCollection — The Subtle Trap
FeatureNodeList (querySelectorAll)HTMLCollection (getElementsByClassName)
Live/Static?Static — snapshot at query timeLive — auto-updates when DOM changes
forEach()?✅ Yes❌ No (must convert to array)
Predictable?✅ Yes — length doesn't change⚠️ No — length changes as DOM mutates
Recommendation✅ Use this⚠️ Avoid unless you need live updates

Live collections cause infinite loops. If you use getElementsByClassName('item') and then add new elements with class item inside the loop, the collection grows — and your loop never ends. querySelectorAll returns a static snapshot, making it safe.

3.3 DOM Manipulation — Creating, Modifying, and Removing Elements

innerHTML vs textContent — Choose Wisely
JavaScript
const el = document.querySelector('.message');

// ═══ textContent — sets PLAIN TEXT (safe from XSS) ═══
el.textContent = 'Hello, <b>Priya</b>!';
// Renders literally as: Hello, <b>Priya</b>! (tags shown as text)

// ═══ innerHTML — parses and renders HTML ═══
el.innerHTML = 'Hello, <b>Priya</b>!';
// Renders as: Hello, Priya! (tags rendered as HTML)

// 🚨 SECURITY WARNING — never use innerHTML with user input!
const userInput = '<img src=x onerror="alert(document.cookie)">';
el.innerHTML = userInput;  // ⚠️ XSS ATTACK! Cookie stolen!
el.textContent = userInput; // ✅ Safe — rendered as plain text
XSS (Cross-Site Scripting) — A security vulnerability where an attacker injects malicious JavaScript through user input. If you use innerHTML with unsanitised user data, the attacker's script runs in your user's browser. Flipkart, Razorpay, and every payment company have strict rules: never use innerHTML with user-generated content.
Creating Elements — The Safe Way
JavaScript
// 🏢 FLIPKART PATTERN: Building product cards dynamically

const product = { name: 'iPhone 15', price: 79999, img: 'iphone.jpg' };

// Step 1: Create elements
const card = document.createElement('div');
const title = document.createElement('h3');
const price = document.createElement('p');
const img = document.createElement('img');

// Step 2: Set content and attributes
card.className = 'product-card';
title.textContent = product.name;
price.textContent = `₹${product.price.toLocaleString('en-IN')}`;
img.src = product.img;
img.alt = product.name;
img.loading = 'lazy';

// Step 3: Assemble the tree
card.appendChild(img);
card.appendChild(title);
card.appendChild(price);

// Step 4: Insert into the DOM
document.querySelector('#product-grid').appendChild(card);
insertAdjacentHTML — The Best of Both Worlds
JavaScript
// insertAdjacentHTML lets you inject HTML strings at precise positions
// WITHOUT replacing existing content (unlike innerHTML)

const list = document.querySelector('.restaurant-list');

//  Position options:
//  'beforebegin' — before the element itself
//  'afterbegin'  — inside, before first child
//  'beforeend'   — inside, after last child ← MOST COMMON
//  'afterend'    — after the element itself

// 🏢 Swiggy pattern: Append new restaurant cards
const cardHTML = `
  <div class="restaurant-card">
    <h3>Paradise Biryani</h3>
    <p>⭐ 4.5 • 30 mins • ₹300 for two</p>
  </div>
`;

list.insertAdjacentHTML('beforeend', cardHTML);
// Adds new card at the end WITHOUT destroying existing cards
Removing Elements
JavaScript
// Modern way: element.remove()
const notification = document.querySelector('.notification');
notification.remove();  // Removes itself from the DOM ✅

// Legacy way: parent.removeChild(child)
const parent = document.querySelector('#cart-items');
const child = document.querySelector('.cart-item:last-child');
parent.removeChild(child); // Still used when you need a reference to the removed node

// Clear all children
const container = document.querySelector('#results');
container.innerHTML = '';  // Quick but not ideal for performance
// Better: container.replaceChildren(); (no arguments = clear all)

3.4 Modifying Attributes & Styles Dynamically

JavaScript
const img = document.querySelector('#product-image');

// ═══ getAttribute / setAttribute ═══
img.getAttribute('src');                    // Read attribute
img.setAttribute('src', 'new-image.jpg');    // Write attribute
img.setAttribute('alt', 'iPhone 15 Pro Max'); // Accessibility!
img.removeAttribute('loading');              // Delete attribute
img.hasAttribute('data-id');                // Check: returns true/false

// ═══ Direct property access (faster for standard attributes) ═══
img.src = 'new-image.jpg';     // Same as setAttribute('src', ...)
img.alt = 'iPhone 15 Pro Max';
img.id = 'hero-img';

// ═══ data-* attributes — custom data storage ═══
// HTML: <div class="card" data-product-id="42" data-category="phones">
const card = document.querySelector('.card');
console.log(card.dataset.productId);   // "42"  (camelCase!)
console.log(card.dataset.category);    // "phones"
card.dataset.inStock = 'true';          // Adds data-in-stock="true"

// ═══ Inline styles ═══
const banner = document.querySelector('.banner');
banner.style.backgroundColor = '#06b6d4';  // camelCase, not kebab-case!
banner.style.padding = '20px';
banner.style.borderRadius = '12px';
banner.style.display = 'none';              // Hide element
banner.style.display = '';                  // Reset to CSS default

// ═══ Computed styles (read actual rendered values) ═══
const styles = getComputedStyle(banner);
console.log(styles.fontSize);      // "16px" (resolved value)
console.log(styles.color);         // "rgb(30, 41, 59)"
Prefer CSS classes over inline styles. Instead of el.style.color = 'red', use el.classList.add('error') where .error { color: red; } is defined in CSS. This keeps styles in stylesheets (separation of concerns), enables transitions, and is the pattern used at every production company.

3.5 classList API — The CSS Class Controller

The classList property is the most important DOM API for UI development. Instead of manipulating inline styles, you toggle CSS classes — which triggers CSS transitions, keeps concerns separated, and is how every modern framework works under the hood.

JavaScript
const modal = document.querySelector('.modal');

// ═══ add() — Add one or more classes ═══
modal.classList.add('visible');                   // Add single class
modal.classList.add('animate', 'fade-in');         // Add multiple classes

// ═══ remove() — Remove classes ═══
modal.classList.remove('hidden');
modal.classList.remove('animate', 'fade-in');

// ═══ toggle() — Add if missing, remove if present ═══
modal.classList.toggle('active');   // Toggle on/off

// toggle() with force parameter:
modal.classList.toggle('dark', isDarkMode);
// If isDarkMode is true → add 'dark'
// If isDarkMode is false → remove 'dark'

// ═══ contains() — Check if class exists ═══
if (modal.classList.contains('visible')) {
  console.log('Modal is showing');
}

// ═══ replace() — Swap one class for another ═══
modal.classList.replace('loading', 'loaded');

// 🏢 CRED PATTERN: Card flip animation
const card = document.querySelector('.reward-card');
card.addEventListener('click', () => {
  card.classList.toggle('flipped');  // CSS does the animation!
});
❌ Bad — Inline styles
// Hard to maintain, no transitions
el.style.color = 'red';
el.style.fontWeight = 'bold';
el.style.border = '2px solid red';
✅ Good — classList + CSS
// Clean, animatable, reusable
el.classList.add('error');
/* CSS: .error { color: red;
   font-weight: bold;
   border: 2px solid red;
   transition: all 0.3s; } */

3.6 Event Handling — Bubbling & Capturing

Events are the heartbeat of interactive web applications. Every click, keystroke, scroll, and hover generates an event that JavaScript can respond to. But understanding how events travel through the DOM tree is crucial — and it's the #1 interview question for frontend developers.

addEventListener — The Modern Way
JavaScript
const btn = document.querySelector('#checkout-btn');

// ═══ Basic event listener ═══
btn.addEventListener('click', (event) => {
  console.log('Button clicked!');
  console.log(event.target);       // The element that was clicked
  console.log(event.currentTarget); // The element with the listener
  console.log(event.type);          // "click"
});

// ═══ Common event types ═══
// Mouse: click, dblclick, mouseenter, mouseleave, mousemove
// Keyboard: keydown, keyup, keypress (deprecated)
// Form: submit, input, change, focus, blur
// Window: load, DOMContentLoaded, scroll, resize
// Touch: touchstart, touchmove, touchend

// ═══ Remove a listener ═══
const handleClick = () => console.log('clicked');
btn.addEventListener('click', handleClick);
btn.removeEventListener('click', handleClick); // Must be same function reference!
Event Bubbling & Capturing — How Events Travel

When you click a button inside a <div> inside a <section>, the event doesn't just fire on the button. It goes through three phases:

EVENT PROPAGATION PHASES Phase 1: CAPTURING (top → down) Phase 3: BUBBLING (bottom → up) ───────────────────────── ───────────────────────── window ──────────────────► │ ◄─────────────────── window │ │ ▲ document ────────────────► │ ◄─────────────────── document │ │ ▲ <html> ──────────────────► │ ◄─────────────────── <html> │ │ ▲ <body> ──────────────────► │ ◄─────────────────── <body> │ │ ▲ <section> ───────────────► │ ◄─────────────────── <section> │ │ ▲ <div> ───────────────────► │ ◄─────────────────── <div> │ │ ▲ └───────► [BUTTON] ◄────┘ ── Phase 2: TARGET ───┘ (you clicked here) Default: listeners fire during BUBBLING (Phase 3) To listen during CAPTURING: addEventListener('click', fn, true)
JavaScript
// ═══ BUBBLING (default) — event travels from target UP to window ═══
document.querySelector('#outer').addEventListener('click', () => {
  console.log('Outer div clicked (bubbling)');
});
document.querySelector('#inner').addEventListener('click', () => {
  console.log('Inner button clicked (bubbling)');
});
// Click inner → logs: "Inner button clicked" THEN "Outer div clicked"

// ═══ CAPTURING — add 'true' as third argument ═══
document.querySelector('#outer').addEventListener('click', () => {
  console.log('Outer div (capturing)');
}, true);
// Click inner → logs: "Outer div (capturing)" THEN "Inner button (bubbling)"

// ═══ stopPropagation — stop the event from traveling further ═══
document.querySelector('#inner').addEventListener('click', (e) => {
  e.stopPropagation();  // Prevents bubbling to #outer
  console.log('Only inner fires!');
});

// ═══ EVENT DELEGATION — the industry pattern ═══
// Instead of adding listeners to 100 product cards,
// add ONE listener to the parent container!

document.querySelector('#product-grid').addEventListener('click', (e) => {
  const card = e.target.closest('.product-card');
  if (!card) return;  // Click was not on a card

  const productId = card.dataset.id;
  console.log(`Product ${productId} clicked!`);
});
Event Delegation — A pattern where you attach a single event listener to a parent element instead of individual listeners on each child. The event bubbles up from the clicked child to the parent, where you use event.target or event.target.closest() to identify which child was clicked. Used at Flipkart (product grids), Swiggy (restaurant lists), and every company that renders dynamic lists.
Always ensure interactive elements are accessible. Use <button> for clickable actions (not <div onclick>). Buttons are focusable, respond to Enter/Space keys, and are announced by screen readers. Event delegation works with both mouse clicks and keyboard events on proper semantic elements.

3.7 DOM Traversal — Walking the Tree

Sometimes you need to navigate from one element to its parent, siblings, or children without running a new query. DOM traversal lets you "walk" the tree.

JavaScript
// Assume HTML:
// <ul id="menu">
//   <li>Home</li>
//   <li class="active">Products</li>
//   <li>About</li>
// </ul>

const active = document.querySelector('.active');

// ═══ PARENT ═══
active.parentNode;        // <ul id="menu"> (any node type)
active.parentElement;     // <ul id="menu"> (element only — same here)

// ═══ CHILDREN ═══
const menu = document.querySelector('#menu');
menu.children;            // HTMLCollection [li, li.active, li] — ELEMENTS only
menu.childNodes;          // NodeList [text, li, text, li, text, li, text]
                          // ⚠️ Includes whitespace text nodes!
menu.firstElementChild;   // <li>Home</li>
menu.lastElementChild;    // <li>About</li>

// ═══ SIBLINGS ═══
active.nextElementSibling;     // <li>About</li>
active.previousElementSibling; // <li>Home</li>

// ⚠️ BEWARE: nextSibling (without "Element") returns TEXT nodes too!
active.nextSibling;       // #text (whitespace node) — NOT the next <li>!
active.nextElementSibling;// <li>About</li> ← This is what you want

// ═══ closest() — Walk UP the tree to find an ancestor ═══
// This is the reverse of querySelector (which searches DOWN)
const deleteBtn = document.querySelector('.delete-btn');
const parentCard = deleteBtn.closest('.product-card');
// Walks up from .delete-btn until it finds .product-card
// Returns null if no ancestor matches
Always use the "Element" versions of traversal properties: parentElement, children, nextElementSibling, previousElementSibling, firstElementChild, lastElementChild. The non-Element versions (childNodes, nextSibling) include text nodes (whitespace!), which almost always causes bugs.

3.8 BOM — Browser Object Model (window, history, location, navigator)

The BOM gives JavaScript access to the browser itself — not the page content (that's the DOM). The window object is the global object in browsers — every global variable, function, and even document itself is a property of window.

window — The Global Object
JavaScript
// window is the top-level object — everything lives here
console.log(window.innerWidth);   // Browser viewport width in px
console.log(window.innerHeight);  // Browser viewport height in px
console.log(window.outerWidth);   // Full browser window width
console.log(window.scrollX);      // Horizontal scroll position
console.log(window.scrollY);      // Vertical scroll position

// Scroll to a position
window.scrollTo({ top: 0, behavior: 'smooth' });  // Smooth scroll to top

// Dialog boxes (rarely used in production)
alert('Payment successful!');       // Blocks execution — avoid!
const ok = confirm('Delete item?'); // Returns true/false
const name = prompt('Your name?');  // Returns string or null
location — URL Management
JavaScript
// 🏢 Example: https://www.flipkart.com/mobiles?brand=apple&sort=price#reviews

console.log(location.href);      // Full URL
console.log(location.origin);    // "https://www.flipkart.com"
console.log(location.pathname);  // "/mobiles"
console.log(location.search);    // "?brand=apple&sort=price"
console.log(location.hash);      // "#reviews"
console.log(location.hostname);  // "www.flipkart.com"
console.log(location.protocol);  // "https:"

// Parse query parameters (modern way)
const params = new URLSearchParams(location.search);
console.log(params.get('brand'));  // "apple"
console.log(params.get('sort'));   // "price"

// Navigate to a new URL
location.href = 'https://www.flipkart.com/cart';  // Full page load
location.replace('/login');   // Navigate WITHOUT adding to history
location.reload();              // Refresh the page
history — Browser Navigation History
JavaScript
// ═══ Basic navigation ═══
history.back();       // Same as clicking browser's Back button
history.forward();    // Same as clicking Forward button
history.go(-2);       // Go back 2 pages
console.log(history.length); // Number of entries in history stack

// ═══ pushState — SPA navigation WITHOUT page reload ═══
// 🏢 PhonePe uses this for their single-page app navigation
history.pushState(
  { page: 'payments' },    // State object (data you want to pass)
  '',                       // Title (ignored by most browsers)
  '/payments'               // New URL to show in address bar
);
// URL changes to /payments but NO page reload!

// replaceState — same but doesn't add new history entry
history.replaceState({ page: 'home' }, '', '/');

// Listen for Back/Forward button clicks
window.addEventListener('popstate', (e) => {
  console.log('User navigated!', e.state);
  // e.state contains the object passed to pushState
  renderPage(e.state.page);  // Re-render the correct view
});
SPA (Single Page Application) — A web application that loads a single HTML page and dynamically rewrites the content using JavaScript, instead of loading new pages from the server. PhonePe, Gmail, and Twitter are SPAs. The History API (pushState/popstate) makes the URL bar update without actual navigation, giving users proper back/forward functionality.
navigator — Browser & Device Info
JavaScript
console.log(navigator.userAgent);    // Browser identification string
console.log(navigator.language);     // "en-US", "hi-IN", etc.
console.log(navigator.onLine);       // true if online, false if offline
console.log(navigator.cookieEnabled);// true if cookies allowed
console.log(navigator.platform);     // "Win32", "MacIntel", etc.

// Geolocation (requires user permission)
navigator.geolocation.getCurrentPosition(
  (pos) => console.log(pos.coords.latitude, pos.coords.longitude),
  (err) => console.error('Location denied')
);

// Online/offline detection
window.addEventListener('offline', () => {
  showToast('You are offline. Changes will sync when reconnected.');
});
window.addEventListener('online', () => {
  showToast('Back online! Syncing...');
  syncPendingChanges();
});

3.9 setTimeout, setInterval, and clearTimeout

JavaScript
// ═══ setTimeout — run ONCE after a delay ═══
const timerId = setTimeout(() => {
  console.log('This runs after 2 seconds');
}, 2000);

// Cancel before it fires
clearTimeout(timerId);

// ═══ setInterval — run REPEATEDLY at an interval ═══
let seconds = 0;
const intervalId = setInterval(() => {
  seconds++;
  console.log(`${seconds}s elapsed`);
  if (seconds >= 10) {
    clearInterval(intervalId);  // Stop after 10 seconds
    console.log('Timer stopped!');
  }
}, 1000);

// 🏢 ZERODHA PATTERN: Live stock price ticker
const startPriceTicker = (symbol) => {
  const priceEl = document.querySelector(`[data-symbol="${symbol}"]`);
  const tickerId = setInterval(async () => {
    try {
      const res = await fetch(`/api/price/${symbol}`);
      const { price, change } = await res.json();
      priceEl.textContent = `₹${price.toFixed(2)}`;
      priceEl.classList.remove('up', 'down');
      priceEl.classList.add(change >= 0 ? 'up' : 'down');
    } catch (err) {
      console.error('Price fetch failed');
    }
  }, 1000); // Update every second
  return tickerId; // So caller can clearInterval when needed
};

// ═══ Debounce pattern — limit how often a function fires ═══
// Used for search-as-you-type on Swiggy, Flipkart, etc.
const debounce = (fn, delay) => {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
};

const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', debounce((e) => {
  fetchSearchResults(e.target.value);
}, 300)); // Only fires 300ms after user stops typing

setTimeout delay is not guaranteed. setTimeout(fn, 0) does NOT execute immediately — it waits for the current call stack to empty. JavaScript is single-threaded; the event loop processes timers only after the main thread is free. A CPU-intensive loop can delay a "0ms" timeout by seconds.

3.10 IntersectionObserver & MutationObserver

IntersectionObserver — Lazy Loading & Infinite Scroll

Before IntersectionObserver, detecting if an element was visible meant listening to the scroll event and calling getBoundingClientRect() on every scroll tick — a massive performance killer. IntersectionObserver does this natively, off the main thread.

JavaScript
// 🏢 SWIGGY PATTERN: Infinite scroll — load more restaurants

// Step 1: Create the observer
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('Sentinel is visible! Loading more...');
      loadMoreRestaurants();
    }
  });
}, {
  root: null,         // null = viewport (default)
  rootMargin: '0px',  // Trigger exactly at viewport edge
  threshold: 0.1      // Trigger when 10% of element is visible
});

// Step 2: Observe a sentinel element at the bottom of the list
const sentinel = document.querySelector('#load-more-trigger');
observer.observe(sentinel);

// Step 3: Stop observing when all data is loaded
// observer.unobserve(sentinel);
// observer.disconnect(); // Stop observing ALL elements

// ═══ LAZY LOADING IMAGES ═══
const imgObserver = new IntersectionObserver((entries, obs) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;  // Load actual image from data-src
      img.classList.remove('lazy');
      obs.unobserve(img);         // Stop watching once loaded
    }
  });
}, { threshold: 0, rootMargin: '200px' }); // Start 200px before viewport

// Observe all lazy images
document.querySelectorAll('img.lazy').forEach(img => {
  imgObserver.observe(img);
});
Chrome's native loading="lazy" attribute (added in 2019) uses IntersectionObserver under the hood! But the API gives you much more control — custom thresholds, root margins, and callbacks. Swiggy's infinite scroll, Flipkart's lazy-loaded product images, and CRED's scroll-triggered animations all use the IntersectionObserver API directly.
MutationObserver — Watching the DOM for Changes
JavaScript
// MutationObserver watches for DOM changes and reacts to them
// Useful for: third-party script monitoring, dynamic content tracking

const targetNode = document.querySelector('#chat-messages');

const mutationObserver = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      console.log('New messages added!');
      mutation.addedNodes.forEach(node => {
        if (node.nodeType === 1) { // Element node
          console.log('New message:', node.textContent);
        }
      });
    }
    if (mutation.type === 'attributes') {
      console.log(`Attribute "${mutation.attributeName}" changed`);
    }
  });
});

// Start observing with configuration
mutationObserver.observe(targetNode, {
  childList: true,     // Watch for added/removed children
  attributes: true,    // Watch for attribute changes
  characterData: true, // Watch for text content changes
  subtree: true        // Watch all descendants, not just direct children
});

// Stop observing
// mutationObserver.disconnect();
MutationObserver replaces the deprecated Mutation Events (DOMNodeInserted, DOMSubtreeModified). Those old events fired synchronously during DOM mutations, causing severe performance problems. MutationObserver batches mutations and delivers them asynchronously — much faster. Chrome DevTools' own Elements panel uses MutationObserver internally to update when you modify the DOM from the Console.
Section 4

Industry Problems — Real-World Case Studies

Case Study 1: Flipkart — Dynamic Product Card Grid

The Problem: Flipkart's search results page needs to render 40+ product cards from API data. Each card has an image, title, price, rating, and "Add to Cart" button. Cards are generated dynamically — the HTML doesn't exist in the source. They must be accessible, performant, and handle click events efficiently.

The DOM Challenge:

  • Use createElement() + appendChild() to build each card (safer than innerHTML with user data)
  • Use data-* attributes to store product IDs on cards
  • Use event delegation on the grid container — one listener handles all 40+ cards
  • Use classList.add('loaded') to trigger CSS fade-in animations
JavaScript
// Flipkart-style product card builder
const renderProducts = (products) => {
  const grid = document.querySelector('#product-grid');

  products.forEach(product => {
    const card = document.createElement('article');
    card.className = 'product-card';
    card.dataset.id = product.id;
    card.dataset.category = product.category;

    card.innerHTML = `
      <img src="${product.thumbnail}" alt="${product.name}"
           loading="lazy" class="card-img">
      <h3 class="card-title">${product.name}</h3>
      <div class="card-price">
        <span class="current">₹${product.price.toLocaleString('en-IN')}</span>
        <span class="mrp">₹${product.mrp.toLocaleString('en-IN')}</span>
        <span class="discount">${product.discount}% off</span>
      </div>
      <div class="card-rating">⭐ ${product.rating} (${product.reviews})</div>
      <button class="add-to-cart-btn" aria-label="Add ${product.name} to cart">
        Add to Cart
      </button>
    `;

    grid.appendChild(card);
    // Trigger fade-in after DOM insertion
    requestAnimationFrame(() => card.classList.add('visible'));
  });
};

// Event delegation — ONE listener for ALL cards
document.querySelector('#product-grid').addEventListener('click', (e) => {
  const btn = e.target.closest('.add-to-cart-btn');
  if (!btn) return;
  const productId = btn.closest('.product-card').dataset.id;
  addToCart(productId);
});

Key Concepts Used: createElement, appendChild, innerHTML (safe — data from trusted API), dataset, event delegation with closest(), classList.add, requestAnimationFrame.

Case Study 2: Swiggy — Infinite Scroll with IntersectionObserver

The Problem: Swiggy's restaurant listing page shows 15 restaurants initially. As the user scrolls to the bottom, it loads the next batch seamlessly — no "Load More" button needed. This must work without janky scroll-event listeners.

The Observer Pattern:

  • Place a hidden sentinel <div> at the bottom of the list
  • IntersectionObserver watches the sentinel
  • When sentinel enters viewport → fetch next page → insertAdjacentHTML new cards → move sentinel below new cards
  • When no more data → observer.disconnect()
JavaScript
// Swiggy-style infinite scroll
let page = 1;
let isLoading = false;
let hasMore = true;

const sentinel = document.querySelector('#scroll-sentinel');
const listContainer = document.querySelector('#restaurant-list');

const observer = new IntersectionObserver(async (entries) => {
  const entry = entries[0];
  if (!entry.isIntersecting || isLoading || !hasMore) return;

  isLoading = true;
  sentinel.textContent = 'Loading more restaurants...';

  try {
    const res = await fetch(`/api/restaurants?page=${++page}`);
    const { data, hasNextPage } = await res.json();
    hasMore = hasNextPage;

    const cardsHTML = data.map(r => `
      <div class="restaurant-card">
        <img src="${r.image}" alt="${r.name}" loading="lazy">
        <h3>${r.name}</h3>
        <p>⭐ ${r.rating} • ${r.deliveryTime} mins</p>
      </div>
    `).join('');

    listContainer.insertAdjacentHTML('beforeend', cardsHTML);
    sentinel.textContent = '';

    if (!hasMore) {
      observer.disconnect();
      sentinel.textContent = 'You have seen all restaurants!';
    }
  } catch (err) {
    sentinel.textContent = 'Failed to load. Scroll down to retry.';
  } finally {
    isLoading = false;
  }
}, { rootMargin: '300px' }); // Start loading 300px before sentinel is visible

observer.observe(sentinel);

Key Concepts Used: IntersectionObserver, rootMargin, insertAdjacentHTML, async/await, observer.disconnect(), debounce-like guard with isLoading flag.

Case Study 3: PhonePe — SPA Navigation with History API

The Problem: PhonePe's web app feels like a native app — clicking "Payments", "History", "Profile" doesn't reload the page. The URL updates, the browser back/forward buttons work correctly, and each view loads its own content dynamically. All of this runs on a single HTML page.

The History API Solution:

  • pushState() updates the URL without navigation
  • popstate event fires when user clicks back/forward
  • A router function matches the URL to a view and renders it
  • Initial load checks location.pathname to render the correct view
JavaScript
// PhonePe-style SPA router
const routes = {
  '/':         () => renderView('home'),
  '/payments': () => renderView('payments'),
  '/history':  () => renderView('history'),
  '/profile':  () => renderView('profile'),
};

const navigate = (path) => {
  history.pushState({ path }, '', path);
  const route = routes[path] || routes['/'];
  route();
  updateActiveNav(path);
};

const renderView = async (view) => {
  const app = document.querySelector('#app');
  app.classList.add('fade-out');
  await new Promise(r => setTimeout(r, 200)); // Wait for fade

  const res = await fetch(`/api/views/${view}`);
  const html = await res.text();
  app.innerHTML = html;

  app.classList.remove('fade-out');
  app.classList.add('fade-in');
};

// Handle navigation link clicks
document.querySelector('nav').addEventListener('click', (e) => {
  const link = e.target.closest('a[data-route]');
  if (!link) return;
  e.preventDefault();
  navigate(link.dataset.route);
});

// Handle browser back/forward buttons
window.addEventListener('popstate', (e) => {
  const path = e.state?.path || '/';
  const route = routes[path] || routes['/'];
  route();
  updateActiveNav(path);
});

// Initial load — render based on current URL
const initialRoute = routes[location.pathname] || routes['/'];
initialRoute();

Key Concepts Used: history.pushState, popstate event, location.pathname, event delegation, classList transitions, async/await, SPA pattern.

Section 5

Lab Exercises — Hands-On Practice

Lab 1: DOM Selection & Traversal — Element Explorer

⏱ 30 minutesBeginner

Objective: Master all DOM selection and traversal methods using the Chrome Console.

Instructions:

  1. Create an HTML page with: a <nav> with 5 links, a <main> with 3 <section>s, each section containing an <h2>, two <p>s, and a <ul> with 4 <li> items. Add unique IDs, classes, and data-* attributes.
  2. Use getElementById, querySelector, querySelectorAll, getElementsByClassName to select various elements. Log the return types.
  3. From the middle <section>, traverse to: its parent, first child, last child, next sibling section, and previous sibling section using traversal properties.
  4. Use closest() to walk up from a deeply nested <li> to its parent <section>.
  5. Convert a NodeList to an Array and use filter() to find all elements with a specific data-* attribute value.
  6. Demonstrate the difference between childNodes (includes text) and children (elements only).

Lab 2: Dynamic Card Builder — Flipkart Product Grid

⏱ 40 minutesIntermediate

Objective: Build a product card grid entirely with JavaScript DOM manipulation — no hardcoded HTML cards.

Requirements:

  1. Create an array of 12 product objects: { id, name, price, mrp, discount, rating, image, category }.
  2. Use createElement() and appendChild() to build each card dynamically. Each card must have: image, title, price (with MRP strikethrough), discount badge, rating stars, and "Add to Cart" button.
  3. Use dataset to store product ID and category on each card.
  4. Add event delegation on the grid — handle "Add to Cart" clicks with a single listener.
  5. Add filter buttons (All, Electronics, Clothing, Books). Use classList.toggle('hidden') to show/hide cards by category.
  6. Add a sort dropdown (Price: Low→High, High→Low, Rating). Re-sort and re-render cards.
  7. Style with CSS Grid and add a classList.add('visible') fade-in animation on each card.

Lab 3: Event Bubbling & Delegation — Interactive Task Manager

⏱ 35 minutesIntermediate

Objective: Build a task manager that demonstrates event bubbling, delegation, and propagation control.

Requirements:

  1. Create a task list where each task item has: a checkbox (toggle complete), task text, an "Edit" button, and a "Delete" button.
  2. Use a single event listener on the task list container (event delegation). Inside the handler, use event.target.closest() to determine which button or checkbox was clicked.
  3. Checkbox click: toggle classList('completed') on the task item (strikethrough style).
  4. Delete button: use element.remove() to remove the task from the DOM.
  5. Edit button: replace the task text with an <input> field (use replaceChild). On Enter or blur, replace back with updated text.
  6. Add a "New Task" form that uses createElement to add tasks. Use event.preventDefault() on form submit.
  7. Add a console log inside each handler showing event.target, event.currentTarget, and event.eventPhase.

Lab 4: Lazy Loading & Infinite Scroll with IntersectionObserver

⏱ 45 minutesAdvanced

Objective: Implement Swiggy-style lazy loading and infinite scroll using IntersectionObserver.

Requirements:

  1. Create a page with 20 placeholder image cards. Each <img> has src="placeholder.svg" and data-src="actual-image.jpg".
  2. Create an IntersectionObserver that watches all lazy images. When an image enters the viewport (with rootMargin: '200px'), swap data-src into src.
  3. Add a CSS class .loaded to fade in the image after it loads (use the load event on the <img>).
  4. Implement infinite scroll: place a sentinel <div> at the bottom. When observed, generate 10 more cards with insertAdjacentHTML() and observe their images.
  5. Show a loading spinner while new cards are being "fetched" (simulate with setTimeout).
  6. After 50 total cards, disconnect() the scroll observer and show "No more items".
  7. Display a counter: "Showing 20 of 50 items" that updates as more load.

Lab 5: Complete SPA — BOM-Powered Dashboard with Routing

⏱ 60 minutesAdvanced

Objective: Build a mini single-page application dashboard using the History API, BOM objects, and all DOM skills.

Requirements:

  1. Create a dashboard with 4 views: Home, Products, Analytics, Settings. Navigation between views must NOT reload the page.
  2. Use history.pushState() to update the URL when navigating. /, /products, /analytics, /settings.
  3. Handle the popstate event so browser Back/Forward buttons work correctly.
  4. The Products view should use createElement to build a dynamic table of products.
  5. The Analytics view should display: window.innerWidth, window.innerHeight, navigator.userAgent, navigator.language, navigator.onLine, location.href.
  6. The Settings view should have a dark mode toggle using classList.toggle('dark') on document.body. Persist the preference in localStorage.
  7. Add a setInterval-based clock in the header showing live time. Clear it when leaving the page (beforeunload).
  8. Use MutationObserver to log every DOM change in the #app container to the console.
  9. Add smooth page transition animations using classList.add('fade-out') before content swap and classList.add('fade-in') after.

Deliverable: dashboard.html, dashboard.css, dashboard.js — single-page app with router.

Section 6

MCQ Assessment Bank — 15 Questions

Hover over any question to reveal the answer. Each question is tagged with Bloom's Taxonomy level.

Q1

What does document.querySelector('.card') return if no element matches?

  1. An empty NodeList
  2. undefined
  3. null
  4. Throws an error
C. nullquerySelector returns the first matching element, or null if none is found. querySelectorAll returns an empty NodeList (not null). Always null-check before using the result.
L1 — RememberDOM Selection
Q2

What is the key difference between innerHTML and textContent?

  1. innerHTML is faster
  2. textContent parses HTML, innerHTML does not
  3. innerHTML parses and renders HTML, textContent treats everything as plain text
  4. There is no difference
C. innerHTML parses the string as HTML (tags are rendered). textContent inserts raw text (tags shown literally). Using innerHTML with user input is an XSS security risk. Use textContent for user-generated content.
L2 — UnderstandDOM Manipulation
Q3

In event propagation, what is the default phase in which addEventListener listens?

  1. Capturing phase
  2. Target phase only
  3. Bubbling phase
  4. Both capturing and bubbling
C. Bubbling phase — By default, addEventListener registers the handler for the bubbling phase. To listen during capturing, pass true as the third argument: addEventListener('click', fn, true).
L2 — UnderstandEvent Propagation
Q4

Which method adds a CSS class if it's missing and removes it if it's present?

  1. classList.add()
  2. classList.replace()
  3. classList.toggle()
  4. classList.switch()
C. classList.toggle() — It adds the class if absent, removes it if present. You can also pass a second boolean argument to force add/remove: classList.toggle('dark', isDarkMode).
L1 — RememberclassList API
Q5

What does event.stopPropagation() do?

  1. Prevents the default browser action (e.g., following a link)
  2. Stops the event from propagating to parent/child elements
  3. Removes the event listener
  4. Cancels the event entirely, including the target phase
B. stopPropagation() prevents the event from traveling further up (bubbling) or down (capturing) the DOM tree. It does NOT prevent the default action — that's preventDefault(). They are independent.
L2 — UnderstandEvent Propagation
Q6

What is the output of document.querySelector('#menu').children.length for this HTML: <ul id="menu"> <li>A</li> <li>B</li> </ul>?

  1. 2
  2. 5
  3. 4
  4. 3
A. 2.children returns only Element nodes (the two <li> elements). .childNodes would return 5 (3 text nodes for whitespace + 2 element nodes). Always use .children when you want elements only.
L3 — ApplyDOM Traversal
Q7

Which insertAdjacentHTML position inserts content inside the element, after its last child?

  1. 'beforebegin'
  2. 'afterbegin'
  3. 'beforeend'
  4. 'afterend'
C. 'beforeend' — This is the most commonly used position. It appends content inside the element at the end, similar to appendChild() but accepting HTML strings. 'afterbegin' prepends inside; 'beforebegin' and 'afterend' insert outside the element.
L1 — RememberDOM Manipulation
Q8

What does history.pushState({page: 'about'}, '', '/about') do?

  1. Navigates to /about and reloads the page
  2. Changes the URL to /about without reloading the page
  3. Adds /about to bookmarks
  4. Opens /about in a new tab
B. pushState updates the URL in the address bar and adds a new entry to the browser history stack — all WITHOUT causing a page reload. This is the foundation of Single Page Application (SPA) routing used by React Router, Vue Router, and apps like PhonePe.
L2 — UnderstandHistory API
Q9

What is the advantage of IntersectionObserver over scroll event listeners for lazy loading?

  1. IntersectionObserver works offline
  2. IntersectionObserver runs off the main thread and doesn't cause jank
  3. IntersectionObserver is synchronous, so it's faster
  4. There is no advantage — they are identical in performance
B. Scroll events fire on every scroll tick (potentially 60+ times/second) and run on the main thread, causing layout thrashing if you call getBoundingClientRect(). IntersectionObserver runs asynchronously, is managed by the browser off-thread, and only calls your callback when intersection state changes.
L5 — EvaluateIntersectionObserver
Q10

What does element.closest('.container') do?

  1. Finds the nearest child with class container
  2. Walks up the DOM tree to find the nearest ancestor with class container
  3. Finds the closest sibling with class container
  4. Returns the element's computed styles
B. closest() traverses UP the DOM tree (from the element itself to the document root) and returns the first ancestor that matches the given CSS selector, or null if no match is found. It's the reverse of querySelector() (which searches down). Essential for event delegation.
L2 — UnderstandDOM Traversal
Q11

A developer uses setTimeout(fn, 0). When does fn execute?

  1. Immediately, before the next line of code
  2. After the current call stack clears and the event loop picks it up
  3. Exactly 0 milliseconds later
  4. Never — 0ms timeouts are ignored
B. setTimeout(fn, 0) doesn't mean "immediate." It means "add to the task queue as soon as possible." The callback executes only after the current synchronous code finishes and the event loop picks it up. This is a classic interview question about JavaScript's single-threaded, event-loop-based execution model.
L4 — AnalyzeEvent Loop
Q12

What is Event Delegation and why is it used?

  1. Assigning events to the window object for global access
  2. Using a single listener on a parent to handle events from all children, improving performance for dynamic lists
  3. Delegating event handling to a Web Worker
  4. Removing events after they fire once
B. Instead of attaching listeners to each child (costly for large lists and broken for dynamically added items), you attach ONE listener to the parent. Events bubble up from children to the parent, where you identify the target with event.target or event.target.closest(). Used universally at Flipkart, Swiggy, and every production app.
L2 — UnderstandEvent Delegation
Q13

Which BOM property tells you if the user is currently connected to the internet?

  1. window.isOnline
  2. navigator.onLine
  3. location.connected
  4. document.online
B. navigator.onLine — Returns true if the browser is online, false if offline. You can also listen for online and offline events on the window object. Apps like PhonePe use this to show "You're offline" banners and queue transactions.
L1 — RememberBOM — navigator
Q14

A Zerodha developer is updating 500 stock prices per second using innerHTML on a table. What is the performance issue?

  1. No issue — innerHTML is always fast
  2. Each innerHTML assignment causes the browser to reparse and rebuild all child nodes, triggering layout reflow
  3. innerHTML doesn't work on table cells
  4. The browser throttles innerHTML to once per second
B. Setting innerHTML destroys all existing child nodes and rebuilds them from the HTML string. For 500 cells per second, this causes massive DOM thrashing. The fix: use textContent to update individual cells (only changes the text node, no parsing or rebuilding needed).
L4 — AnalyzePerformance
Q15

A developer needs to detect when a third-party script dynamically adds elements to a container. Which API should they use?

  1. IntersectionObserver
  2. ResizeObserver
  3. MutationObserver
  4. PerformanceObserver
C. MutationObserver — It watches for changes to the DOM tree: added/removed nodes, attribute changes, and text content changes. IntersectionObserver tracks visibility, ResizeObserver tracks size changes, and PerformanceObserver tracks performance metrics. MutationObserver is the correct tool for detecting DOM mutations.
L3 — ApplyMutationObserver
Section 7

Chapter Summary

🎯 Unit 6 — Key Takeaways

  • DOM Tree: HTML is parsed into a tree of nodes — Document, Element, and Text. JavaScript reads and writes this tree, not the HTML file.
  • Selection: Use querySelector (single) and querySelectorAll (multiple) as defaults. They accept any CSS selector. getElementById for performance-critical ID lookups.
  • Manipulation: createElement + appendChild for safe element creation. insertAdjacentHTML for efficient HTML injection. textContent for safe text (XSS-proof). innerHTML only with trusted data.
  • Attributes: getAttribute/setAttribute for all attributes. dataset for data-* attributes. getComputedStyle to read rendered styles.
  • classList: add, remove, toggle, contains, replace. Always prefer classList + CSS over inline styles.
  • Events: Three phases — Capturing (down) → Target → Bubbling (up). Default is bubbling. stopPropagation() stops travel; preventDefault() stops default action.
  • Event Delegation: One listener on parent, use event.target.closest() to identify child. Essential for dynamic lists.
  • Traversal: Use parentElement, children, nextElementSibling, previousElementSibling, firstElementChild, lastElementChild. Use closest() to walk up.
  • BOM: window (viewport, scroll), location (URL), history (navigation + pushState for SPA), navigator (browser info, online status).
  • Timers: setTimeout (once), setInterval (repeated). Always store the ID and clearTimeout/clearInterval to prevent leaks.
  • IntersectionObserver: Efficient viewport detection for lazy loading and infinite scroll. Use rootMargin to pre-load.
  • MutationObserver: Watch for DOM changes — childList, attributes, characterData. Replaces deprecated Mutation Events.

📋 DOM & BOM Quick Reference

// === SELECTION ===
document.querySelector('.card');         // First match or null
document.querySelectorAll('.card');      // Static NodeList
document.getElementById('app');          // By ID (fastest)

// === CREATION & INSERTION ===
const el = document.createElement('div');  // Create element
parent.appendChild(el);                    // Append child
parent.insertAdjacentHTML('beforeend', html); // Insert HTML
el.remove();                               // Remove element

// === CONTENT ===
el.textContent = 'Safe text';  // XSS-safe
el.innerHTML = '<b>HTML</b>'; // Parses HTML (careful!)

// === CLASSLIST ===
el.classList.add('active');     el.classList.remove('active');
el.classList.toggle('dark');    el.classList.contains('dark');

// === EVENTS ===
el.addEventListener('click', (e) => { ... });
e.stopPropagation();  e.preventDefault();
e.target.closest('.card');  // Event delegation

// === TRAVERSAL ===
el.parentElement;  el.children;  el.firstElementChild;
el.nextElementSibling;  el.previousElementSibling;
el.closest('.ancestor');  // Walk UP the tree

// === BOM ===
location.href;  location.pathname;  location.search;
history.pushState(state, '', url);  // SPA routing
navigator.onLine;  navigator.language;

// === TIMERS ===
const t = setTimeout(fn, ms);   clearTimeout(t);
const i = setInterval(fn, ms);  clearInterval(i);

// === OBSERVERS ===
new IntersectionObserver(callback, { threshold, rootMargin });
new MutationObserver(callback).observe(node, config);
Section 8

Interview Preparation — DOM & BOM Questions That Companies Ask

These are real questions asked at TCS, Infosys, Wipro, Accenture, Flipkart, Swiggy, CRED, and Zerodha interviews.

Q1: What is Event Delegation and why is it important?

Model Answer:

Event delegation is a pattern where you attach a single event listener to a parent element instead of individual listeners on each child. It works because of event bubbling — when a child is clicked, the event bubbles up to the parent where the listener catches it.

Benefits:

  • Performance: 1 listener vs 100+ listeners (less memory, faster setup)
  • Dynamic elements: Works for elements added after the listener was attached (no need to re-bind)
  • Simpler code: One handler with event.target.closest() logic
// Instead of this (bad for 100 items):
cards.forEach(card => card.addEventListener('click', handleClick));

// Do this (one listener for all):
grid.addEventListener('click', (e) => {
  const card = e.target.closest('.card');
  if (card) handleClick(card);
});

Real-world: Flipkart uses event delegation on their product grid (40+ cards), Swiggy on restaurant lists, and every React/Vue app uses it internally.

Q2: Explain the difference between Event Bubbling and Event Capturing.

Model Answer:

When you click an element, the event goes through three phases:

  1. Capturing (Phase 1): Event travels DOWN from windowdocument<html> → ... → target's parent
  2. Target (Phase 2): Event reaches the actual clicked element
  3. Bubbling (Phase 3): Event travels UP from target → parent → ... → documentwindow

By default, addEventListener listens during the bubbling phase. To listen during capturing, pass true as the third argument. Most developers only use bubbling because event delegation relies on it.

stopPropagation() stops the event from moving to the next element. preventDefault() stops the browser's default action (like following a link) — these are independent and often confused.

Q3: What is the difference between querySelector and getElementById? Which should you use?

Model Answer:

FeaturegetElementByIdquerySelector
Selector typeID only (no #)Any CSS selector
ReturnsElement or nullFirst match or null
SpeedSlightly faster (optimized internally)Very fast (negligible difference)
FlexibilityLow — IDs onlyHigh — classes, attributes, combinators

Recommendation: Use querySelector as the default — it's flexible, consistent, and the performance difference is negligible (nanoseconds). Use getElementById only when you specifically need an element by ID and maximum speed matters (e.g., a ticker updating 60 times/second).

Q4: How does IntersectionObserver work? Give a use case.

Model Answer:

IntersectionObserver is a browser API that asynchronously detects when an element enters or exits the viewport (or any specified root container). You create an observer with a callback, then call observe(element) on target elements.

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // Element is visible — load content, start animation, etc.
    }
  });
}, { threshold: 0.1, rootMargin: '200px' });

observer.observe(document.querySelector('.lazy-img'));

Use cases:

  • Lazy loading images: Load src from data-src when image nears viewport
  • Infinite scroll: Fetch more data when a sentinel element becomes visible
  • Scroll-triggered animations: Add animation classes when elements enter view
  • Ad impression tracking: Count an ad view only when it's actually visible

It replaces the old scroll event + getBoundingClientRect() approach, which was synchronous and caused layout thrashing.

Q5: What is history.pushState() and how is it used in SPAs?

Model Answer:

history.pushState(state, title, url) allows you to:

  1. Change the URL in the browser's address bar
  2. Add a new entry to the browser's history stack
  3. Store a state object associated with that history entry

Without causing a page reload.

In a Single Page Application (SPA) like PhonePe or Gmail:

// When user clicks "Payments" link:
history.pushState({ view: 'payments' }, '', '/payments');
renderPaymentsView(); // Update DOM without reload

// When user presses Back button:
window.addEventListener('popstate', (e) => {
  renderView(e.state.view); // Restore previous view
});

Without pushState, SPAs would show the same URL for every view, breaking browser back/forward, bookmarking, and link sharing. React Router, Vue Router, and Angular Router all use pushState internally.