Warm-up · Activity 1 of 7
Warm-up from lesson B5.3: what does this print?
const list = ["tea", "milk"];
const more = [...list, "eggs"];
console.log(list.length, more.length);// B6.1 · ~30 min · Beginner
After this lesson you find elements on a page, change their text safely, and set their attributes, data values and classes from JavaScript.
Lesson 1 of 5 in B6 The DOM and events
You will be able to
Warm-up · Activity 1 of 7
const list = ["tea", "milk"];
const more = [...list, "eggs"];
console.log(list.length, more.length);Predict · Activity 2 of 7
const msg = document.querySelector('#msg');
msg.textContent = '<b>Bye</b>';
console.log(msg.children.length, msg.textContent);Practice · Activity 3 of 7
const title = document.____('#title');
console.log(title.textContent);Practice · Activity 4 of 7
Practice · Activity 5 of 7
const li = document.querySelector('#list li');
li.classList.add('on');
li.classList.remove('tag');
console.log(li.classList.contains('tag'), li.className);Brain teaser · Activity 6 of 7
const items = document.querySelectorAll('#list li');
const list = document.querySelector('#list');
list.append(document.createElement('li'));
console.log(items.length, list.children.length);Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
The page has a heading, a list of three books with their page counts in data-pages, and a note. main.js selects the items, turns the NodeList into an array to read names and numbers, updates the heading, marks long books with a class and sets two attributes. Then it puts the same string into the note with textContent and with innerHTML, so you can see the difference, and asks for an element that does not exist.
main.js
// main.js runs after the HTML is parsed: module scripts are deferred.
const title = document.querySelector('#title');
const items = document.querySelectorAll('#books li');
console.log(items.length, typeof items.map);
// A NodeList has no map, so spread it into an array first.
const names = [...items].map((li) => li.textContent);
console.log(names);
const pages = [...items].map((li) => Number(li.dataset.pages));
console.log(pages);
title.textContent = 'My books (' + items.length + ')';
console.log(title.textContent);
for (const li of items) {
if (Number(li.dataset.pages) > 400) li.classList.add('long');
}
items[0].dataset.read = 'yes';
items[2].setAttribute('title', 'A short one');
console.log(items[0].getAttribute('data-read'), items[1].classList.contains('long'));
// textContent shows text as it is; innerHTML reads it as HTML.
const note = document.getElementById('note');
note.textContent = '<b>2</b> long books';
console.log(note.textContent, note.children.length);
note.innerHTML = '<b>2</b> long books';
console.log(note.textContent, note.children.length);
console.log(note.classList.toggle('hint'), note.classList.length);
console.log(document.querySelector('#missing'));
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Reading list</title>
</head>
<body>
<h1 id="title">My books</h1>
<ul id="books"><li data-pages="412">Dune</li><li data-pages="474">Emma</li><li data-pages="96">Animal Farm</li></ul>
<p id="note" class="hint">Loading...</p>
<script type="module" src="main.js"></script>
</body>
</html>
To run the page on your computer, open index.html through a local web server, not as a file: browsers do not run module scripts from file:// addresses. For the checks, install jsdom first (the commands below).
Output
3 undefined
[ 'Dune', 'Emma', 'Animal Farm' ]
[ 412, 474, 96 ]
My books (3)
yes true
<b>2</b> long books 0
2 long books 1
false 0
null
<!-- #books -->
<ul id="books"><li data-pages="412" class="long" data-read="yes">Dune</li><li data-pages="474" class="long">Emma</li><li data-pages="96" title="A short one">Animal Farm</li></ul>Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Your page
Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.
Run the program to see its page here.
Exercise 1 of 2
The page lists four tasks, each with data-done="yes" or data-done="no". main.js should add the class done to every finished task, and to no other, and set the paragraph #summary to "2 of 4 done", counted from the page. The starter marks only the first item and writes a fixed summary. Select all the items, spread them into an array, and use dataset and classList.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Your page
Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.
Run the program to see its page here.
Run the tests: only the first task is marked, and the summary is fixed text.
[...document.querySelectorAll('#tasks li')] gives an array you can loop over and filter.
task.dataset.done is the string 'yes' or 'no'; count with filter(...).length and build the text from that number and tasks.length.
One way to solve it. Yours can look different and still pass the checks.
const tasks = [...document.querySelectorAll('#tasks li')];
for (const task of tasks) {
if (task.dataset.done === 'yes') task.classList.add('done');
}
const done = tasks.filter((task) => task.dataset.done === 'yes').length;
document.querySelector('#summary').textContent = done + ' of ' + tasks.length + ' done';
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
const task = document.querySelector('#tasks li');
task.classList.add('done');
// TODO: mark every finished task, then count them for the summary.
document.querySelector('#summary').textContent = '1 of 1 done';
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';
const marked = (page) => [...page.document.querySelectorAll('#tasks li')].map((li) => li.classList.contains('done'));
test('Every finished task has the class done', async () => {
const page = await loadPage();
const got = marked(page);
assert.equal(got[0] && got[2], true, 'the class done is on these tasks: ' + JSON.stringify(got) + ', expected on the 1st and 3rd');
});
test('Unfinished tasks do not get the class done', async () => {
const page = await loadPage();
const got = marked(page);
assert.deepEqual(got, [true, false, true, false], 'the class done is on these tasks: ' + JSON.stringify(got));
});
test('The summary says 2 of 4 done', async () => {
const page = await loadPage();
const text = page.document.querySelector('#summary').textContent;
assert.equal(text, '2 of 4 done', 'the summary says ' + JSON.stringify(text));
});
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Today</title>
</head>
<body>
<h1>Today</h1>
<ul id="tasks">
<li data-done="yes">Buy milk</li>
<li data-done="no">Call Ada</li>
<li data-done="yes">Water the plants</li>
<li data-done="no">Book tickets</li>
</ul>
<p id="summary"></p>
<script type="module" src="main.js"></script>
</body>
</html>
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
To run the page on your computer, open index.html through a local web server, not as a file: browsers do not run module scripts from file:// addresses. For the checks, install jsdom first (the commands below).
Run the checks (needs learnrun.js in the same folder):
npm install jsdom@28.1.0
node --testDownload learnrun.jsExercise 2 of 2
The profile comes from a sign-up form, so every value is text a user typed, and the bio contains markup. Fill the card: #name and #bio show the values exactly, as text, with no elements made from them; the image #photo gets src set to the photo and alt "Photo of " plus the name; the card gets data-role with the role, and the class admin when the role is "admin", keeping its class card. The starter uses innerHTML and leaves the rest undone.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Your page
Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.
Run the program to see its page here.
Run the tests: the bio shows "I love maths" in bold, because innerHTML made a <b> element from the text.
Swap innerHTML for textContent, then use photo.setAttribute('src', …) and photo.setAttribute('alt', …).
card.dataset.role = profile.role writes data-role; classList.add('admin') adds a class and keeps card.
One way to solve it. Yours can look different and still pass the checks.
// Values a user typed into a sign-up form: treat them as text.
const profile = { name: 'Ada Lovelace', bio: 'I love <b>maths</b>', role: 'admin', photo: 'ada.png' };
const card = document.querySelector('#card');
document.querySelector('#name').textContent = profile.name;
document.querySelector('#bio').textContent = profile.bio;
const photo = document.querySelector('#photo');
photo.setAttribute('src', profile.photo);
photo.setAttribute('alt', 'Photo of ' + profile.name);
card.dataset.role = profile.role;
if (profile.role === 'admin') card.classList.add('admin');
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// Values a user typed into a sign-up form: treat them as text.
const profile = { name: 'Ada Lovelace', bio: 'I love <b>maths</b>', role: 'admin', photo: 'ada.png' };
const card = document.querySelector('#card');
document.querySelector('#name').innerHTML = profile.name;
document.querySelector('#bio').innerHTML = profile.bio;
// TODO: the photo and its alt text, data-role, and the class admin.
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';
test('The bio is shown as text, not as HTML', async () => {
const page = await loadPage();
const bio = page.document.querySelector('#bio');
assert.equal(bio.children.length, 0, 'the bio holds ' + bio.children.length + ' element(s) made from the text, expected none');
assert.equal(bio.textContent, 'I love <b>maths</b>', 'the bio shows ' + JSON.stringify(bio.textContent));
});
test('The name is shown', async () => {
const page = await loadPage();
const text = page.document.querySelector('#name').textContent;
assert.equal(text, 'Ada Lovelace', 'the name shows ' + JSON.stringify(text));
});
test('The photo has its src and alt text', async () => {
const page = await loadPage();
const photo = page.document.querySelector('#photo');
assert.equal(photo.getAttribute('src'), 'ada.png', 'src is ' + JSON.stringify(photo.getAttribute('src')));
assert.equal(photo.getAttribute('alt'), 'Photo of Ada Lovelace', 'alt is ' + JSON.stringify(photo.getAttribute('alt')));
});
test('The card has data-role and the classes card and admin', async () => {
const page = await loadPage();
const card = page.document.querySelector('#card');
assert.equal(card.getAttribute('data-role'), 'admin', 'data-role is ' + JSON.stringify(card.getAttribute('data-role')));
assert.equal(card.classList.contains('admin'), true, 'the card has no class admin');
assert.equal(card.classList.contains('card'), true, 'the card lost its class card');
});
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Profile</title>
</head>
<body>
<article id="card" class="card">
<img id="photo" src="blank.png" alt="">
<h2 id="name">Name</h2>
<p id="bio"></p>
</article>
<script type="module" src="main.js"></script>
</body>
</html>
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
To run the page on your computer, open index.html through a local web server, not as a file: browsers do not run module scripts from file:// addresses. For the checks, install jsdom first (the commands below).
Run the checks (needs learnrun.js in the same folder):
npm install jsdom@28.1.0
node --testDownload learnrun.jsconst title = document.querySelector('#titel');
title.textContent = 'My tags';
What Node.js prints
TypeError: Cannot set properties of null (setting 'textContent')Why, and the fix
The id is spelled #titel, and no element has it, so querySelector returned null. The error comes one line later, when the program sets a property on null. Compare the selector with index.html, character by character: here it should be #title. If an element may really be missing, check first: if (title) { … }. Firefox and Safari word this message differently.
const tags = document.querySelectorAll('.tag');
const names = tags.map((li) => li.textContent);
console.log(names);
What Node.js prints
TypeError: tags.map is not a functionWhy, and the fix
querySelectorAll returns a NodeList, which looks like an array but is not one: it has length, indexes and forEach, but no map, filter or reduce. Spread it into a real array first: [...tags].map((li) => li.textContent). A for...of loop over the NodeList works too.
const tags = document.querySelectorAll('.tag');
tags.classList.add('on');
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'add')Why, and the fix
classList belongs to each element, not to the list of them, so tags.classList is undefined, and calling add on it fails. Change every element in a loop: for (const tag of tags) tag.classList.add('on'). If you meant only the first, use querySelector, which returns one element.
JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.