Internet Programming Laboratory
Unit 5: JavaScript Functions, Events & Form Validation
Function Declarations vs Expressions vs Arrows, Closures, Callbacks, Higher-Order Functions, addEventListener(), Event Delegation, Preventing Defaults, HTML5 Validation API, Regex Validation, Real-Time Feedback, ARIA Accessibility — making web pages respond and validate.
🏢 Industry-Aligned | 📝 15 MCQs (Bloom's Taxonomy) | 🔬 5 Lab Exercises | 💼 Interview Prep
Why This Chapter Matters in 2025
In Unit 4 you learned the language of JavaScript. Now you learn the action. Functions, events, and form validation are what turn a static page into a living, breathing application. Without events, nothing happens when a user clicks a button. Without validation, garbage data enters your database. Without well-structured functions, your codebase collapses under its own weight.
Every tap on Swiggy, every swipe on CRED, every payment on Razorpay — all powered by event listeners wired to functions that validate data before sending it to the server. This unit teaches you how all three work together.
🏢 Industry Connection — Events Power Everything
Swiggy — When you type "biryani" in the search bar, a keyup event fires on every keystroke. A debounced callback waits 300ms after you stop typing, then triggers an API call. This prevents 50 unnecessary requests while you type. That's closures + events + callbacks working together.
CRED — Their signature dark-themed card selection UI is entirely event-driven. mouseover highlights the card, click selects it, focus/blur handles keyboard navigation for accessibility. Every animation is triggered by an addEventListener().
Razorpay — Their payment form validates card numbers using the Luhn algorithm in real-time. As you type your card number, an input event validates each digit, formats the number with spaces, and shows a red/green border — all before hitting the server. The submit event calls preventDefault() to validate everything client-side first.
Prerequisite Checklist ✅
- ✅ Completed Unit 4 (JavaScript Core & ES6+ — you need
const/let, arrow functions, template literals, destructuring) - ✅ VS Code with Live Server extension
- ✅ Chrome DevTools — Console tab for debugging, Elements tab for inspecting event listeners
- ✅ A working HTML form from Unit 2/3 (we'll add JavaScript validation to it)
addEventListener() method replaced the old onclick attribute pattern. Today, React's synthetic event system, Vue's v-on directives, and Angular's (click) bindings all compile down to native addEventListener() under the hood. Master the fundamentals, and every framework becomes easier.Learning Outcomes — Bloom's Taxonomy
| Bloom's Level | Learning Outcome |
|---|---|
| L1 — Remember | Recall the syntax for function declarations, function expressions, arrow functions, default parameters, addEventListener(), and the HTML5 Validation API methods (checkValidity(), setCustomValidity()) |
| L2 — Understand | Explain the difference between closures and callbacks, event bubbling vs capturing, preventDefault() vs stopPropagation(), and how event delegation works |
| L3 — Apply | Write JavaScript programs using map(), filter(), reduce(), closures, event delegation, regex-based validation, and ARIA attributes for form accessibility |
| L4 — Analyze | Debug event propagation issues, identify memory leaks from unremoved event listeners, and trace closure variable references using DevTools |
| L5 — Evaluate | Compare inline validation vs submit-time validation strategies, evaluate debouncing vs throttling for input events, and justify when to use event delegation over direct binding |
| L6 — Create | Build a complete, accessible, real-time validated registration form with regex patterns, custom error messages, ARIA labels, and event-driven UX feedback |
Concept Explanations — Theory, Earned
3.1 Functions Deep Dive — Declarations, Expressions & Arrows
Unit 4 introduced arrow functions briefly. Now let's go deep — understanding the differences between the three function styles is a top interview question at every Indian tech company.
Three Ways to Define Functions
| Style | Syntax | Hoisted? | this Binding | Use in 2025 |
|---|---|---|---|---|
| Declaration | function greet() {} | ✅ Yes | Dynamic (caller) | ✅ Named utilities |
| Expression | const greet = function() {} | ❌ No | Dynamic (caller) | ⚠️ Rare |
| Arrow | const greet = () => {} | ❌ No | Lexical (parent) | ✅ Default choice |
JavaScript
// ═══ FUNCTION DECLARATION — hoisted to top ═══
// You can call it BEFORE the declaration line
calculateGST(1000); // ✅ Works — hoisted!
function calculateGST(price, rate = 0.18) {
const gst = price * rate;
const total = price + gst;
return { price, gst, total };
}
// ═══ FUNCTION EXPRESSION — NOT hoisted ═══
// calculateDiscount(1000); // ❌ ReferenceError!
const calculateDiscount = function(price, percent = 10) {
return price * (percent / 100);
};
// ═══ ARROW FUNCTION — INDUSTRY STANDARD ═══
// Concise, lexical `this`, perfect for callbacks
const add = (a, b) => a + b; // Implicit return
const greet = name => `Hello, ${name}!`; // Single param: no parens
const getUser = () => ({ name: 'Rahul', age: 25 }); // Return object: wrap in ()
// Multi-line arrow function
const processOrder = (items, coupon) => {
let total = items.reduce((sum, item) => sum + item.price, 0);
if (coupon) total *= 0.9;
return { subtotal: total, gst: total * 0.18 };
};
Default Parameters
JavaScript
// Default parameters — ES6+ (replaces the old `param || default` hack)
const createOrder = (
item,
qty = 1,
currency = 'INR',
gstRate = 0.18
) => {
const subtotal = item.price * qty;
const gst = subtotal * gstRate;
return { item: item.name, qty, subtotal, gst, currency };
};
// All defaults used
createOrder({ name: 'Paneer Tikka', price: 250 });
// { item: 'Paneer Tikka', qty: 1, subtotal: 250, gst: 45, currency: 'INR' }
// Override specific defaults
createOrder({ name: 'Biryani', price: 350 }, 2, 'INR', 0.05);
// qty=2, gstRate=5%
Arrow functions don't have their own this. They inherit this from the enclosing scope (lexical binding). This means you cannot use arrow functions as object methods that need this: const obj = { name: 'A', greet: () => this.name } — this.name is undefined! Use a regular function or shorthand method instead.
3.2 Closures & Callbacks
Closures — Functions That Remember
A closure is a function that retains access to variables from its outer (enclosing) function's scope, even after the outer function has returned. This is one of the most powerful — and most asked — concepts in JavaScript.
JavaScript
// ═══ CLOSURE — The counter pattern ═══
function createCounter(initialValue = 0) {
let count = initialValue; // This variable is "closed over"
return {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; }
};
}
const cart = createCounter();
cart.increment(); // 1
cart.increment(); // 2
cart.decrement(); // 1
cart.getCount(); // 1
// `count` is private — you can't access it directly!
console.log(cart.count); // undefined ✅ (encapsulation)
useState, useEffect) are powered by closures internally.JavaScript
// 🏢 REAL-WORLD CLOSURE: Debounce (used at Swiggy for search)
function debounce(fn, delay = 300) {
let timer; // Closed over — persists between calls
return (...args) => {
clearTimeout(timer); // Cancel previous timer
timer = setTimeout(() => {
fn(...args); // Call after delay
}, delay);
};
}
// Usage at Swiggy — search only fires 300ms after user STOPS typing
const handleSearch = debounce((query) => {
fetch(`/api/search?q=${query}`);
}, 300);
searchInput.addEventListener('input', (e) => {
handleSearch(e.target.value);
});
Callbacks — Functions as Arguments
JavaScript
// A CALLBACK is a function passed to another function as an argument
// The receiving function "calls it back" when it's ready
// ═══ Synchronous callback ═══
const numbers = [10, 25, 5, 40, 15];
// sort() accepts a callback — the comparison function
numbers.sort((a, b) => a - b);
console.log(numbers); // [5, 10, 15, 25, 40]
// ═══ Asynchronous callback ═══
setTimeout(() => {
console.log('This runs after 2 seconds');
}, 2000);
// ═══ Custom function with callback ═══
const fetchData = (url, onSuccess, onError) => {
fetch(url)
.then(res => res.json())
.then(data => onSuccess(data))
.catch(err => onError(err));
};
fetchData(
'https://api.example.com/users',
(data) => console.log('Got users:', data), // success callback
(err) => console.error('Failed:', err) // error callback
);
addEventListener, every setTimeout, every .then() — they all accept callbacks. Master callbacks first, and Promises and async/await become trivial to understand.3.3 Higher-Order Functions — map(), filter(), reduce()
A higher-order function (HOF) is a function that either (1) takes a function as an argument, or (2) returns a function. The "Big Three" array HOFs — map, filter, reduce — are the most used functions in modern JavaScript.
JavaScript
// 🏢 Sample data — Swiggy restaurant menu
const menu = [
{ name: 'Chicken Biryani', price: 320, isVeg: false, rating: 4.5 },
{ name: 'Paneer Tikka', price: 250, isVeg: true, rating: 4.2 },
{ name: 'Masala Dosa', price: 120, isVeg: true, rating: 4.8 },
{ name: 'Butter Chicken', price: 380, isVeg: false, rating: 4.6 },
{ name: 'Veg Thali', price: 180, isVeg: true, rating: 4.0 },
];
// ═══ map() — Transform each item (returns new array) ═══
const menuCards = menu.map(item => ({
display: `${item.name} — ₹${item.price}`,
badge: item.isVeg ? '🟢 VEG' : '🔴 NON-VEG'
}));
// [{ display: 'Chicken Biryani — ₹320', badge: '🔴 NON-VEG' }, ...]
// ═══ filter() — Keep items that pass a test ═══
const vegItems = menu.filter(item => item.isVeg);
// Only Paneer Tikka, Masala Dosa, Veg Thali
const premiumItems = menu.filter(item => item.price > 200 && item.rating >= 4.5);
// Chicken Biryani, Butter Chicken
// ═══ reduce() — Reduce array to a single value ═══
const totalPrice = menu.reduce((sum, item) => sum + item.price, 0);
// 320 + 250 + 120 + 380 + 180 = 1250
const avgRating = menu.reduce((sum, item) => sum + item.rating, 0) / menu.length;
// (4.5 + 4.2 + 4.8 + 4.6 + 4.0) / 5 = 4.42
// ═══ CHAINING — The real power ═══
// "Get formatted names of veg items sorted by rating"
const result = menu
.filter(item => item.isVeg)
.sort((a, b) => b.rating - a.rating)
.map(item => `${item.name} (${item.rating}⭐)`);
// ['Masala Dosa (4.8⭐)', 'Paneer Tikka (4.2⭐)', 'Veg Thali (4.0⭐)']
const vegNames = [];
for (let i = 0; i < menu.length; i++) {
if (menu[i].isVeg) {
vegNames.push(menu[i].name);
}
}const vegNames = menu
.filter(item => item.isVeg)
.map(item => item.name);
// Cleaner, no mutation,
// easier to readfilter() to apply category/price/rating filters, sort() for ordering, and map() to transform API data into UI-ready components. A single product page might chain 5-6 array methods together.3.4 Event Handling — addEventListener()
Events are signals fired by the browser when something happens: a user clicks, types, scrolls, submits a form, or the page finishes loading. addEventListener() is the modern, industry-standard way to listen for events.
JavaScript
// ═══ addEventListener() — The modern way ═══
// Syntax: element.addEventListener(eventType, callbackFn, options)
const btn = document.querySelector('#submit-btn');
// Basic click handler
btn.addEventListener('click', (event) => {
console.log('Button clicked!');
console.log('Event target:', event.target); // The element clicked
console.log('Event type:', event.type); // 'click'
console.log('Timestamp:', event.timeStamp); // When it happened
});
// Multiple listeners on the same element — all fire!
btn.addEventListener('click', () => console.log('Handler 1'));
btn.addEventListener('click', () => console.log('Handler 2'));
// Both fire — unlike onclick which overwrites
// Removing an event listener (must use named function)
const handleClick = () => console.log('Clicked!');
btn.addEventListener('click', handleClick);
btn.removeEventListener('click', handleClick); // ✅ Must be same reference
<button onclick="handleClick()">) and DOM Level 0 properties (btn.onclick = function() {}) are outdated. Inline handlers mix HTML and JS, and onclick can only have ONE handler (the second overwrites the first). Always use addEventListener().Anonymous functions cannot be removed. If you write btn.addEventListener('click', () => { ... }), you can never remove that listener because you have no reference to pass to removeEventListener(). For listeners you need to remove (e.g., cleanup in SPAs), always use named functions.
3.5 Event Types & Event Delegation
Common Event Types
| Category | Event | Fires When... | Common Use Case |
|---|---|---|---|
| Mouse | click | Element is clicked | Buttons, links, cards |
| Mouse | dblclick | Element is double-clicked | Text selection, zoom |
| Mouse | mouseover | Cursor enters element | Tooltips, hover effects |
| Mouse | mouseout | Cursor leaves element | Hide tooltips |
| Keyboard | keydown | Key is pressed down | Shortcuts, game controls |
| Keyboard | keyup | Key is released | Search input, filters |
| Form | submit | Form is submitted | Validation before send |
| Form | change | Input value committed (blur) | Dropdowns, checkboxes |
| Form | input | Input value changes (real-time) | Live search, char count |
| Focus | focus | Element gains focus | Highlight active field |
| Focus | blur | Element loses focus | Validate on leave |
JavaScript
// ═══ KEYBOARD EVENTS ═══
const searchInput = document.querySelector('#search');
searchInput.addEventListener('keydown', (e) => {
console.log('Key pressed:', e.key); // 'Enter', 'a', 'Shift'
console.log('Key code:', e.code); // 'Enter', 'KeyA', 'ShiftLeft'
if (e.key === 'Enter') {
console.log('Search submitted!');
}
// Keyboard shortcuts
if (e.ctrlKey && e.key === 'k') {
e.preventDefault(); // Prevent browser default
openSearchModal();
}
});
// ═══ MOUSE EVENTS ═══
const card = document.querySelector('.product-card');
card.addEventListener('mouseover', () => {
card.style.transform = 'scale(1.02)';
card.style.boxShadow = '0 8px 24px rgba(0,0,0,0.12)';
});
card.addEventListener('mouseout', () => {
card.style.transform = 'scale(1)';
card.style.boxShadow = 'none';
});
// ═══ FOCUS / BLUR ═══
const emailField = document.querySelector('#email');
emailField.addEventListener('focus', () => {
emailField.parentElement.classList.add('active-field');
});
emailField.addEventListener('blur', () => {
emailField.parentElement.classList.remove('active-field');
validateEmail(emailField.value); // Validate when user leaves field
});
Preventing Default Behavior
JavaScript
// ═══ preventDefault() — Stop the browser's default action ═══
// 1. Prevent form submission (most common use)
const form = document.querySelector('#registration-form');
form.addEventListener('submit', (e) => {
e.preventDefault(); // Don't reload the page!
// Validate, then submit via fetch()
if (validateForm()) {
submitViaAjax();
}
});
// 2. Prevent link navigation
document.querySelector('a.no-nav').addEventListener('click', (e) => {
e.preventDefault(); // Don't follow the href
showModal(); // Do something else instead
});
// 3. Prevent right-click context menu (e.g., on image gallery)
document.querySelector('.gallery').addEventListener('contextmenu', (e) => {
e.preventDefault();
showCustomMenu(e.clientX, e.clientY);
});
Event Delegation — The Industry Pattern
Instead of adding an event listener to every child element, add ONE listener to the parent and use event.target to determine which child was clicked. This is crucial for dynamic content.
JavaScript
// 🏢 EVENT DELEGATION — Used everywhere at Flipkart, Swiggy
// ❌ BAD: Adding listener to each button (100 products = 100 listeners)
document.querySelectorAll('.add-to-cart').forEach(btn => {
btn.addEventListener('click', (e) => { /* ... */ });
});
// ✅ GOOD: ONE listener on the parent container
const productList = document.querySelector('#product-list');
productList.addEventListener('click', (e) => {
// Find the closest .add-to-cart button (even if user clicked an icon inside it)
const btn = e.target.closest('.add-to-cart');
if (!btn) return; // Click wasn't on a button — ignore
const productId = btn.dataset.id;
addToCart(productId);
console.log(`Added product ${productId} to cart`);
});
// ✅ Works for dynamically added products too!
// New products added via JS automatically get the listener
.on() with delegation was one of its killer features.JavaScript
// ═══ Event Bubbling vs Capturing ═══
// Events have 3 phases:
// 1. CAPTURING: document → parent → target (top-down)
// 2. TARGET: event reaches the clicked element
// 3. BUBBLING: target → parent → document (bottom-up) ← DEFAULT
// By default, addEventListener listens during BUBBLING phase
parent.addEventListener('click', handler); // Bubbling (default)
parent.addEventListener('click', handler, true); // Capturing
// stopPropagation() — Stop the event from bubbling further
child.addEventListener('click', (e) => {
e.stopPropagation(); // Parent's click handler won't fire
console.log('Only child handles this');
});
3.6 HTML5 Form Validation API
HTML5 provides a built-in Constraint Validation API that lets you validate form fields without writing regex from scratch for common patterns. This API works with HTML attributes like required, minlength, maxlength, pattern, type="email", etc.
HTML
<!-- HTML5 validation attributes -->
<form id="signup-form" novalidate>
<input type="text" id="name" required minlength="2" maxlength="50">
<input type="email" id="email" required>
<input type="tel" id="phone" required pattern="[6-9]\d{9}">
<input type="password" id="password" required minlength="8">
<button type="submit">Sign Up</button>
</form>
JavaScript
// ═══ checkValidity() — Returns true/false ═══
const form = document.querySelector('#signup-form');
const emailInput = document.querySelector('#email');
// Check single field
console.log(emailInput.checkValidity()); // true or false
// Check entire form
console.log(form.checkValidity()); // true only if ALL fields valid
// ═══ validity property — Detailed validation state ═══
const v = emailInput.validity;
console.log(v.valid); // true if all constraints pass
console.log(v.valueMissing); // true if required field is empty
console.log(v.typeMismatch); // true if type="email" but value isn't email
console.log(v.tooShort); // true if shorter than minlength
console.log(v.tooLong); // true if longer than maxlength
console.log(v.patternMismatch); // true if doesn't match pattern attribute
// ═══ setCustomValidity() — Custom error messages ═══
const passwordInput = document.querySelector('#password');
passwordInput.addEventListener('input', () => {
const val = passwordInput.value;
if (val.length < 8) {
passwordInput.setCustomValidity('Password must be at least 8 characters');
} else if (!/[A-Z]/.test(val)) {
passwordInput.setCustomValidity('Must include at least one uppercase letter');
} else if (!/[0-9]/.test(val)) {
passwordInput.setCustomValidity('Must include at least one number');
} else {
passwordInput.setCustomValidity(''); // ← Empty string = VALID
}
});
// ═══ Form submit with validation ═══
form.addEventListener('submit', (e) => {
e.preventDefault();
if (!form.checkValidity()) {
form.reportValidity(); // Shows browser's built-in error tooltips
return;
}
// All valid — submit via fetch()
console.log('Form is valid! Submitting...');
});
novalidate to your <form> tag when you're handling validation with JavaScript. This prevents the browser's default validation tooltips from conflicting with your custom validation UI. You control when reportValidity() or your custom error messages appear.3.7 Regex-Based Custom Validation
While HTML5 validation handles basics, real-world forms need custom regex patterns — Indian phone numbers, PAN cards, Aadhaar numbers, IFSC codes, GST numbers, UPI IDs.
JavaScript
// ═══ INDIAN VALIDATION PATTERNS ═══
// Indian mobile number: starts with 6-9, followed by 9 digits
const PHONE_REGEX = /^[6-9]\d{9}$/;
// Email: basic pattern
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
// PAN Card: 5 letters, 4 digits, 1 letter
const PAN_REGEX = /^[A-Z]{5}[0-9]{4}[A-Z]$/;
// Aadhaar: 12 digits (with optional spaces every 4)
const AADHAAR_REGEX = /^\d{4}\s?\d{4}\s?\d{4}$/;
// IFSC Code: 4 letters, 0, 6 alphanumeric
const IFSC_REGEX = /^[A-Z]{4}0[A-Z0-9]{6}$/;
// UPI ID: username@bankname
const UPI_REGEX = /^[\w.-]+@[\w]+$/;
// Password: min 8 chars, 1 uppercase, 1 lowercase, 1 digit, 1 special
const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
// ═══ Validation function ═══
const validateField = (value, regex, fieldName) => {
if (!value.trim()) {
return { valid: false, message: `${fieldName} is required` };
}
if (!regex.test(value)) {
return { valid: false, message: `Invalid ${fieldName} format` };
}
return { valid: true, message: '' };
};
// Usage
validateField('9876543210', PHONE_REGEX, 'Phone');
// { valid: true, message: '' }
validateField('12345', PHONE_REGEX, 'Phone');
// { valid: false, message: 'Invalid Phone format' }
validateField('rahul@okhdfcbank', UPI_REGEX, 'UPI ID');
// { valid: true, message: '' }
rahul@okicici, PhonePe validates the format client-side using regex, then makes a server call to verify the VPA (Virtual Payment Address) actually exists. The input event handles format checking; the blur event triggers the server verification. Two layers of validation working together.3.8 Real-Time Input Feedback
Modern forms don't wait for submission to show errors. They validate as the user types using the input event, and show validation status on blur (when the user leaves the field). This is the UX pattern used by every major company.
JavaScript
// 🏢 REAL-TIME VALIDATION — The Flipkart/Razorpay pattern
const setupFieldValidation = (inputId, regex, errorMsg) => {
const input = document.getElementById(inputId);
const errorSpan = document.getElementById(`${inputId}-error`);
// Real-time feedback as user types
input.addEventListener('input', () => {
const value = input.value.trim();
if (value === '') {
// Reset — user is still typing
input.className = '';
errorSpan.textContent = '';
} else if (regex.test(value)) {
// Valid ✅
input.className = 'valid';
errorSpan.textContent = '✅';
errorSpan.style.color = '#16a34a';
} else {
// Invalid ❌ (but don't show full error yet — user is still typing)
input.className = 'invalid';
errorSpan.textContent = '';
}
});
// Show full error message when user LEAVES the field
input.addEventListener('blur', () => {
const value = input.value.trim();
if (value !== '' && !regex.test(value)) {
errorSpan.textContent = errorMsg;
errorSpan.style.color = '#dc2626';
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
});
};
// Setup validation for each field
setupFieldValidation('phone', /^[6-9]\d{9}$/, 'Enter a valid 10-digit Indian mobile number');
setupFieldValidation('email', /^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Enter a valid email address');
setupFieldValidation('pan', /^[A-Z]{5}[0-9]{4}[A-Z]$/, 'PAN format: ABCDE1234F');
CSS
/* Visual feedback styles */
input.valid { border: 2px solid #16a34a; background: #f0fdf4; }
input.invalid { border: 2px solid #dc2626; background: #fef2f2; }
.error-msg { font-size: 0.82rem; min-height: 1.2em; margin-top: 4px; }
3.9 Accessibility in Forms — ARIA Labels
Accessible forms are not optional — they're legally required in many countries and are a core criterion in Google's Lighthouse audit. At companies like Flipkart and Razorpay, accessibility is part of the code review checklist.
aria-label, aria-describedby, aria-invalid, aria-required, aria-live, and role="alert".HTML
<!-- ═══ ACCESSIBLE FORM — Best practices ═══ -->
<form id="payment-form" novalidate aria-label="Payment Details Form">
<!-- 1. Always use <label> with for/id connection -->
<div class="field-group">
<label for="card-number">Card Number</label>
<input
type="text"
id="card-number"
inputmode="numeric"
autocomplete="cc-number"
aria-required="true"
aria-describedby="card-number-hint card-number-error"
placeholder="1234 5678 9012 3456"
/>
<span id="card-number-hint" class="hint">Enter 16-digit card number</span>
<span id="card-number-error" class="error" role="alert" aria-live="polite"></span>
</div>
<!-- 2. For inputs without visible labels, use aria-label -->
<input type="search" aria-label="Search products" placeholder="Search..."/>
<!-- 3. Submit button with clear text -->
<button type="submit" aria-label="Pay ₹1,250">Pay ₹1,250</button>
</form>
JavaScript
// ═══ ARIA-aware validation ═══
const showError = (input, errorId, message) => {
const errorEl = document.getElementById(errorId);
errorEl.textContent = message;
input.setAttribute('aria-invalid', 'true');
input.classList.add('invalid');
};
const clearError = (input, errorId) => {
const errorEl = document.getElementById(errorId);
errorEl.textContent = '';
input.setAttribute('aria-invalid', 'false');
input.classList.remove('invalid');
};
// When the error message changes, aria-live="polite" announces it
// to screen reader users automatically — no extra JS needed!
aria-live="polite" on error message containers. When the text content changes, screen readers will announce the new message to the user. Use "polite" for validation messages (waits for the user to stop) and "assertive" for critical errors (interrupts immediately).Industry Problems — Case Studies
🏢 Case Study 1: Swiggy — Search Debouncing with Closures & Events
The Problem: Swiggy's search bar fires an API request on every keystroke. If a user types "chicken biryani" (16 characters), that's 16 API calls — 15 of which are wasted because the user hasn't finished typing. At 20 million daily searches, this means billions of unnecessary server requests.
The Solution: Implement a debounce function using closures. The function waits 300ms after the user stops typing before firing the API call. If the user types another character within 300ms, the previous timer is cancelled.
JavaScript
// Swiggy's search implementation (simplified)
const debounce = (fn, delay) => {
let timer; // Closure — persists between calls
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
const searchRestaurants = async (query) => {
if (query.length < 2) return;
const loader = document.querySelector('.search-loader');
loader.style.display = 'block';
try {
const res = await fetch(`/api/restaurants/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
renderResults(data.restaurants);
} catch (err) {
showError('Search failed. Please try again.');
} finally {
loader.style.display = 'none';
}
};
// Debounced version — waits 300ms
const debouncedSearch = debounce(searchRestaurants, 300);
// Event listener on the search input
document.querySelector('#search-input')
.addEventListener('input', (e) => debouncedSearch(e.target.value));
Key Concepts Used: Closures (debounce timer), callbacks (event handler), input event, async/await, encodeURIComponent() for URL-safe queries.
🏢 Case Study 2: Flipkart — Multi-Step Registration Form Validation
The Problem: Flipkart's registration form collects name, email, phone, and password across a single page. They need: real-time validation as the user types, custom error messages (not browser defaults), Indian phone number validation, and password strength indicators — all while being accessible to screen readers.
The Solution: Combine HTML5 Validation API with custom regex patterns, input events for real-time feedback, and ARIA attributes for accessibility.
JavaScript
// Flipkart-style form validation
const validators = {
name: {
regex: /^[a-zA-Z\s]{2,50}$/,
message: 'Name must be 2-50 characters, letters only'
},
email: {
regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Please enter a valid email address'
},
phone: {
regex: /^[6-9]\d{9}$/,
message: 'Enter 10-digit Indian mobile number (starting 6-9)'
},
password: {
regex: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/,
message: 'Min 8 chars: 1 uppercase, 1 lowercase, 1 digit, 1 special char'
}
};
// Setup all fields with real-time + blur validation
Object.entries(validators).forEach(([field, { regex, message }]) => {
const input = document.getElementById(field);
const errorEl = document.getElementById(`${field}-error`);
input.addEventListener('input', () => {
if (regex.test(input.value)) {
input.classList.replace('invalid', 'valid') || input.classList.add('valid');
errorEl.textContent = '';
input.setAttribute('aria-invalid', 'false');
}
});
input.addEventListener('blur', () => {
if (input.value && !regex.test(input.value)) {
input.className = 'invalid';
errorEl.textContent = message;
input.setAttribute('aria-invalid', 'true');
}
});
});
Key Concepts Used: Regex validation, Object.entries(), higher-order function (forEach with closure), input and blur events, ARIA attributes, classList API.
🏢 Case Study 3: Razorpay — Payment Form with Event Delegation & Real-Time UPI Validation
The Problem: Razorpay's payment form lets users choose between UPI, Card, and Netbanking. Each payment method shows different fields dynamically. They need event delegation for the dynamically rendered fields, real-time UPI ID validation (format: user@bank), and card number formatting (adding spaces every 4 digits as the user types).
The Solution: Use event delegation on the form container, closures for formatting state, and the input event for real-time validation.
JavaScript
// Razorpay-style payment form
const paymentForm = document.querySelector('#payment-form');
// Event delegation — handles all dynamic inputs
paymentForm.addEventListener('input', (e) => {
const { id, value } = e.target;
switch (id) {
case 'upi-id':
validateUPI(e.target, value);
break;
case 'card-number':
e.target.value = formatCardNumber(value);
validateCardNumber(e.target, value.replace(/\s/g, ''));
break;
case 'card-expiry':
e.target.value = formatExpiry(value);
break;
}
});
// UPI validation
const validateUPI = (input, value) => {
const isValid = /^[\w.-]+@[\w]+$/.test(value);
input.className = value ? (isValid ? 'valid' : 'invalid') : '';
};
// Card number formatting: "4111111111111111" → "4111 1111 1111 1111"
const formatCardNumber = (value) => {
return value
.replace(/\D/g, '') // Remove non-digits
.slice(0, 16) // Max 16 digits
.replace(/(\d{4})/g, '$1 ') // Add space every 4
.trim();
};
// Submit with full validation
paymentForm.addEventListener('submit', (e) => {
e.preventDefault();
if (!paymentForm.checkValidity()) {
paymentForm.reportValidity();
return;
}
processPayment();
});
Key Concepts Used: Event delegation, input event, submit event with preventDefault(), regex for UPI/card validation, string formatting with replace(), checkValidity()/reportValidity().
Lab Exercises — Hands-On Practice
Lab 1: Functions & Higher-Order Functions — Menu Processor
Objective: Master function declarations, expressions, arrow functions, default parameters, and map()/filter()/reduce().
Instructions:
- Create an array of 8 restaurant menu items (objects with:
name,price,category['starter', 'main', 'dessert'],isVeg,rating). - Write a function declaration
calculateGST(price, rate = 0.05)that returns the price with GST. - Write a function expression
formatPricethat formats a number as"₹1,250.00"usingtoLocaleString('en-IN'). - Write an arrow function
getBadgethat returns'🟢 VEG'or'🔴 NON-VEG'based onisVeg. - Use
filter()to get only veg items with rating ≥ 4.0. - Use
map()to create display strings:"🟢 Paneer Tikka — ₹250.00 (4.2⭐)". - Use
reduce()to calculate the total price of all main course items. - Chain
filter → sort → mapto get top 3 highest-rated items as formatted strings.
Lab 2: Closures & Callbacks — Counter & Debounce
Objective: Understand closures through practical patterns — counters, private state, and debouncing.
Requirements:
- Build a cart counter using closures:
createCart()returns an object withaddItem(name, price),removeItem(name),getTotal(),getItems(), andgetCount(). Theitemsarray must be private (not accessible directly). - Build a rate limiter using closures:
createRateLimiter(maxCalls, windowMs)returns a function that only executes the callback if it hasn't been called more thanmaxCallstimes in the lastwindowMsmilliseconds. - Implement a debounce function and wire it to a text input. Log each keystroke to show the difference between debounced and non-debounced calls.
- Build a memoize function:
memoize(fn)caches results for previously seen arguments. Test it with an expensive calculation (e.g., factorial).
Lab 3: Event Handling — Interactive Product Card Gallery
Objective: Master addEventListener(), event delegation, keyboard/mouse events, and preventDefault().
Requirements:
- Create a product gallery with 6 cards (use template literals to generate HTML).
- Use event delegation — ONE click listener on the parent container handles: "Add to Cart" button clicks, "Wishlist" heart icon toggles, card click to show details.
- Add
mouseover/mouseoutfor a hover zoom effect on product images. - Add a search input with
keyupevent that filters products in real-time (case-insensitive). - Add keyboard navigation:
ArrowLeft/ArrowRightto navigate between cards,Enterto select. - Show a "Quick View" modal on card click — use
Escapekey to close it. - Prevent the default form submission when the search input is inside a
<form>tag.
Lab 4: Form Validation — Flipkart-Style Registration
Objective: Build a complete registration form with HTML5 Validation API, regex patterns, real-time feedback, and ARIA accessibility.
Requirements:
- Create a form with fields: Full Name, Email, Phone (Indian), Password, Confirm Password.
- Add
novalidateto the form — handle all validation via JavaScript. - Use
checkValidity()andsetCustomValidity()for basic validation. - Add regex validation for: Indian phone (
/^[6-9]\d{9}$/), email, password strength. - Show real-time feedback: green border + ✅ when valid, red border + error message on blur when invalid.
- Add a password strength meter: Weak (red), Medium (yellow), Strong (green) — based on length, uppercase, numbers, special chars.
- Confirm Password must match Password — validate on both
inputandblur. - Add
aria-invalid,aria-describedby,aria-live="polite"for all error messages. - On submit, display all form data in a styled summary card (no page reload).
Lab 5: Complete App — PhonePe-Style UPI Payment Form
Objective: Build a UPI payment form combining all concepts: functions, closures, events, validation, delegation, and accessibility.
Requirements:
- Payment method selector: Three tabs — UPI, Card, Netbanking. Clicking a tab shows the relevant fields (event delegation). Only one method visible at a time.
- UPI tab: Input for UPI ID with regex validation (
user@bank). Show green ✅ when valid format. Add a "Verify" button that simulates an API call (usesetTimeoutas a fake async call). - Card tab: Card number input that auto-formats with spaces every 4 digits (using
inputevent). Expiry date input that auto-adds "/" after MM. CVV input limited to 3 digits. - Amount field: Validate amount is between ₹1 and ₹1,00,000. Format display with Indian number formatting.
- Debounced validation: Use a debounce closure so validation doesn't fire on every keystroke — wait 200ms.
- Accessibility: All fields have labels,
aria-describedbyfor hints,aria-invalidfor errors,role="alert"for error messages. - Submit flow:
preventDefault(), validate all visible fields, show a confirmation modal with payment summary, then show "Payment Successful ✅" (simulated). - Style with CSS — dark payment card theme, smooth transitions, green/red validation borders.
Deliverable: payment.html, payment.css, payment.js — all using ES6+ syntax with no inline handlers.
MCQ Assessment Bank — 15 Questions
Hover over any question to reveal the answer. Each question is tagged with Bloom's Taxonomy level.
Which of the following is a key difference between arrow functions and regular functions?
- Arrow functions are always hoisted
- Arrow functions have their own
thisbinding - Arrow functions inherit
thisfrom their enclosing scope (lexicalthis) - Arrow functions cannot accept parameters
this — they inherit it from the surrounding lexical scope. This makes them ideal for callbacks and event handlers inside class methods or closures, but unsuitable as object methods that need their own this.What does the following code output? const add = (a, b = 10) => a + b; console.log(add(5));
515NaNundefined
15 — When only one argument is passed, the default parameter b = 10 is used. So add(5) computes 5 + 10 = 15. Default parameters activate only when the argument is undefined, not when it's null or 0.What is a closure in JavaScript?
- A function defined inside a loop
- A function that retains access to variables from its outer scope even after the outer function has returned
- A function that is immediately invoked
- A function that returns
undefined
What does [10, 20, 30].reduce((acc, val) => acc + val, 0) return?
[10, 20, 30]600undefined
60 — reduce() iterates through the array, accumulating a single value. Starting with 0, it adds: 0+10=10, 10+20=30, 30+30=60. The second argument 0 is the initial accumulator value.What is the correct way to add a click event listener to a button in modern JavaScript?
<button onclick="handleClick()">button.onclick = handleClick;button.addEventListener('click', handleClick);- All three are equally recommended
addEventListener() is the modern, industry-standard approach. It supports multiple listeners on the same event, works with capturing/bubbling phases, and can be removed with removeEventListener(). Options A and B are legacy patterns — A mixes HTML/JS, and B only allows one handler per event.What does event.preventDefault() do?
- Stops the event from bubbling up to parent elements
- Removes the event listener
- Prevents the browser's default action for that event (e.g., form submission, link navigation)
- Prevents JavaScript errors
preventDefault() cancels the browser's default behavior — for example, preventing a form from submitting and reloading the page, or preventing a link from navigating. It does NOT stop event propagation — use stopPropagation() for that.What is event delegation?
- Adding an event listener to every child element individually
- Delegating event handling to a Web Worker
- Attaching a single event listener to a parent element and using
event.targetto handle child events - Using
setTimeoutto delay event handling
event.target (or event.target.closest()) to identify which child was clicked. This works for dynamically added elements too.Which event fires continuously as the user types in an input field?
changeblurinputsubmit
input — The input event fires on every keystroke, paste, or programmatic change. change fires only when the user commits the value (usually on blur). input is used for real-time search, character counters, and live validation.What does input.checkValidity() return?
- The input's current value
- A validity state object
trueif the input satisfies all its validation constraints,falseotherwise- An error message string
checkValidity() returns a boolean — true if the element satisfies all HTML5 validation constraints (required, pattern, minlength, type, etc.), false otherwise. For detailed state info, use the .validity property which gives a ValidityState object.What does setCustomValidity('') (empty string) do?
- Sets a custom error message
- Clears any custom validation error — marks the field as valid
- Resets the input value to empty
- Disables validation on the field
setCustomValidity('') with an empty string clears the custom error, marking the field as valid. Any non-empty string marks it as invalid. This is how you toggle between valid/invalid states in custom validation logic. You must call this on every validation check.Which regex correctly validates an Indian mobile number (10 digits, starting with 6-9)?
/^\d{10}$//^[6-9]\d{9}$//^[1-9]\d{9}$//^[6-9]{10}$/
/^[6-9]\d{9}$/ — Indian mobile numbers start with 6, 7, 8, or 9, followed by exactly 9 more digits. Option A allows numbers starting with 0-5 (invalid). Option C allows 1-5 (invalid). Option D requires ALL 10 digits to be 6-9 (too restrictive).A developer uses map() to filter items from an array. Is this correct?
- Yes,
map()can filter and transform - No —
map()always returns an array of the same length; usefilter()for removal - Yes, but only for primitive arrays
- No —
map()is only for objects
map() transforms each element but always returns an array with the SAME number of elements. It cannot remove items. Use filter() to remove items, then map() to transform. Chaining them (.filter().map()) is the correct pattern.Which ARIA attribute should be set when a form field has a validation error?
aria-required="true"aria-hidden="true"aria-invalid="true"aria-disabled="true"
aria-invalid="true" — This tells screen readers that the field contains an invalid value. It should be set to "true" when validation fails and "false" (or removed) when validation passes. Combined with aria-describedby pointing to the error message, this provides a complete accessible validation experience.A Swiggy developer wants to validate search input but avoid firing validation on every keystroke. Which pattern should they use?
- Throttling with
setInterval - Debouncing with closures — wait until the user stops typing
- Using
changeevent instead ofinput - Validating only on form submit
A developer builds a to-do list where new tasks are added dynamically via JavaScript. Click handlers on delete buttons stop working for new items. What's the best fix?
- Re-attach event listeners every time a new item is added
- Use
onclickattribute in the HTML template - Use event delegation — attach one listener to the parent
<ul>and checkevent.target - Use
MutationObserverto detect new elements
Chapter Summary
🎯 Unit 5 — Key Takeaways
- Functions: Arrow functions are the default. Function declarations are hoisted; expressions/arrows are not. Arrow functions have lexical
this. - Default Parameters:
function greet(name = 'Guest')— activate only when argument isundefined. - Higher-Order Functions:
map()transforms,filter()selects,reduce()aggregates. Chain them for powerful data pipelines. - Closures: Functions that retain access to their outer scope's variables. Powers debounce, throttle, memoize, private state, and React Hooks.
- Callbacks: Functions passed as arguments. Foundation of JS's event-driven, asynchronous architecture.
- addEventListener(): The only correct way to attach event handlers. Supports multiple handlers, capturing/bubbling, and removal.
- Event Types:
click,submit,keydown/keyup,mouseover/mouseout,focus/blur,input,change— know when each fires. - Event Delegation: ONE listener on parent, use
event.target.closest()to find child. Essential for dynamic content. - preventDefault(): Stops browser defaults (form submit, link navigation).
stopPropagation()stops bubbling. - HTML5 Validation API:
checkValidity()returns boolean,reportValidity()shows tooltips,setCustomValidity('')clears errors. - Regex Validation: Indian phone
/^[6-9]\d{9}$/, PAN/^[A-Z]{5}[0-9]{4}[A-Z]$/, UPI/^[\w.-]+@[\w]+$/. - Real-Time Feedback:
inputevent for typing,blurfor full error messages. Green/red borders + error text. - Accessibility:
aria-invalid,aria-describedby,aria-live="polite",role="alert", proper<label>associations.
📋 Functions, Events & Validation Quick Reference
// === FUNCTIONS ===
function greet(name) { return `Hi ${name}`; } // Declaration (hoisted)
const greet = function(name) { return name; }; // Expression
const greet = (name) => `Hi ${name}`; // Arrow (implicit return)
const fn = (x, y = 10) => x + y; // Default params
// === HIGHER-ORDER FUNCTIONS ===
arr.map(x => x * 2); // Transform each element
arr.filter(x => x > 5); // Keep matching elements
arr.reduce((acc, x) => acc + x, 0); // Reduce to single value
// === CLOSURES ===
function outer() {
let count = 0;
return () => ++count; // Inner fn "closes over" count
}
// === EVENTS ===
el.addEventListener('click', handler); // Add
el.removeEventListener('click', handler); // Remove (needs ref)
e.preventDefault(); // Stop default action
e.stopPropagation(); // Stop bubbling
e.target.closest('.class'); // Event delegation helper
// === VALIDATION ===
input.checkValidity(); // Returns boolean
input.setCustomValidity('Error msg'); // Set custom error
input.setCustomValidity(''); // Clear error = valid
form.reportValidity(); // Show browser tooltips
// === REGEX (Indian) ===
/^[6-9]\d{9}$/ // Phone
/^[A-Z]{5}[0-9]{4}[A-Z]$/ // PAN
/^[\w.-]+@[\w]+$/ // UPI ID
/^\d{4}\s?\d{4}\s?\d{4}$/ // Aadhaar
Interview Preparation — Functions, Events & Validation Questions
These are real questions asked at TCS, Infosys, Wipro, Accenture, Flipkart, Swiggy, CRED, and startup interviews for frontend developer roles.
Q1: What is a closure? Give a real-world use case.
Model Answer:
A closure is when a function "remembers" variables from its outer scope even after the outer function has finished executing. The inner function maintains a reference to the outer function's variable environment.
// Practical example: Debounce (used at Swiggy for search)
function debounce(fn, delay) {
let timer; // This is the "closed over" variable
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// The returned function "remembers" `timer` between calls
// Even though debounce() has already returned
Real-world use cases: debouncing search inputs, creating private variables (counter pattern), memoization for caching expensive calculations, and factory functions. React's useState Hook internally uses closures to persist state between renders.
Q2: What is the difference between event.target and event.currentTarget?
Model Answer:
event.target— The element that triggered the event (the actual element the user clicked/interacted with).event.currentTarget— The element that the event listener is attached to (the one withaddEventListener).
// If you click on a <span> inside a <button>:
// <button id="btn"><span>Click me</span></button>
btn.addEventListener('click', (e) => {
console.log(e.target); // <span> (what was actually clicked)
console.log(e.currentTarget); // <button> (where listener is attached)
});
This distinction is critical for event delegation — when you listen on a parent, event.target tells you which child was clicked, while event.currentTarget is always the parent. Use event.target.closest('.selector') to reliably find the intended target element.
Q3: Explain map(), filter(), and reduce() with a practical example.
Model Answer:
These are higher-order functions that take callbacks and are used for declarative data transformations:
const orders = [
{ item: 'Pizza', price: 299, delivered: true },
{ item: 'Burger', price: 149, delivered: false },
{ item: 'Biryani', price: 349, delivered: true },
];
// filter: Keep only delivered orders
const delivered = orders.filter(o => o.delivered);
// map: Get formatted strings
const names = delivered.map(o => `${o.item} — ₹${o.price}`);
// reduce: Calculate total revenue of delivered orders
const revenue = delivered.reduce((sum, o) => sum + o.price, 0);
// 299 + 349 = 648
Key differences: map() always returns an array of same length (transforms each item). filter() returns a shorter or equal array (removes items). reduce() returns a single value (aggregates). They're chainable: .filter().sort().map().
Q4: 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 you click a child, the event travels up through all ancestors.
// Instead of 1000 listeners on 1000 list items:
const list = document.querySelector('ul#products');
list.addEventListener('click', (e) => {
const item = e.target.closest('li');
if (!item) return;
console.log('Selected:', item.dataset.id);
});
Benefits: (1) Performance — one listener vs thousands. (2) Dynamic content — new items added via JavaScript automatically work without re-attaching listeners. (3) Memory — fewer listeners = less memory usage. React's event system uses delegation under the hood — one root listener handles all events.
Q5: How would you implement real-time form validation with accessibility?
Model Answer:
I'd use a three-layer approach:
- HTML5 attributes for basic validation:
required,type="email",minlength,pattern. - JavaScript validation for custom rules using the
inputevent (real-time) andblurevent (full error on leave). - ARIA attributes for screen readers.
// Real-time validation with accessibility
const input = document.getElementById('email');
const error = document.getElementById('email-error');
input.addEventListener('input', () => {
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.value)) {
input.setAttribute('aria-invalid', 'false');
input.classList.replace('invalid', 'valid');
error.textContent = '';
}
});
input.addEventListener('blur', () => {
if (input.value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.value)) {
input.setAttribute('aria-invalid', 'true');
error.textContent = 'Please enter a valid email';
// aria-live="polite" on error element announces this to screen readers
}
});
The key pattern is: validate silently on input (show green when correct), show error messages on blur (so users aren't interrupted while typing). Use aria-invalid, aria-describedby, and aria-live so screen reader users get the same feedback as sighted users.