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
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.
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
Learning Outcomes — Bloom's Taxonomy
| Bloom's Level | Learning Outcome |
|---|---|
| L1 — Remember | Recall DOM selection methods (getElementById, querySelector, querySelectorAll) and BOM objects (window, history, location, navigator) |
| L2 — Understand | Explain the difference between event bubbling and capturing, between innerHTML and textContent, and between NodeList and HTMLCollection |
| L3 — Apply | Use DOM manipulation methods (createElement, appendChild, removeChild, insertAdjacentHTML) to dynamically build UI components without page reload |
| L4 — Analyze | Debug event propagation issues using DevTools, trace DOM traversal paths, and diagnose performance problems caused by excessive DOM mutations |
| L5 — Evaluate | Compare IntersectionObserver vs scroll-event-based approaches for lazy loading and justify the performance benefits of the Observer pattern |
| L6 — Create | Build a complete interactive product listing page with dynamic card generation, infinite scroll, classList-driven animations, and SPA-style navigation using the History API |
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.
Three Types of Nodes You Must Know
| Node Type | nodeType Value | Example | Description |
|---|---|---|---|
| Document | 9 | document | The root of the tree — entry point for all DOM access |
| Element | 1 | <div>, <p>, <h1> | Every HTML tag becomes an Element node |
| Text | 3 | "Hello World" | The actual text inside elements — it's a separate node! |
ASCII Art: The DOM Tree
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.
| Method | Returns | Selector | Industry Use? |
|---|---|---|---|
getElementById('id') | Single Element / null | ID only | ✅ Fast for unique elements |
querySelector('css') | First matching Element / null | Any CSS selector | ✅ Industry default |
querySelectorAll('css') | Static NodeList | Any CSS selector | ✅ For multiple elements |
getElementsByClassName('cls') | Live HTMLCollection | Class name only | ⚠️ Rarely used |
getElementsByTagName('tag') | Live HTMLCollection | Tag 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');
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
| Feature | NodeList (querySelectorAll) | HTMLCollection (getElementsByClassName) |
|---|---|---|
| Live/Static? | Static — snapshot at query time | Live — 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
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)"
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!
});
// Hard to maintain, no transitions
el.style.color = 'red';
el.style.fontWeight = 'bold';
el.style.border = '2px solid red';// 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:
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.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.
<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
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
});
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);
});
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();
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.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 thaninnerHTMLwith 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 IntersectionObserverwatches the sentinel- When sentinel enters viewport → fetch next page →
insertAdjacentHTMLnew 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 navigationpopstateevent fires when user clicks back/forward- A router function matches the URL to a view and renders it
- Initial load checks
location.pathnameto 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.
Lab Exercises — Hands-On Practice
Lab 1: DOM Selection & Traversal — Element Explorer
Objective: Master all DOM selection and traversal methods using the Chrome Console.
Instructions:
- 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, anddata-*attributes. - Use
getElementById,querySelector,querySelectorAll,getElementsByClassNameto select various elements. Log the return types. - From the middle
<section>, traverse to: its parent, first child, last child, next sibling section, and previous sibling section using traversal properties. - Use
closest()to walk up from a deeply nested<li>to its parent<section>. - Convert a
NodeListto an Array and usefilter()to find all elements with a specificdata-*attribute value. - Demonstrate the difference between
childNodes(includes text) andchildren(elements only).
Lab 2: Dynamic Card Builder — Flipkart Product Grid
Objective: Build a product card grid entirely with JavaScript DOM manipulation — no hardcoded HTML cards.
Requirements:
- Create an array of 12 product objects:
{ id, name, price, mrp, discount, rating, image, category }. - Use
createElement()andappendChild()to build each card dynamically. Each card must have: image, title, price (with MRP strikethrough), discount badge, rating stars, and "Add to Cart" button. - Use
datasetto store product ID and category on each card. - Add event delegation on the grid — handle "Add to Cart" clicks with a single listener.
- Add filter buttons (All, Electronics, Clothing, Books). Use
classList.toggle('hidden')to show/hide cards by category. - Add a sort dropdown (Price: Low→High, High→Low, Rating). Re-sort and re-render cards.
- Style with CSS Grid and add a
classList.add('visible')fade-in animation on each card.
Lab 3: Event Bubbling & Delegation — Interactive Task Manager
Objective: Build a task manager that demonstrates event bubbling, delegation, and propagation control.
Requirements:
- Create a task list where each task item has: a checkbox (toggle complete), task text, an "Edit" button, and a "Delete" button.
- 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. - Checkbox click: toggle
classList('completed')on the task item (strikethrough style). - Delete button: use
element.remove()to remove the task from the DOM. - Edit button: replace the task text with an
<input>field (usereplaceChild). On Enter or blur, replace back with updated text. - Add a "New Task" form that uses
createElementto add tasks. Useevent.preventDefault()on form submit. - Add a console log inside each handler showing
event.target,event.currentTarget, andevent.eventPhase.
Lab 4: Lazy Loading & Infinite Scroll with IntersectionObserver
Objective: Implement Swiggy-style lazy loading and infinite scroll using IntersectionObserver.
Requirements:
- Create a page with 20 placeholder image cards. Each
<img>hassrc="placeholder.svg"anddata-src="actual-image.jpg". - Create an
IntersectionObserverthat watches all lazy images. When an image enters the viewport (withrootMargin: '200px'), swapdata-srcintosrc. - Add a CSS class
.loadedto fade in the image after it loads (use theloadevent on the<img>). - Implement infinite scroll: place a sentinel
<div>at the bottom. When observed, generate 10 more cards withinsertAdjacentHTML()and observe their images. - Show a loading spinner while new cards are being "fetched" (simulate with
setTimeout). - After 50 total cards,
disconnect()the scroll observer and show "No more items". - Display a counter: "Showing 20 of 50 items" that updates as more load.
Lab 5: Complete SPA — BOM-Powered Dashboard with Routing
Objective: Build a mini single-page application dashboard using the History API, BOM objects, and all DOM skills.
Requirements:
- Create a dashboard with 4 views: Home, Products, Analytics, Settings. Navigation between views must NOT reload the page.
- Use
history.pushState()to update the URL when navigating./,/products,/analytics,/settings. - Handle the
popstateevent so browser Back/Forward buttons work correctly. - The Products view should use
createElementto build a dynamic table of products. - The Analytics view should display:
window.innerWidth,window.innerHeight,navigator.userAgent,navigator.language,navigator.onLine,location.href. - The Settings view should have a dark mode toggle using
classList.toggle('dark')ondocument.body. Persist the preference inlocalStorage. - Add a
setInterval-based clock in the header showing live time. Clear it when leaving the page (beforeunload). - Use
MutationObserverto log every DOM change in the#appcontainer to the console. - Add smooth page transition animations using
classList.add('fade-out')before content swap andclassList.add('fade-in')after.
Deliverable: dashboard.html, dashboard.css, dashboard.js — single-page app with router.
MCQ Assessment Bank — 15 Questions
Hover over any question to reveal the answer. Each question is tagged with Bloom's Taxonomy level.
What does document.querySelector('.card') return if no element matches?
- An empty NodeList
undefinednull- Throws an error
null — querySelector 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.What is the key difference between innerHTML and textContent?
innerHTMLis fastertextContentparses HTML,innerHTMLdoes notinnerHTMLparses and renders HTML,textContenttreats everything as plain text- There is no difference
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.In event propagation, what is the default phase in which addEventListener listens?
- Capturing phase
- Target phase only
- Bubbling phase
- Both capturing and bubbling
addEventListener registers the handler for the bubbling phase. To listen during capturing, pass true as the third argument: addEventListener('click', fn, true).Which method adds a CSS class if it's missing and removes it if it's present?
classList.add()classList.replace()classList.toggle()classList.switch()
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).What does event.stopPropagation() do?
- Prevents the default browser action (e.g., following a link)
- Stops the event from propagating to parent/child elements
- Removes the event listener
- Cancels the event entirely, including the target phase
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.What is the output of document.querySelector('#menu').children.length for this HTML: <ul id="menu"> <li>A</li> <li>B</li> </ul>?
- 2
- 5
- 4
- 3
.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.Which insertAdjacentHTML position inserts content inside the element, after its last child?
'beforebegin''afterbegin''beforeend''afterend'
'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.What does history.pushState({page: 'about'}, '', '/about') do?
- Navigates to
/aboutand reloads the page - Changes the URL to
/aboutwithout reloading the page - Adds
/aboutto bookmarks - Opens
/aboutin a new tab
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.What is the advantage of IntersectionObserver over scroll event listeners for lazy loading?
- IntersectionObserver works offline
- IntersectionObserver runs off the main thread and doesn't cause jank
- IntersectionObserver is synchronous, so it's faster
- There is no advantage — they are identical in performance
getBoundingClientRect(). IntersectionObserver runs asynchronously, is managed by the browser off-thread, and only calls your callback when intersection state changes.What does element.closest('.container') do?
- Finds the nearest child with class
container - Walks up the DOM tree to find the nearest ancestor with class
container - Finds the closest sibling with class
container - Returns the element's computed styles
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.A developer uses setTimeout(fn, 0). When does fn execute?
- Immediately, before the next line of code
- After the current call stack clears and the event loop picks it up
- Exactly 0 milliseconds later
- Never — 0ms timeouts are ignored
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.What is Event Delegation and why is it used?
- Assigning events to the window object for global access
- Using a single listener on a parent to handle events from all children, improving performance for dynamic lists
- Delegating event handling to a Web Worker
- Removing events after they fire once
event.target or event.target.closest(). Used universally at Flipkart, Swiggy, and every production app.Which BOM property tells you if the user is currently connected to the internet?
window.isOnlinenavigator.onLinelocation.connecteddocument.online
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.A Zerodha developer is updating 500 stock prices per second using innerHTML on a table. What is the performance issue?
- No issue —
innerHTMLis always fast - Each
innerHTMLassignment causes the browser to reparse and rebuild all child nodes, triggering layout reflow innerHTMLdoesn't work on table cells- The browser throttles innerHTML to once per second
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).A developer needs to detect when a third-party script dynamically adds elements to a container. Which API should they use?
IntersectionObserverResizeObserverMutationObserverPerformanceObserver
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.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) andquerySelectorAll(multiple) as defaults. They accept any CSS selector.getElementByIdfor performance-critical ID lookups. - Manipulation:
createElement+appendChildfor safe element creation.insertAdjacentHTMLfor efficient HTML injection.textContentfor safe text (XSS-proof).innerHTMLonly with trusted data. - Attributes:
getAttribute/setAttributefor all attributes.datasetfordata-*attributes.getComputedStyleto 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. Useclosest()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 andclearTimeout/clearIntervalto prevent leaks. - IntersectionObserver: Efficient viewport detection for lazy loading and infinite scroll. Use
rootMarginto 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);
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:
- Capturing (Phase 1): Event travels DOWN from
window→document→<html>→ ... → target's parent - Target (Phase 2): Event reaches the actual clicked element
- Bubbling (Phase 3): Event travels UP from target → parent → ... →
document→window
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:
| Feature | getElementById | querySelector |
|---|---|---|
| Selector type | ID only (no #) | Any CSS selector |
| Returns | Element or null | First match or null |
| Speed | Slightly faster (optimized internally) | Very fast (negligible difference) |
| Flexibility | Low — IDs only | High — 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
srcfromdata-srcwhen 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:
- Change the URL in the browser's address bar
- Add a new entry to the browser's history stack
- 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.