Term 2 · Module 7 of 8

JavaScript DOM Manipulation

Website Development

What is the DOM?

The Document Object Model (DOM) is the bridge between JavaScript and a webpage. It is a structured, object-oriented representation of the HTML content that the browser creates when a page loads. The DOM treats the HTML document as a tree of nodes, where each HTML element becomes a JavaScript object.

Without the DOM, JavaScript would have no way to interact with the page. With the DOM, we can:

  • Change text and attributes without reloading
  • Show or hide elements based on user actions
  • Create animations and effects
  • Build interactive components (forms, sliders, etc.)

Examples from real sites — Amazon’s live cart count, social media comment updates, reaction buttons — all rely on JavaScript manipulating the DOM.

Exam tip: The DOM is language-independent (used by Python, Ruby, etc.), but in web development we interact with it purely through JavaScript in the browser.

DOM Tree Structure

Every time an HTML page is loaded, the browser builds a tree of objects. Each HTML element becomes a node; relationships between nodes (parent, child, sibling) define the tree’s hierarchy.

  • The document object is the top-level entry point (parent of everything).
  • Every node has properties like children, parentNode, nextSibling.
  • Each node is a full JavaScript object with its own properties and methods.

The Document Object

The document object is created automatically by the browser when the page loads. It can be accessed directly in the console or in scripts.

console.log(document);           // shows the HTML representation
console.dir(document);           // shows the JavaScript object with all properties

console.dir(document) reveals a massive object with properties such as:

PropertyDescription
titleThe page <title> text
bodyThe <body> element object
childrenHTMLCollection of top-level children (usually <html>)
allAll elements in the document
URLCurrent page URL
anchorsCollection of anchor (<a>) elements
headThe <head> element object
formsCollection of <form> elements

These properties themselves are objects that can be drilled into with dot notation:

document.body.children          // HTMLCollection of <header>, <main>, <script>, etc.
document.body.children[0]       // first child of <body>
document.title                  // returns the page title string

Navigating the DOM

Because the DOM is a tree, you can traverse it step by step:

document.body.children          // direct children of <body>
document.body.children[1]       // second child (often <main>)
document.body.children[1].children[0]  // first child of <main>

Each node also has properties for relationships:

  • parentNode
  • firstChild
  • lastChild
  • nextSibling
  • previousSibling

Exam tip: children returns only element nodes (ignoring text/whitespace). Use childNodes to get all node types.

Manipulating Elements (Quick Examples)

Once you have a reference to a DOM node, you can change its content or style in real time.

Change text content:

document.body.children[1].children[0].innerText = "Haha you are hacked!";

Change background color:

document.body.style.backgroundColor = "yellow";

Change inner HTML (includes tags):

element.innerHTML = "<strong>New content</strong>";

These simple manipulations show the power of the DOM: no page reload needed.

Key takeaways

  • The DOM is a tree of JavaScript objects representing the HTML structure.
  • document is the entry point – everything starts from it.
  • Every element is an object with properties (e.g., title, body, children) and methods (e.g., style, innerText).
  • Use console.dir(document) to explore the live DOM object.
  • Navigate using dot notation and index-based children.
  • Manipulation is immediate – change text, HTML, or CSS on the fly.

Connecting JavaScript to HTML

To manipulate a web page with JavaScript, you must first link the JS code to the HTML document. Without this connection, the browser cannot run your scripts or interact with the DOM.

Three ways to embed JavaScript

MethodHow it worksBest forPitfalls
InlineCode placed directly inside an HTML element’s event attribute, e.g., onclick="alert('Hi')"Quick tests of single-line actionsMixes logic with markup; quotation mark conflicts; unreadable as project grows
InternalCode written inside a <script> tag in the HTML file (in <head> or <body>)Small projects or one-off scriptsStill couples JS with HTML; not reusable across pages
ExternalCode in a separate .js file, linked via <script src="file.js">All real-world projectsRequires correct file path and placement

Exam tip: Never use inline JavaScript in production. It violates separation of concerns, becomes unmanageable, and confuses quote nesting — exactly like mixing inline CSS into HTML.

Where to place the <script> tag

Script placement determines when the JS code runs relative to the HTML elements it needs to interact with.

  • Best practice: place the <script> tag just before the closing </body> tag. This ensures all DOM nodes are created before JS tries to access them.
  • Result: no null-reference errors and faster perceived load time (HTML renders first, JS executes after).

Worked example: script in <head> vs. <body>

Given HTML with <h1>Original</h1> and a script that tries to change it:

<!-- BAD: script in <head> -->
<head>
  <script>
    document.querySelector('h1').innerText = 'Changed'; // runs before <h1> exists → null error
  </script>
</head>

<!-- GOOD: script at bottom of <body> -->
<body>
  <h1>Original</h1>
  <!-- ... -->
  <script>
    document.querySelector('h1').innerText = 'Changed'; // works fine
  </script>
</body>

Key takeaways

  • Inline JS is quick but messy — avoid beyond one-liners.
  • External JS (separate .js file) is the cleanest, most reusable method.
  • Place <script> at the bottom of <body> to guarantee all elements are loaded.
  • Placing scripts in <head> frequently causes null errors when the script attempts to access DOM elements that don't exist yet.

Selecting DOM Elements

Before you can modify a web page, you must select the element(s) you want to interact with. JavaScript provides several methods on the global document object to do this.

Old-style selectors (still valid but less flexible)

MethodReturnsExampleNotes
document.getElementById('id')Single elementdocument.getElementById('main-paragraph')IDs must be unique; fast
document.getElementsByClassName('class')HTMLCollection (live, array-like)document.getElementsByClassName('main-div')Index with [0] to get first
document.getElementsByTagName('tag')HTMLCollectiondocument.getElementsByTagName('li')Use index to access specific item

HTMLCollection is a live collection – it updates automatically when the DOM changes. It does not have forEach or other array methods (though you can convert it with Array.from).

Modern, powerful selector: querySelector and querySelectorAll

These methods accept any CSS selector string, making them far more flexible.

MethodReturnsExample
document.querySelector(selector)First matching element (or null)document.querySelector('.main-div')
document.querySelectorAll(selector)NodeList (static, array-like)document.querySelectorAll('li img')

NodeList is static (does not update when DOM changes) and supports forEach directly.

CSS selector examples (same as in stylesheets)

// By ID (use #)
document.querySelector('#main-paragraph')

// By class (use .)
document.querySelector('.main-div')

// By tag name
document.querySelector('h1')

// Descendant
document.querySelectorAll('.main-div img')

// Complex: image inside an li
document.querySelectorAll('li img')

Exam tip: Prefer querySelector and querySelectorAll over old methods. They are more powerful, easier to remember (use CSS syntax you already know), and cover every selection scenario.

Assigning selections to variables

Once selected, you can store the element reference in a variable and reuse it.

const paragraph = document.getElementById('main-paragraph');
paragraph.innerText = 'Modified text';
paragraph.style.backgroundColor = 'brown';

Why use const for DOM elements? Variables assigned to objects (including DOM elements) store a reference to the object, not a copy. Using const prevents accidental reassignment of the variable to a different element, but still allows you to change the element's properties.

const arr = [1, 2];
arr[0] = 23;    // allowed – array content changes
// arr = [3,4];   // error – cannot reassign a const

const heading = document.querySelector('h1');
heading.innerText = 'New'; // allowed
// heading = somethingElse; // error – keeps reference stable

Key difference: HTMLCollection vs. NodeList

  • getElementsByClassName / getElementsByTagName → HTMLCollection (live, no forEach)
  • querySelectorAll → NodeList (static, has forEach)

Worked example: changing the first image’s alt text

const images = document.querySelectorAll('img'); // NodeList
images[0].alt = 'ha ha ha';   // change alt of first image

Key takeaways

  • Select by ID with getElementById or querySelector('#id') – always returns a single element.
  • Select by class/tag with getElementsByClassName/getElementsByTagName returns an HTMLCollection.
  • Best practice: use querySelector (first match) and querySelectorAll (all matches) for any CSS selector.
  • Store selections in variables – prefer const for stability (the element reference is fixed, properties remain changeable).
  • All selection methods are called on document (e.g., document.querySelector()).

DOM Traversing

DOM traversal is the ability to navigate the relationships between elements in the DOM tree — moving to an element's parent, children, or siblings. Think of the DOM as a family tree: each element is a node connected to others. Traversal methods let you target elements based on their position, essential for dynamic web interactions.

Parent Access

  • parentElement – returns the immediate parent HTML element of the selected element.
    • Chaining .parentElement repeatedly climbs the tree: element.parentElement.parentElement gives the grandparent.
const h2 = document.querySelector('h2');
h2.parentElement;           // <section>
h2.parentElement.parentElement; // <main>

Note: The root of the tree is document; its parent is null.

Children Access

  • children – returns a live HTMLCollection of only HTML child elements (ignores text nodes, comments).
    • To get all child nodes including text, use childNodes (rarely needed for traversal).
const body = document.querySelector('body');
body.children;   // [<header>, <main>, <script>, ...]
  • firstElementChild – first child element.
  • lastElementChild – last child element.
  • childElementCount – number of child elements (equivalent to children.length).
body.firstElementChild;  // <header>
body.lastElementChild;   // <script>
body.childElementCount;  // 3 (example)

Sibling Access

  • nextElementSibling – the next sibling element (following the same parent).
  • previousElementSibling – the previous sibling element.

Chaining these moves horizontally through the tree.

const divMain = document.querySelector('.main');
divMain.nextElementSibling;          // <p>
divMain.nextElementSibling.nextElementSibling; // <button>
divMain.previousElementSibling;      // null (if first child)

Visualising Traversal

Worked Example: Climbing the Tree

Given this HTML structure:

<body>
  <header>
    <div class="main">...</div>
    <p>...</p>
    <button>...</button>
  </header>
  <main>
    <section>...</section>
    <section>...</section>
  </main>
</body>
// Start with the first <li> inside a list
const li = document.querySelector('li');
li.parentElement;                     // <ul>
li.parentElement.parentElement;       // <section> or parent container
li.children;                          // [<br>, <img>] (HTMLCollection)
li.nextElementSibling;                // next <li> (if any)
li.previousElementSibling;            // previous <li> (if any)

Property Summary

PropertyReturnsUse
parentElementSingle elementClimb one level up
childrenLive HTMLCollectionAll child elements
firstElementChildSingle elementFirst child
lastElementChildSingle elementLast child
childElementCountNumberCount children
nextElementSiblingSingle elementMove forward
previousElementSiblingSingle elementMove backward

Exam tip: children returns only elements, not text nodes. If you need all nodes (including whitespace), use childNodes. But for traversal, always prefer the element-specific properties — they are robust and what exam questions expect.

Key Takeaways

  • parentElement climbs up; children, firstElementChild, lastElementChild go down.
  • nextElementSibling / previousElementSibling move sideways among siblings.
  • childElementCount gives the number of child elements quickly.
  • Chaining these methods navigates the tree in any direction (up, down, sideways).
  • All methods return null or empty collections when nothing exists — always check before further chaining.

Modifying Styles and Attributes with JavaScript

JavaScript + the DOM lets you change how a page looks and behaves on the fly — reacting to user actions, time, or any condition. At its core: you grab an element and rewrite its visual presentation or its HTML attributes.

The style Property – Direct Inline Changes

Every DOM element has a .style property that reflects its inline styles. You can read or set any CSS property, but the property names must be written in camelCase (not hyphenated):

CSS propertyJavaScript .style access
background-colorelement.style.backgroundColor
font-familyelement.style.fontFamily
font-sizeelement.style.fontSize
borderelement.style.border

Example – turn the navbar background blue:

const navBar = document.querySelector('nav');
navBar.style.backgroundColor = 'blue';

Values are always strings; include units where needed:

navBar.style.border = '2px solid black';

Limitation: .style only sees and modifies inline styles — it ignores styles from external or internal CSS sheets. If you try to read a property that comes from a stylesheet, you get null.

Why it’s problematic – Mixes presentation (CSS) with logic (JS), harder to maintain, and cannot read the full computed style of an element.


The classList API – The Right Way to Style

Instead of writing inline styles, define your styles in CSS classes and toggle them with JavaScript. The classList property gives you a DOMTokenList (array-like object) of all classes on an element.

Key methods:

MethodEffect
element.classList.add('class-name')Adds a class
element.classList.remove('class-name')Removes a class
element.classList.toggle('class-name')Adds if absent, removes if present
element.classList.contains('class-name')Returns true/false

Example workflow:

.sample {
  color: green;
  background-color: red;
  text-decoration: solid;
}
const section = document.querySelector('section');
section.classList.add('sample');       // applies the green/red style
section.classList.remove('sample');    // removes it
section.classList.toggle('sample');    // toggles
console.log(section.classList.contains('sample')); // false (after removal)

Exam tip: Always prefer classList over .style for production code. It keeps styles in CSS, makes debugging easier, and lets you reuse the same class across many elements.


Working with Other Attributes

Every HTML attribute (e.g., href, src, id, alt) can be read or changed using two generic methods:

  • element.getAttribute('attribute-name') – returns the current value.
  • element.setAttribute('attribute-name', 'new-value') – sets the value (overwrites existing).

Example – change the first link’s href:

const firstLink = document.querySelector('a');
console.log(firstLink.getAttribute('href'));   // e.g., "events.html"
firstLink.setAttribute('href', 'https://govind.com');
firstLink.setAttribute('id', 'new-id');         // also works for any attribute

Warning: Using setAttribute('style', ...) overwrites ALL inline styles – avoid it for styling. Use classList instead.


Text Content: innerText , textContent , innerHTML

Three properties control what text (and HTML) lives inside an element. They differ in what they read and how they treat markup.

PropertyWhat it returnsSecurity concernUse case
innerTextVisible text only (respects CSS display:none)SafeReading or setting plain visible text
textContentAll raw text, including hidden text and whitespaceSafeWhen you need the full raw string (e.g., for comparison)
innerHTMLText plus any nested HTML tags; parses them as elementsDangerous with user input (XSS)When you must insert structured HTML (only with trusted content)

Example – given a list item <li>Only one guest allowed <br> <img src="..."></li>:

const li = document.querySelector('li');
console.log(li.innerText);      // "Only one guest allowed" (no br, no image text)
console.log(li.textContent);    // "Only one guest allowed \n " (includes newline)
console.log(li.innerHTML);      // "Only one guest allowed <br> <img src="...">"

// Setting:
li.innerText = 'Any number of guests';   // Replaces everything (including br/img) with plain text
li.innerHTML = '<span>Hi</span>';        // Inserts a <span> element

Exam tip: Never use innerHTML with user-generated input (comments, search queries). Use innerText or textContent to avoid cross-site scripting (XSS) vulnerabilities.


Key takeaways – Styles & Attributes

  • .style gives access to inline styles only; property names use camelCase.
  • classList is the preferred way to style via JS – .add(), .remove(), .toggle(), .contains().
  • Use getAttribute() and setAttribute() for any HTML attribute except style.
  • setAttribute('style', ...) overrides all inline styles – avoid.

Key takeaways – Text Content

  • innerText – visible text only.
  • textContent – all raw text, including hidden.
  • innerHTML – parses HTML; only use with trusted content.
  • Setting any of these replaces all children of the element.

DOM Manipulation: Creating, Appending, Removing, and Inserting Elements

DOM manipulation lets you dynamically add, remove, or rearrange elements on a page. Instead of hard‑coding everything in HTML, you use JavaScript to modify the DOM tree at runtime – the engine behind interactive apps and single‑page interfaces.

Creating Elements

Before you can insert an element, you must create it. The method document.createElement(tagName) returns a new, empty DOM node of the given tag (e.g., 'p', 'div', 'span'). It exists only in memory until attached to the page.

Example

const newParagraph = document.createElement('p');
newParagraph.innerText = 'This P was added with JavaScript';

The element is now a node object with an innerText property, but it is not yet visible in the document.

Appending Elements

Once created, an element must be attached to a parent node. Two primary methods exist: appendChild() and the newer, more flexible append().

appendChild(parentNode, childNode)

  • Appends one child node to the end of the parent’s children list.
  • The child must be a node (e.g., a p element created with createElement).
  • Cannot insert text strings directly.
const section1 = document.querySelector('section');
section1.appendChild(newParagraph);

append(parentNode, ...nodesOrStrings)

  • Adds one or more children or strings to the end of the parent.
  • Strings are converted to text nodes automatically – they are not parsed as HTML.
  • More versatile than appendChild.
const newPara2 = document.createElement('p');
newPara2.innerText = 'Second dynamic paragraph';
section1.append(newPara2, 'Some random text', '<span>this is treated as text, not HTML</span>');
MethodAccepts multiple children?Accepts text strings?Returns
appendChildNo (one node at a time)NoThe appended child node
appendYesYes (as text)undefined

Removing Elements

Two ways to delete an element from the DOM: directly on the element itself, or via its parent.

remove() on the Target Element

Simply call element.remove(). No parent reference needed – the element is detached from the DOM.

const listItem1 = document.querySelector('li'); // first <li>
listItem1.remove(); // gone from the page

removeChild() on the Parent Element

Requires both the parent and the child node you want to remove.

const ul = document.querySelector('ul');
const firstChild = ul.firstElementChild;
ul.removeChild(firstChild);

Key difference: remove() is cleaner when you already have a reference to the element; removeChild() forces you to know the parent.

Inserting Elements at Specific Positions

Appending always puts a child at the end. To control exactly where an element appears, use insertBefore() or insertAdjacentElement().

insertBefore(parentNode, newNode, referenceNode)

  • Inserts newNode before referenceNode (which must be a child of the parent).
  • referenceNode is the sibling before which the new node goes.
const ul = document.querySelector('ul');
const listItem2 = ul.children[1]; // second <li>
const newP = document.createElement('p');
newP.innerText = 'Inserted before second list item';
ul.insertBefore(newP, listItem2);

insertAdjacentElement(position, targetElement, newElement)

  • Inserts newElement at a position relative to targetElement – no parent required.
  • Four position strings (case‑sensitive):
PositionMeaning
'beforebegin'Before the target element itself (outside)
'afterbegin'Inside the target, before its first child
'beforeend'Inside the target, after its last child
'afterend'After the target element itself (outside)

Example – inserting before and inside:

const li1 = document.querySelector('#adjacent li:first-child');
const pBefore = document.createElement('p');
pBefore.innerText = 'Added beforebegin';
li1.insertAdjacentElement('beforebegin', pBefore);

const li2 = document.querySelector('#adjacent li:nth-child(2)');
const pAfterBegin = document.createElement('p');
pAfterBegin.innerText = 'Added afterbegin';
li2.insertAdjacentElement('afterbegin', pAfterBegin);

Comparison of insertion methods:

MethodUses parent?PositionsCan insert multiple nodes?
append / appendChildYesOnly end of childrenOnly append can do multiple
insertBeforeYesBefore a specified childNo
insertAdjacentElementNo (relative to an element)Four exact positionsNo (but call repeatedly)

Exam tip: insertAdjacentElement is the most flexible for precise positioning. Remember the four position strings – they are often tested. Note that 'beforeend' is inside the element at the end, while 'beforebegin' is outside before it.

Key Takeaways

  • Use document.createElement(tagName) to create a new element in memory.
  • appendChild() appends a single node to a parent; append() can add multiple nodes and text strings.
  • element.remove() deletes the element directly; parent.removeChild(child) needs both references.
  • insertBefore(parent, new, ref) places a node before a sibling child.
  • insertAdjacentElement(position, target, new) gives four precise placement options relative to the target element.
  • Always ensure the target elements exist in the DOM before manipulating them – check for null if using querySelector.

Event Listeners

Events are signals sent by the browser to JavaScript that something has happened – a user clicked, typed, hovered, or a page finished loading. They are the core mechanism behind interactivity: without events, a webpage is static; with events, JavaScript can react and update the DOM.

How the browser handles events (the flow)

  1. User triggers an action – e.g., clicks a button, presses a key, scrolls.
  2. Browser detects the action – creates an event and fires it on the target element.
  3. JavaScript responds – runs code attached to that event (the listener).

Intuition: Think of events as “when something happens, do something else.” The browser shouts “Hey, button clicked!”, and your JavaScript wakes up and runs the code you wrote for that moment.

Three ways to attach event handlers

MethodExampleProfile
Inline HTML (old, messy)<button onclick="alert('Clicked!')">Hard to maintain, mixes HTML & JS, avoid
DOM property (better, still not ideal)button.onclick = function() { … }Only one handler per event type; overwrites previous
addEventListener (gold standard)button.addEventListener('click', handler)Multiple handlers per event, clean separation, modern

Exam tip: Interviewers love to ask why addEventListener is preferred over the other two. Always mention: it allows multiple listeners, works with all event types uniformly, and doesn’t override existing handlers.

Inline example (don’t use)

<button onclick="alert('I was clicked')">Click me</button>

Property example (not recommended)

const button = document.querySelector('button');
button.onclick = function() {
  alert('I was clicked');
};

This works, but if you later assign another onclick, it replaces the first.

addEventListener – the modern way

Syntax:

element.addEventListener(type, listener);
  • type – a string naming the event (e.g. 'click', 'mouseover', 'keydown').
  • listener – a callback function that runs when the event occurs. This function automatically receives an event object as its first argument.

Worked example (background colour change)

Given a <button> in the HTML:

<button type="button">Change background</button>

Step 1 – Define the handler function (using classList is best practice):

function changeBg() {
  document.body.classList.add('bg-blue');
}

With CSS:

.bg-blue { background-color: blue; }

Step 2 – Attach the listener:

const button = document.querySelector('button');
button.addEventListener('click', changeBg);

Now clicking the button applies the class.

Alternative – anonymous function (also works):

button.addEventListener('click', function() {
  document.body.style.backgroundColor = 'cyan';
});

Alternative – arrow function (modern):

button.addEventListener('click', () => {
  document.body.style.backgroundColor = 'cyan';
});

Exam tip: Named functions are more readable and reusable; anonymous functions are convenient for one-off logic. Arrow functions do not have their own this (important for certain contexts like object methods).

The Event Object

When the listener runs, JavaScript automatically passes an event object as the first argument to the callback. You can name it anything (common names: e, event, evt).

button.addEventListener('click', function(e) {
  console.log(e);           // entire event object (e.g., PointerEvent)
  console.log(e.target);    // the element that triggered the event
  console.log(e.currentTarget); // the element the listener is attached to
});
  • e.target – the element that was actually clicked (useful if you have nested elements).
  • e.currentTarget – the element the listener is attached to (often the same, but differs with event delegation).

Common event types (selected)

CategoryEventsWhen fired
Mouseclick, dblclick, mouseover, mouseout, mousedown, mouseup, contextmenu (right-click)Cursor interactions
Keyboardkeydown, keyup, keypress (deprecated)Key presses
Formsubmit, change, input, focus, blurForm field interactions
Document / WindowDOMContentLoaded, load, scroll, resizePage lifecycle
Touchtouchstart, touchend, touchmoveTouchscreen devices

A full reference is maintained on MDN: Event reference.

Exam tip: For mouse events, remember click = mousedown + mouseup on the same element. For keyboard events, keydown fires repeatedly while holding a key; keyup fires once on release.

Key takeaways

  • An event is a signal that something happened; event listeners let JavaScript react.
  • Three attachment methods exist; addEventListener is the gold standard.
  • addEventListener(type, listener) – type is a string, listener is a callback.
  • The callback receives an event object providing details (e.g., e.target).
  • Named functions, anonymous functions, and arrow functions all work as listeners.
  • Always separate JS from HTML; never use inline onclick attributes in production.

Painting Website

This project ties together HTML, CSS, and JavaScript (DOM manipulation, events, loops) to build an interactive painting canvas. The goal: hover over a grid of pixel‑divs and color them, but only while holding the Alt key — simulating a brush. The project also demonstrates event delegation for performance when handling thousands of child elements.

1. HTML Structure

Minimal markup: a color input, a paragraph of instructions, and a container <div> that will hold the pixel grid. All pixels are created dynamically with JavaScript.

<h1>Painting Canvas</h1>
<label for="color">Select Color</label>
<input type="color" id="color" value="#000000">
<p>Press Alt + move over canvas to paint</p>
<div class="container"></div>

2. CSS Grid Canvas

The container is turned into a responsive grid. Each grid cell is a “pixel” — a <div> with class .pixel. The grid uses 1fr columns so the canvas scales.

.container {
  border: 2px solid black;
  display: grid;
  grid-template-columns: repeat(150, 1fr);
  width: 75vw;
  height: 60vh;
}

Why 150 columns? Creates small square cells. With 5000 total pixels, we get about 30 rows.

3. JavaScript: Creating Pixels Dynamically

A loop generates 5000 <div> elements, each with a unique id and class pixel. They are appended to the container.

const canvas = document.querySelector('.container');
for (let i = 0; i < 5000; i++) {
  const pixel = document.createElement('div');
  pixel.setAttribute('id', i + 1);
  pixel.setAttribute('class', 'pixel');
  canvas.appendChild(pixel);
}

4. Adding Event Listeners (Inefficient Approach)

The naive way: attach a mouseover listener to every pixel inside the loop. When the event fires, a function runs that:

  1. Checks if the Alt key was pressed (event.altKey).
  2. Reads the current value of the color input (document.querySelector('#color').value).
  3. Sets the background-color of the pixel that triggered the event (using event.currentTarget).
pixel.addEventListener('mouseover', function(e) {
  if (e.altKey) {
    const color = document.querySelector('#color').value;
    e.currentTarget.style.backgroundColor = color;
  }
});

Why e.currentTarget? It refers to the element on which the listener was attached — in this case, the individual pixel. e.target would also work here because there’s no nesting inside the pixel.

5. The Better Way: Event Delegation

Attaching 5000 listeners is wasteful. Instead, use event bubbling — set a single listener on the parent container. Events that occur on any child pixel bubble up to the container. Then inside the handler, use e.target to identify the actual pixel that was hovered.

canvas.addEventListener('mouseover', function(e) {
  if (e.altKey && e.target.classList.contains('pixel')) {
    const color = document.querySelector('#color').value;
    e.target.style.backgroundColor = color;
  }
});

Event Bubbling vs. Capturing

  • Events bubble from the target element up through its ancestors.
  • e.target = the element that originally triggered the event (the pixel).
  • e.currentTarget = the element with the event listener (the container, in the delegation version).

Exam tip: Event delegation is the standard way to handle events on many dynamic children. It saves memory and works even for elements added after the listener is attached.

6. Conditional Painting (Alt Key)

Only color a pixel when the Alt key is held down. This prevents accidental scribbling. The property e.altKey is true if the Alt key was pressed when the event fired.

if (e.altKey) {
  // apply color
}

7. Worked Example (Step‑by‑Step Flow)

  1. User selects a color (e.g., green) from the <input type="color">.
  2. User holds the Alt key and moves the mouse over the canvas.
  3. A mouseover event fires on a pixel div.
  4. The event bubbles to the container, which has the listener.
  5. The handler checks: e.altKey === true and e.target is a .pixel.
  6. It reads document.querySelector('#color').value (which is "#00ff00" at that moment).
  7. It sets e.target.style.backgroundColor = "#00ff00".

8. Key Concepts Summarised

ConceptExplanation
Event object (e)Contains properties like target, currentTarget, altKey, button.
e.targetThe element on which the event originally occurred (the pixel).
e.currentTargetThe element on which the listener was attached (container in delegation).
Event bubblingDefault behaviour where an event propagates from child to parent elements.
Event delegationTechnique of listening on a parent for events that bubble from children, improving performance and allowing dynamic elements.

9. Connection to Previous Modules

  • HTML/CSS: Grid layout, selectors, styling.
  • Module 6 (JS Basics): variables, loops (for), functions, callback functions (used as event handlers).
  • Module 7: DOM selection (querySelector), element creation (createElement), attribute setting (setAttribute), appending children, modifying inline styles.

Key Takeaways

  • Use a <div> container with CSS grid to create a pixel canvas.
  • Generate grid cells dynamically with a loop to avoid hard‑coding HTML.
  • Apply color only when a modifier key (e.g., Alt) is held — check event.altKey.
  • Event delegation (listener on the parent) is far more efficient than attaching listeners to every child.
  • e.target (the originated element) vs e.currentTarget (the listener’s element) is a common exam distinction.
  • This project integrates loops, functions, DOM manipulation, and events — typical of interactive web pages.

Overview: The Typewriter Effect

The goal is to display sentences one by one, each typed out character by character, then erased character by character before the next sentence appears. A blinking cursor sits at the end of the typed text. The animation starts automatically when the page loads — no user interaction required.

The key insight: this is a continuous loop that must not block the browser. We achieve it by combining recursive functions (a function calling itself with a delay) and JavaScript’s asynchronous timing methods (setTimeout, setInterval).


The Building Blocks

ComponentHTML ElementPurposeCSS Class Used
Static prefix<span> (e.g. "IIMB")Always visible—
Animated text<span id="animated">Holds the typed/erased string—
Blinking cursor<span id="cursor">Vertical bar (``) that toggles visibility

Delay functions in JavaScript

  • setTimeout(fn, delay) – Executes fn once after delay milliseconds.
  • setInterval(fn, delay) – Executes fn repeatedly every delay milliseconds.

Both accept a callback function as an argument. They do not block the main thread — they run asynchronously. Without this, an infinite loop would freeze the page.


Step-by-step Construction

1. Blinking Cursor with setInterval

const cursor = document.querySelector('#cursor');

function toggleCursor() {
  cursor.classList.toggle('hidden');   // toggles display: none
}

setInterval(toggleCursor, 500);        // blink every 0.5s
  • The .hidden class sets display: none.
  • setInterval keeps calling toggleCursor → cursor appears and disappears.

2. Wait for DOM to be ready

document.addEventListener('DOMContentLoaded', startAnimation);
  • The event DOMContentLoaded fires when the HTML is fully parsed.
  • Only then do we start the animation (calling startAnimation, which triggers both cursor blinking and the typewriter).

3. Typewriter Logic: Recursion with setTimeout

The problem: a regular for loop would print all characters instantly — no delay. Solution: use a recursive function that calls itself via setTimeout.

Data

  • sentences = ["Lay the foundation to web development.", "The best institute in the world.", "Learn to live your life happily and keep being curious."]
  • sentenceCounter (0..2 → cycles with modulo)
  • charCounter (0..length-1)

State diagram (conceptual)

Key implementation details

  • Printing: animatedSpan.textContent += sentence[sentenceCounter][charCounter]
  • Erasing: animatedSpan.textContent = animatedSpan.textContent.slice(0, -1)
  • textContent (not innerText) preserves spaces correctly.
  • Use setTimeout inside the recursive call — never setInterval for one-shot steps. Using setInterval here causes infinite rapid re‑calls.

4. Achieving the infinite loop with modulo

When a sentence is fully erased, we need to move to the next sentence:

sentenceCounter = (sentenceCounter + 1) % sentences.length;
  • % (modulo) keeps the counter within 0..2, wrapping back to 0 after the last sentence.

Worked example: First iteration

Initial state: sentenceCounter = 0, charCounter = 0, sentences[0] = "Lay..."

  1. addCharacters runs:

    • charCounter (0) < "Lay...".length → true
    • Append sentences[0][0] → "L"
    • charCounter++ → 1
    • setTimeout(addCharacters, 100)
  2. After 100ms:

    • Append "a" → "La"
    • charCounter = 2
    • setTimeout(addCharacters, 100)

… continues until charCounter reaches 37 (length of first sentence).

  1. Then: charCounter >= length →

    • setTimeout(removeCharacters, 2000) → pause 2 seconds
  2. removeCharacters:

    • charCounter = 37 > 0
    • Slice off last char → "Lay ... developmen"
    • charCounter-- → 36
    • setTimeout(removeCharacters, 20)

… fast removal at 20ms per character until charCounter = 0.

  1. When charCounter = 0:
    • sentenceCounter = (0 + 1) % 3 = 1 → next sentence
    • charCounter = 0
    • setTimeout(addCharacters, 100)

The cycle repeats forever.


Asynchronous behaviour

  • setTimeout and setInterval are non‑blocking. The JavaScript event loop schedules them; the main thread continues running other code (e.g. user clicks).
  • Without asynchronous delay, an infinite loop would freeze the browser tab.

Exam tip: The difference between setTimeout (one‑shot) and setInterval (repeated) is a common trick question. In recursive animation, use setTimeout inside the function to create a controlled delay between steps. Confusing them leads to bugs such as a “super fast” loop.


Connecting the pieces

The final animation runs two independent async loops:

  1. Cursor blink – setInterval, simple toggle every 500ms.
  2. Typewriter – recursive setTimeout chain for add/erase, with state updated via counters and modulo.

Both are started synchronously inside startAnimation(), triggered by DOMContentLoaded.


Key takeaways

  • Typewriter animations are built character‑by‑character using recursive setTimeout, not synchronous loops.
  • setInterval is appropriate for repeated actions (blinking); setTimeout for one‑shot delays inside recursion.
  • Modulo arithmetic (%) elegantly cycles through an array without extra checks.
  • textContent correctly handles whitespace; innerText sometimes omits it.
  • Asynchronous timing keeps the page responsive — the animation does not block user interactions.
  • Professional result achieved with just a few concepts: DOM selection, class toggle, string slicing, counters, and delay functions.