Practice 2 - DOM Manipulation & Event Handling
Introduction
This practice has two parts. We start by wrapping up the remaining topics from p1/main.js — explicit type conversion, the ternary operator, reduce(), and JavaScript objects (all brand new). Then we move into a completely new area: DOM manipulation and event handling, which is where JavaScript starts to feel truly interactive.
By the end you will be able to:
- Convert between types explicitly using
Number(),parseInt(), and the unary+ - Write ternary expressions as a shorthand for simple
if/else - Understand and use
reduce()for collapsing an array into a single value - Create and work with objects — JavaScript’s built-in key-value data structure
- Select and modify DOM elements with
querySelector/querySelectorAll - Respond to user events (clicks, etc.) with
addEventListener
Part 1 — Finishing p1/main.js
Type Conversion
In the previous practice we saw that JavaScript silently coerces types when you mix strings and numbers. Sometimes you want to do that conversion explicitly and predictably.
Checking the type of a value: typeof
let n = "4";
console.log(typeof n); // "string"
let n_number = Number(n);
console.log(typeof n_number); // "number"
Three ways to convert a string to a number
let s = "10";
Number(s); // 10 — general purpose, returns NaN (Not a Number) for invalid input
parseInt(s); // 10 — parses integer portion, ignores trailing text
+s; // 10 — unary plus, shortest form
All three produce the same result for a clean numeric string. The differences matter at the edges:
parseInt("10px"); // 10 — stops at the first non-digit
Number("10px"); // NaN — the whole string must be numeric
+"10px"; // NaN
Use Number() when the input should be a pure number and you want NaN as a clear error signal. Use parseInt() when parsing values like "16px" from CSS.
Type coercion puzzles
Understanding coercion helps you avoid subtle bugs. Work through these mentally before reading the answers:
console.log("31" + 2 + "4"); // ?
console.log("3" * 4 + "100"); // ?
Answers:
console.log("31" + 2 + "4");
// "31" + 2 → "312" (+ with a string → concatenation)
// "312" + "4" → "3124"
// Result: "3124"
console.log("3" * 4 + "100");
// "3" * 4 → 12 (* always converts to number)
// 12 + "100" → "12100" (+ with a string → concatenation)
// Result: "12100"
The rule: *, /, - always convert both sides to numbers. + converts to number only if both sides are numbers — if either side is a string, it concatenates.
The Ternary Operator — Shorthand if/else
Before we look at reduce(), we need one small new piece of syntax: the ternary operator. It is a compact way to write a simple if/else that produces a value:
// Full if/else:
let result;
if (a > b) {
result = a;
} else {
result = b;
}
// Same thing as a ternary:
let result = a > b ? a : b;
// ^^^^^^ ^ ^
// test yes no
Read it as: “if a > b then a, otherwise b”. The ternary is handy when the two branches are short, single values. If the logic is more complex, stick with a regular if/else for readability.
reduce() — Accumulating a Result
map() and filter() return a new array. Sometimes you need to collapse an array into a single value — a sum, a maximum, a count. That is reduce().
const nums = [23, 56, 100, 14];
// Sum with a for...of loop (you already know this):
let accumulation = 0;
for (const item of nums) {
accumulation = accumulation + item;
}
// The same thing with reduce():
let sum = nums.reduce((acc, item) => acc + item, 0);
reduce() takes two arguments:
- A callback
(accumulator, currentItem) => newAccumulator— called once for every element - An initial value for the accumulator (here
0)
Think of it as: “start with acc = 0, then for each item update acc and keep going.”
Step-by-step trace for [23, 56, 100, 14]:
acc = 0 (initial value)
+ 23 → acc = 23
+ 56 → acc = 79
+ 100 → acc = 179
+ 14 → acc = 193 ← final result
Each step the callback returns the new acc, which becomes the input for the next step.
Finding the maximum value
const nums = [23, 56, 100, 14];
// Using a ternary in the callback:
let max = nums.reduce((acc, item) => item > acc ? item : acc, -Infinity);
console.log("Max:", max); // 100
// Using Math.max():
let max2 = nums.reduce((acc, item) => Math.max(acc, item), -Infinity);
console.log("Max2:", max2); // 100
Why -Infinity as initial value? We need a starting value that any real number will beat. If we used 0, an array of all negative numbers would return 0 — wrong. -Infinity is smaller than every possible number, so the very first element always becomes the new acc on step one.
Step-by-step trace for [23, 56, 100, 14]:
acc = -Infinity (initial value)
vs 23 → 23 > -Infinity → acc = 23
vs 56 → 56 > 23 → acc = 56
vs 100 → 100 > 56 → acc = 100
vs 14 → 14 > 100? No → acc = 100 ← final result
Math.max(a, b) is a built-in function that returns the larger of two numbers: Math.max(3, 7) → 7. That is all we use it for in the second reduce() example above — calling it inside the callback to compare two numbers cleanly.
JavaScript Objects
This is the first time we are using objects — a completely new concept.
So far all our values have been primitives (numbers, strings, booleans) or arrays of primitives. Objects let you group related data together under named keys — think of them as a single variable that can hold multiple pieces of information at once.
const car = {
brand: "Honda",
year: 2020,
color: "black"
};
console.log(car); // { brand: "Honda", year: 2020, color: "black" }
console.log(car.brand); // "Honda"
console.log(car.year); // 2020
An object is a set of key: value pairs wrapped in { }. You access a property with dot notation: object.key.
Comparison with other languages:
| C / Java | JavaScript |
|---|---|
struct Car { char* brand; int year; } | const car = { brand: "Honda", year: 2020 } |
| Fixed fields declared upfront | Keys are dynamic, no declaration needed |
| Strongly typed fields | Values can be any type |
Arrays of objects
Objects become even more powerful when combined with arrays:
const cars = [];
cars.push(car); // push the Honda we created above
cars.push({
brand: "Tesla",
year: 2024,
color: "white"
});
console.log(cars.length); // 2
push() adds an element to the end of an array. Its counterpart pop() removes and returns the last element — together they let you use an array as a stack:
After push(Honda): [Honda]
After push(Tesla): [Honda, Tesla]
After pop(): [Honda] ← Tesla is returned and removed
map() and filter() on arrays of objects
The same methods you know from arrays of numbers work identically with arrays of objects. The callback just receives the whole object as item:
// Extract only the brand property from each car
const brands = cars.map((car) => car.brand);
console.log(brands); // ["Honda", "Tesla"]
// Keep only cars produced after 2022
const newCars = cars.filter((car) => car.year > 2022);
console.log(`There are ${newCars.length} cars produced after 2022.`); // 1
This pattern — an array of objects, map to extract a field, filter to narrow by a condition — is one of the most common patterns in real JavaScript applications.
Part 2 — DOM Manipulation (p2/task1)
Now we move to a new project. The browser renders HTML into a tree of objects called the DOM (Document Object Model). JavaScript can read and modify this tree at runtime, making the page dynamic.
Project structure
p2/
├── task1.html # HTML with h1, two <p> elements, a script tag
├── task1.js # DOM manipulation
├── task2.html # HTML with a <span> counter and a <button>
└── task2.js # Event handling
Script placement — why it matters
In practice-01 the <script> tag was in <head>. In p2 it is at the bottom of <body>:
<body>
<h1>Some text</h1>
<p>First paragraph</p>
<p id="second">Second paragraph</p>
<script src="./task1.js"></script> <!-- ← at the end of body -->
</body>
Why? JavaScript runs as soon as the browser parses the <script> tag. If the script is in <head>, the <h1> and <p> elements have not been created yet — querySelector would return null. Placing the script at the end of <body> guarantees all elements exist by the time the script runs.
There is a modern alternative: add defer to the script tag in <head>:
<script src="./task1.js" defer></script>
defer tells the browser to download the script in parallel but execute it only after the HTML is fully parsed — the same effect as putting it at the end of <body>. When using type="module", defer is applied automatically.
Selecting elements: querySelector
document.querySelector(selector) returns the first element in the page that matches a CSS selector:
const heading = document.querySelector("h1"); // by tag name
const second = document.querySelector("#second"); // by id
const first = document.querySelector(".intro"); // by class
The selector syntax is identical to CSS:
| CSS selector | Matches |
|---|---|
"h1" | The first <h1> element |
"#second" | The element with id="second" |
".intro" | The first element with class="intro" |
"p.intro" | The first <p> with class="intro" |
Modifying content: innerText vs innerHTML
const heading = document.querySelector("h1");
console.log(heading.innerText); // Read: "Some text"
heading.innerText = "<i>Another Text</i>";
// Sets the literal string — the <i> tags show up as plain text
heading.innerHTML = "<i>Another Text in italic</i>";
// Parses HTML — the text appears in italic
Rule of thumb:
- Use
innerTextwhen setting plain text — it is safer because even if the string accidentally contains characters like<or>, they are displayed as text and not interpreted as HTML tags - Use
innerHTMLwhen you intentionally want to inject HTML markup
Selecting multiple elements: querySelectorAll
querySelectorAll(selector) returns a NodeList of all matching elements. You can iterate over it with for...of:
const ps = document.querySelectorAll("p");
let index = 0;
for (const item of ps) {
index++;
item.innerText = index; // Sets each <p> to its position number
}
// First <p> now shows "1", second shows "2"
A NodeList is not a real array. It supports for...of and has .length, but array methods like .map() or .filter() are not available directly. Convert it first with [...ps] or Array.from(ps) if you need those.
Changing styles with .style
Every DOM element has a .style property that maps to its inline CSS:
const second = document.querySelector("#second");
second.style.color = "red";
second.style.backgroundColor = "yellow";
Notice: CSS property names with hyphens (background-color) become camelCase in JavaScript (backgroundColor). The equivalent CSS would be:
#second {
color: red;
background-color: yellow;
}
Part 3 — Event Handling (p2/task2)
HTML is static. Events make it interactive. An event is something that happens in the browser — a click, a key press, a form submission. We can attach a JavaScript function (a handler) that runs whenever that event occurs.
The task
The HTML for task2 has a counter display and a button:
<span>0</span>
<button>Increase (+)</button>
Goal: every time the button is clicked, the number in the span increases by 1.
The three-step pattern
// 1. Select the elements
const span = document.querySelector("span");
const button = document.querySelector("button");
// 2. Define the event handler
function handleButtonClick() {
console.log("Button has been clicked");
let number = Number(span.innerText); // Read the current count and convert to number
number = number + 1;
span.innerText = number; // Write it back
}
// 3. Register the handler
button.addEventListener("click", handleButtonClick);
Every DOM element has an addEventListener(eventName, handlerFunction) method. The browser calls handlerFunction automatically whenever the event fires.
Pass the function reference — not a call. handleButtonClick is correct; handleButtonClick() is wrong because it would call the function immediately and pass its return value (undefined) to addEventListener.
Converting span.innerText to a number
innerText always returns a string. Doing arithmetic on strings produces wrong results ("0" + 1 = "01"), so you must convert first. All three forms below work:
let number = Number(span.innerText); // "0" → 0
let number = parseInt(span.innerText); // "0" → 0
let number = +span.innerText; // "0" → 0
Incrementing — four equivalent ways
number = number + 1; // explicit
number += 1; // compound assignment
number++; // post-increment
++number; // pre-increment
In this context all four have the same effect.
Summary
Type conversion
| Method | Usage | Notes |
|---|---|---|
Number(x) | General conversion | NaN for non-numeric strings |
parseInt(x) | Strips units like "16px" | Stops at first non-digit |
+x | Shortest form | Same as Number(x) for strings |
typeof x | Check the type of x | Returns a string: "string", "number", etc. |
Objects
- Created with
{ key: value, ... }literal syntax - Properties accessed via dot notation:
obj.key - Work seamlessly with
map()andfilter()when stored in arrays
reduce()
- Collapses an array to a single value
array.reduce((acc, item) => newAcc, initialValue)- Use
-Infinityas initial value when finding a maximum
DOM
document.querySelector(selector)→ first matchdocument.querySelectorAll(selector)→ NodeList of all matcheselement.innerText— plain text content (read/write)element.innerHTML— HTML content (read/write)element.style.propertyName— inline CSS (camelCase names)
Events
element.addEventListener("click", handlerFn)— registers a handler- Always convert
innerTextto a number before arithmetic - Pass the function reference, not a call
Practice Exercises
Exercise 1: Highlight long paragraphs
Given a page with several <p> elements, use querySelectorAll to iterate over them. If a paragraph’s text is longer than 100 characters, set its background color to "lightyellow".
Hints:
element.innerText.lengthgives you the character countelement.style.backgroundColor = "lightyellow"
Exercise 2: Click counter with reset
Extend the counter from task2 so that there is also a Reset button that sets the span back to 0.
Hints:
- Add a second
<button>with id"reset-btn"in the HTML - Select it with
querySelector("#reset-btn") - In its handler, set
span.innerText = 0
Exercise 3: Object array summary
Given this array of students:
const students = [
{ name: "Alice", grade: 88 },
{ name: "Bob", grade: 55 },
{ name: "Carol", grade: 72 },
{ name: "Dave", grade: 91 },
];
- Use
filter()to get students who passed (grade ≥ 60) - Use
map()to extract only the names of passing students - Use
reduce()to calculate the average grade of all students
Expected results:
// 1. passing: [{name:"Alice", grade:88}, {name:"Carol", ...}, {name:"Dave", ...}]
// 2. names: ["Alice", "Carol", "Dave"]
// 3. average: (88 + 55 + 72 + 91) / 4 = 76.5
Hint for part 3 — average with reduce():
First sum all grades with reduce(), then divide by the number of students:
const total = students.reduce((acc, student) => acc + student.grade, 0);
const average = total / students.length;
Next Steps
In the next practice we’ll go deeper into events:
- Event delegation — handling events on dynamic lists efficiently
- Form handling — reading user input
- Building more complex interactive UIs