Learning Goals
3 minBy the end of this lesson you will be able to:
- Grab one element with
document.querySelector(). - Grab a whole set with
document.querySelectorAll(). - Loop over that set and print each item's text.
- Explain why a failed selection returns
null, and guard against it.
Warm-Up · One Tag Ruins Everything
5 minLast lesson you walked the tree with square brackets. It worked — but look what a small edit does.
<body>
<h1>Warung Aisyah</h1>
<ul id="menu">…</ul>
</body>const menu = document.body.children[1];Predict together
- A designer adds a
<p>tagline under the heading. What ischildren[1]now? - Would you get an error, or just the wrong element?
Reveal
children[1] becomes the new paragraph, and the menu shifts to children[2]. No error at all — your code just quietly works on the wrong thing.
Position is a fragile way to find something. Today you ask for the menu by its id, and a hundred new paragraphs cannot break it.
New Concept · Asking by Name
12 minFinding a friend at a wedding by counting “third table, second chair” falls apart the moment somebody moves. Calling their name works wherever they are sitting. Selectors are names.
querySelector — the first match
const title = document.querySelector('h1');
const menu = document.querySelector('#menu');
const tagline = document.querySelector('.tagline');
console.log(title.textContent);Warung AisyahThe string inside is a CSS selector — the same syntax you wrote stylesheets with in Level 1. Nothing new to learn:
'h1''#menu''.tagline''#menu li'querySelector returns the first match only, however many exist.
When nothing matches
const missing = document.querySelector('#drinks');
console.log(missing);
console.log(missing.textContent);null
TypeError: Cannot read properties of null (reading 'textContent')This is the most common error in DOM code. A failed selection gives null, and reading a key from null throws. Guard it exactly as you guarded .find() in Lesson 11:
const drinks = document.querySelector('#drinks');
if (drinks) {
console.log(drinks.textContent);
} else {
console.log('No drinks list on this page.');
}Nine times in ten the cause is a typo in the selector, or a script running before the element exists — Lesson 13's problem.
querySelectorAll — every match
const items = document.querySelectorAll('#menu li');
console.log(items.length);
for (const item of items) {
console.log(item.textContent);
}2
nasi lemak
roti canaiquerySelector
Returns one element, or null.
Use for a single thing: a heading, a form, a total.
Guard against null.
querySelectorAll
Returns a NodeList — never null.
Use for sets: all list items, all buttons.
No matches means length is 0.
A NodeList is not quite an array
It has .length and works with for…of, so it feels like an array. But it has no .map() or .filter() — the Lesson 10 methods are missing.
const items = document.querySelectorAll('#menu li');
const texts = Array.from(items).map((item) => item.textContent);
console.log(texts);[ 'nasi lemak', 'roti canai' ]Array.from() converts it into a real array, and everything from Lesson 10 works again.
Searching inside an element
Any element can be searched, not just document. That keeps your selectors short and safe:
const menu = document.querySelector('#menu');
const firstItem = menu.querySelector('li'); // only inside #menu
console.log(firstItem.textContent);nasi lemakWhy it matters
Selecting is the first half of every DOM task — you select, then you change or listen. The next two lessons are those two halves. React later removes most of this work, but the selector idea survives in testing and in every debugging session.
Worked Example · Reading the Menu
12 minUse this page, saved as menu.html with an app.js beside it.
<body>
<h1 id="stall-name">Warung Aisyah</h1>
<p class="tagline">Open 8am to 10pm</p>
<ul id="menu">
<li class="dish" data-price="5.50">nasi lemak</li>
<li class="dish" data-price="1.50">roti canai</li>
<li class="dish" data-price="4.00">cendol</li>
</ul>
<script src="app.js"></script>
</body>Step 1 — Select one thing
// app.js
const stallName = document.querySelector('#stall-name');
console.log(stallName.textContent);Warung AisyahStep 2 — Select the set
const dishes = document.querySelectorAll('.dish');
console.log(`${dishes.length} dishes on the menu`);3 dishes on the menuStep 3 — Loop and number them
for…of from Lesson 9 works directly on a NodeList:
let n = 1;
for (const dish of dishes) {
console.log(`${n}. ${dish.textContent}`);
n = n + 1;
}1. nasi lemak
2. roti canai
3. cendolStep 4 — Read the data attributes
Those data-price attributes hold real data in the HTML. Read them through .dataset, where data-price becomes .dataset.price:
let total = 0;
for (const dish of dishes) {
const price = Number(dish.dataset.price); // attributes are text
total = total + price;
console.log(`${dish.textContent} — RM ${price.toFixed(2)}`);
}
console.log(`Total: RM ${total.toFixed(2)}`);nasi lemak — RM 5.50
roti canai — RM 1.50
cendol — RM 4.00
Total: RM 11.00That Number() is essential. Attributes are always strings, so without it you would join text instead of adding — Lesson 4's trap, now arriving from the HTML.
Step 5 — Filter with array methods
const cheap = Array.from(dishes)
.filter((dish) => Number(dish.dataset.price) < 5)
.map((dish) => dish.textContent);
console.log(`Under RM 5: ${cheap.join(', ')}`);Under RM 5: roti canai, cendolWhat changed? Nothing about the page — but your Section A skills now work on real page content rather than hard-coded arrays. The data lives in the HTML where a designer can edit it, and your JavaScript reads whatever is there.
Try It Yourself
13 minWork on menu.html and app.js.
Task 1 — A drinks list
Add a second
<ul id="drinks">with three drinks, each with adata-price. Select it, count its items, and print each one with its price.Use
'#drinks li'so you get only the drinks, not the dishes.Task 2 — Guard a missing selector
Try to select
'#desserts', which does not exist. Print the result, then wrap it in anifso the page prints a friendly message instead of throwing.Read the error first, before you add the guard.
Task 3 — The priciest item
Combine both lists using
querySelectorAll('#menu li, #drinks li')— a selector can take a comma-separated list.Convert with
Array.from(), then find the most expensive item and print its name and price. The “best so far” pattern from Lesson 9 will help.
🔥 Mini-Challenge · The Selector That Finds Nothing
8 minAaron's script throws on the first line and his total is wrong. Find three mistakes.
<!-- aaron-kedai.html -->
<ul id="stock">
<li class="item" data-qty="4">kuih</li>
<li class="item" data-qty="6">keropok</li>
</ul>// aaron-kedai.js — buggy
const list = document.querySelector('stock');
console.log('Items: ' + list.children.length);
const items = document.querySelector('.item');
let total = 0;
for (const item of items) {
total = total + item.dataset.qty;
}
console.log('Total stock: ' + total);It works if: the console prints Items: 2 and Total stock: 10.
Reveal the answer
Mistake 1 — a missing # on the id.
'stock' asks for a <stock> tag, which does not exist. The result is null, and line 2 throws:
TypeError: Cannot read properties of null (reading 'children')Mistake 2 — querySelector where querySelectorAll was meant.
querySelector('.item') returns the single first item. Looping over one element with for…of throws, because a lone element is not a list:
TypeError: items is not iterableMistake 3 — adding attribute text instead of numbers.
item.dataset.qty is the string '4'. Starting from total = 0, JavaScript gives 0 + '4' = '04', then '046' — Lesson 4 all over again.
// aaron-kedai.js — fixed
const list = document.querySelector('#stock'); // # for an id
console.log(`Items: ${list.children.length}`);
const items = document.querySelectorAll('.item'); // All, for a set
let total = 0;
for (const item of items) {
total = total + Number(item.dataset.qty); // convert on the way in
}
console.log(`Total stock: ${total}`);Items: 2
Total stock: 10Three bugs, three habits: check your selector prefix, match One versus All to what you need, and convert attribute text before doing maths.
Recap
3 minquerySelector(sel)returns the first match, ornull.querySelectorAll(sel)returns a NodeList of every match.- Selectors are ordinary CSS:
h1,#id,.class,#id li. - Forgetting
#or.is the classic cause of anullresult. - Always guard a
querySelectorresult before reading a key from it. - A NodeList works with
for…ofbut has no.map()— useArray.from(). data-*attributes are read via.dataset, and are always strings.- Selecting by name survives page edits; selecting by position does not.
New words
- Selector — a CSS pattern describing which elements you want.
- NodeList — the array-like set
querySelectorAllreturns. - dataset — the object holding an element's
data-*attributes.
📦 Homework
4 min to brief · ~20 min to doRequired
- Take your
myshop.htmlfrom last lesson. Give every product a class and adata-price, then write a script that prints each product with its price, the total, and how many cost over RM 10. - Include at least one guarded
querySelector. Bring the file next lesson.
Optional stretch
- Look up
getElementById. It predatesquerySelector— what is the one difference in the string you pass it? - Try the selector
'li:last-child'in the console. What other CSS pseudo-selectors work here?