Learning Goals
3 minBy the end of this lesson you will be able to:
- Rewrite an element's words with
.textContent. - Add, remove and toggle styling with
.classList. - Build a new element with
createElementandappend. - Render a whole array onto the page as a list.
Warm-Up · Read-Only So Far
5 minLast lesson you selected elements and read their text. Everything still went to the console.
const title = document.querySelector('h1');
console.log(title.textContent);Predict together
titleis an object with atextContentkey. What did you do to object keys back in Lesson 11?- So what might this line do — and where would you see the result?
title.textContent = 'Warung Aisyah — Buka!';Reveal
In Lesson 11 you wrote dish.price = 6.0; to change an object key. An element is just an object, so the same assignment works — and because this object is the live page, the heading changes on screen immediately.
That is the whole idea of today. Reading a key looks at the page; writing a key changes it.
New Concept · Writing Back to the Tree
12 minA whiteboard in a café shows today's special. Nobody rebuilds the café to change it — they wipe that one line and write a new one. The DOM works the same way: change the smallest part that needs changing.
Changing words
const title = document.querySelector('#stall-name');
title.textContent = 'Warung Aisyah — Buka!';One line, and the page updates. You can build the new text with a template literal from Lesson 3:
const count = 3;
const status = document.querySelector('#status');
status.textContent = `${count} dishes available today`;Changing appearance — use classes
You can set styles directly, but it gets messy fast:
Inline style — avoid
el.style.color = 'red';
el.style.fontWeight = 'bold';Design details scattered through your JavaScript.
Hard to undo — you must set every property back.
classList — prefer
el.classList.add('sold-out');Design stays in the CSS, where it belongs.
One line to add, one to remove.
Write the look once in CSS, then let JavaScript decide when it applies:
.sold-out {
color: #94A3B8;
text-decoration: line-through;
}const dish = document.querySelector('.dish');
dish.classList.add('sold-out');
dish.classList.remove('sold-out');
dish.classList.toggle('sold-out'); // on if off, off if on
console.log(dish.classList.contains('sold-out'));.toggle() is the one you will reach for most — it flips a state without you tracking which way round it currently is.
Making new elements
Three steps, always in this order:
- Create it — it exists, but is nowhere.
- Fill it — text, classes, attributes.
- Attach it — now it appears on screen.
const menu = document.querySelector('#menu');
const item = document.createElement('li'); // 1. create
item.textContent = 'satay'; // 2. fill
item.classList.add('dish');
menu.append(item); // 3. attachForget step 3 and nothing appears — no error, just silence. That is the usual reason a “created” element never shows up.
Emptying and rebuilding
To redraw a list, clear it first. Otherwise you add to what is already there and items pile up.
const menu = document.querySelector('#menu');
const dishes = ['nasi lemak', 'roti canai', 'cendol'];
menu.textContent = ''; // clear it out
for (const name of dishes) {
const li = document.createElement('li');
li.textContent = name;
menu.append(li);
}Clear, then rebuild from your data. Remember that phrase — it is exactly what React does for you from Lesson 28 onwards, and knowing the manual version makes React's behaviour obvious rather than magical.
A warning about innerHTML
You will see .innerHTML = '<li>…</li>' in older tutorials. It writes raw HTML, so any text a visitor typed could become real tags — a genuine security hole called XSS.
.textContent is always safe: it treats everything as plain words. Use it unless you truly need HTML, and never with text a visitor supplied.
Why it matters
Select, then change — those two lessons together are how every plain JavaScript interface works. Next lesson adds the trigger: doing this when the visitor clicks.
Worked Example · Render the Menu
12 minThis time the dishes live in an array of objects — Lesson 11's shape — and the page starts empty.
<body>
<h1 id="stall-name">Warung Aisyah</h1>
<p id="status">Loading…</p>
<ul id="menu"></ul>
<script src="app.js"></script>
</body>.sold-out {
color: #94A3B8;
text-decoration: line-through;
}Step 1 — The data
// app.js
const menu = [
{ name: 'nasi lemak', price: 5.5, inStock: true },
{ name: 'roti canai', price: 1.5, inStock: true },
{ name: 'char kway teow', price: 8.0, inStock: false },
];Step 2 — One element per dish
const list = document.querySelector('#menu');
for (const { name, price } of menu) {
const li = document.createElement('li');
li.textContent = `${name} — RM ${price.toFixed(2)}`;
list.append(li);
}Open the page and the list appears:
nasi lemak — RM 5.50
roti canai — RM 1.50
char kway teow — RM 8.00The destructuring in the for…of header is straight from Lesson 11 — only the two keys this loop needs.
Step 3 — Mark the sold-out ones
for (const { name, price, inStock } of menu) {
const li = document.createElement('li');
li.textContent = `${name} — RM ${price.toFixed(2)}`;
if (!inStock) {
li.classList.add('sold-out');
}
list.append(li);
}Step 4 — Update the status line
const available = menu.filter((dish) => dish.inStock);
const status = document.querySelector('#status');
status.textContent = `${available.length} of ${menu.length} dishes available`;2 of 3 dishes availableThe Loading… placeholder is replaced. Lesson 10's .filter() is now feeding real page content.
Step 5 — Wrap it in a function
Put the whole thing in a function so you can redraw whenever the data changes:
function render(dishes) {
list.textContent = ''; // clear first!
for (const { name, price, inStock } of dishes) {
const li = document.createElement('li');
li.textContent = `${name} — RM ${price.toFixed(2)}`;
if (!inStock) li.classList.add('sold-out');
list.append(li);
}
}
render(menu);Now try calling render(menu) twice in a row, deleting the clearing line first. You get six items instead of three.
What changed? Nothing about the output when it works — but you now have a redraw button. Change the data, call render() again, and the page matches. That single idea — data in, page out — is the whole of React, and you have just built it by hand.
Try It Yourself
13 minWork on app.js. Refresh after each change.
Task 1 — Your own render
Replace the menu with five items of your own, including at least two sold out. Add a
.spicyclass in your CSS and give it to any dish withisSpicy: true.Task 2 — A heading that counts
Change the
<h1>from JavaScript so it reads “Warung Aisyah (5 dishes)”, taking the number from your array rather than typing it.Add a dish and check the heading updates by itself.
Task 3 — Build a card, not a line
Instead of one
<li>per dish, build a<div>containing an<h3>for the name and a<p>for the price. Append the smaller elements to the div, then the div to the page.Order matters: create the parent, create the children, append children to parent, then parent to the page.
🔥 Mini-Challenge · The List That Grows and Grows
8 minFaridah's stock page shows duplicates and one item never appears. Find three mistakes.
// faridah-stock.js — buggy
const stock = [
{ item: 'kuih', qty: 12 },
{ item: 'keropok', qty: 0 },
];
const list = document.querySelector('#stock');
function render() {
for (const row of stock) {
const li = document.createElement('li');
li.textContent = row.item + ' (' + row.qty + ')';
if (row.qty = 0) {
li.classList.add('sold-out');
}
}
}
render();
render();It works if: calling render() twice still shows exactly two items, with keropok struck through.
Reveal the answer
Mistake 1 — the element is never attached.
The li is created and filled, but never appended. It exists only in memory, so the page stays empty — with no error at all. Step 3 of the three steps is missing.
Mistake 2 — assignment inside the if.
row.qty = 0 uses one equals sign. It sets every quantity to 0 and returns 0, which is falsy — so nothing ever gets the class, and your data is now wrong. Lesson 5's trap doing double damage.
Mistake 3 — no clearing before redraw.
Once appending is fixed, calling render() twice appends twice, giving four items. The list must be emptied first.
// faridah-stock.js — fixed
function render() {
list.textContent = ''; // clear, then rebuild
for (const row of stock) {
const li = document.createElement('li');
li.textContent = `${row.item} (${row.qty})`;
if (row.qty === 0) { // === not =
li.classList.add('sold-out');
}
list.append(li); // attach it!
}
}
render();
render();kuih (12)
keropok (0)Two renders, two items. If a created element never shows up, check for the missing append first — it is silent every time.
Recap
3 min- Writing to
.textContentchanges what the visitor reads. - Style with
.classList.add/remove/toggle, not inline.style— keep design in the CSS. - New elements take three steps: create → fill → attach.
- Forgetting
.append()is silent — nothing appears, no error. - Clear, then rebuild when redrawing, or items pile up.
- Prefer
.textContentover.innerHTML— visitor text becoming HTML is an XSS risk. - A
render(data)function is data in, page out — the idea React is built on.
New words
- createElement — makes an element that is not yet on the page.
- append — attaches an element inside another.
- classList — the object for adding, removing and toggling classes.
- XSS — cross-site scripting, where visitor text runs as code.
📦 Homework
4 min to brief · ~20 min to doRequired
- Build
shop.htmlwith an empty<ul>and arender()function that fills it from an array of at least six product objects. Style out-of-stock items with a class. - Add a status line showing how many are in stock, written from JavaScript. Bring the file next lesson.
Optional stretch
- Sort your array by price before rendering, then call
render()again. Did clearing save you? - Look up
.prepend()and.remove(). Write one sentence on what each does.