Magyar zászlóMagyar

Practice 2 - DOM Manipulation & Event Handling

2025-02-20 8 min read GitHub

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:

  1. Convert between types explicitly using Number(), parseInt(), and the unary +
  2. Write ternary expressions as a shorthand for simple if/else
  3. Understand and use reduce() for collapsing an array into a single value
  4. Create and work with objects — JavaScript’s built-in key-value data structure
  5. Select and modify DOM elements with querySelector / querySelectorAll
  6. 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:

  1. A callback (accumulator, currentItem) => newAccumulator — called once for every element
  2. 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 / JavaJavaScript
struct Car { char* brand; int year; }const car = { brand: "Honda", year: 2020 }
Fixed fields declared upfrontKeys are dynamic, no declaration needed
Strongly typed fieldsValues 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 selectorMatches
"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 innerText when 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 innerHTML when 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

MethodUsageNotes
Number(x)General conversionNaN for non-numeric strings
parseInt(x)Strips units like "16px"Stops at first non-digit
+xShortest formSame as Number(x) for strings
typeof xCheck the type of xReturns a string: "string", "number", etc.

Objects

  • Created with { key: value, ... } literal syntax
  • Properties accessed via dot notation: obj.key
  • Work seamlessly with map() and filter() when stored in arrays

reduce()

  • Collapses an array to a single value
  • array.reduce((acc, item) => newAcc, initialValue)
  • Use -Infinity as initial value when finding a maximum

DOM

  • document.querySelector(selector) → first match
  • document.querySelectorAll(selector) → NodeList of all matches
  • element.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 innerText to 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.length gives you the character count
  • element.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 },
];
  1. Use filter() to get students who passed (grade ≥ 60)
  2. Use map() to extract only the names of passing students
  3. 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