Building Elements from Data
The site's Projects page has three cards written by hand. Adding a fourth means copying a card, changing every piece of it, and hoping nothing is missed. With thirty projects, or with projects that arrive from a server, nobody writes the cards by hand. A script builds one card for each item in the data, and the data becomes the only thing anyone edits. This idea, the page drawn from data by code, is the one every frontend framework is built on.
Think of a rubber stamp. You carve the design once, then stamp it once for every name on a list, writing a different name into each copy. In this topic you carve the card, then let a loop do the stamping. Where the comparison stops: a stamp prints ink, while each copy here is a separate, live element you can still change.
How Do You Make a New Element?
document.createElement makes a new element of the kind you name. The new element exists only in memory; it is not in the page yet, and nothing on the screen changes. You give it text and classes the way you changed existing elements in the last two topics. Then append puts it inside another element, as that element's last child, and only then does the browser draw it.
const note = document.createElement("p");
note.textContent = "Built by JavaScript.";
document.querySelector("main").append(note);
After the first two lines, nothing on the page has changed: the paragraph exists, but it has no place in the tree. The third line appends it to the main element, and the sentence "Built by JavaScript." appears at the end of the page's main content. The Elements panel shows the new paragraph as the last child of main. Reload, and it is gone, like every other DOM change.
One Card for Each Project
Chapter 7 moved the projects array into js/projects.js and gave js/main.js a renderProjects function to import. Now those pieces get their real job. The first step is a function, createCard, that takes one project object and returns one finished card element. Add it to js/projects.js.
export function createCard(project) {
const card = document.createElement("article");
card.classList.add("project-card");
const heading = document.createElement("h2");
const link = document.createElement("a");
link.href = project.url;
link.textContent = project.title;
heading.append(link);
const description = document.createElement("p");
description.textContent = project.description;
const tags = document.createElement("ul");
for (const tag of project.tags) {
const item = document.createElement("li");
item.textContent = tag;
tags.append(item);
}
card.append(heading, description, tags);
return card;
}
The function makes an article with the class project-card. Inside it goes a heading holding a link to the project, with the project's title as the link text. Then a paragraph with the description, and a list with one item per tag, built by a for...of loop. The last lines put the three parts into the card, in order, and hand the card back. Nothing is added to the page yet; the function only builds.
Every piece of text goes in with textContent. The projects are written by you today, but Chapter 11 loads them from a file and then from a server, and topic 3's rule is to treat text from outside the code as text, always.
The function works, and it has a real weakness: from the code alone it is hard to see what a card looks like. Twenty lines of making and appending hide a shape that fits in a few lines of HTML.
template: Carve the Stamp Once
The template element holds HTML that the browser reads but does not show. Its contents are inert: nothing in them is drawn, no picture loads, no script runs. The template waits in the page until a script copies it. Add this one near the end of the main element on projects.html.
<template id="project-card-template">
<article class="project-card">
<h2><a></a></h2>
<p></p>
<ul></ul>
</article>
</template>
The page looks exactly as before: a template shows nothing. It is the empty shape of a card, an article holding a heading with a link, a paragraph and a list, waiting to be filled in. Now createCard can copy that shape instead of building it piece by piece. Replace the first version with this one.
export function createCard(project) {
const template = document.querySelector("#project-card-template");
const card = template.content.firstElementChild.cloneNode(true);
const link = card.querySelector("a");
link.href = project.url;
link.textContent = project.title;
card.querySelector("p").textContent = project.description;
const tags = card.querySelector("ul");
for (const tag of project.tags) {
const item = document.createElement("li");
item.textContent = tag;
tags.append(item);
}
return card;
}
The function finds the template. Its content holds the parsed HTML, and firstElementChild is the article inside. cloneNode with true makes a copy of the article and everything inside it; without true it would copy only the empty article. The rest fills the copy in: the link's address and text, the description, and one list item per tag. Each search, such as the one for the paragraph, looks only inside this card, as topic 2 showed.
The card's shape now lives in HTML, where you can read it, and the script only fills in text. Changing the card's layout later means editing the template, not the function.
Replacing, Not Adding
The second function, renderProjects, puts the cards on the page. It takes an array of projects and fills the grid with one card for each. It replaces the version from Chapter 7, which only wrote titles to the Console, so delete that one: a module cannot export two functions with the same name.
export function renderProjects(projects) {
const grid = document.querySelector(".project-grid");
if (!grid) {
return;
}
grid.replaceChildren();
for (const project of projects) {
grid.append(createCard(project));
}
}
It finds the grid and, on the pages that have none, stops at once: the guard from topic 2. Then replaceChildren, called with nothing inside the round brackets, removes everything already in the grid. Only then does the loop append a fresh card for each project.
That emptying line is what makes the function safe to call again. Without it, a second call would append three more cards after the first three, and the page would show six. In Chapter 9 the project filter calls renderProjects on every click, with a different list each time, so each call must replace the grid's contents rather than add to them.
The Site's Projects Page
Now the hand-written cards can go. On projects.html, delete the three card article elements and leave the grid empty. Below it, add a noscript element: its contents are shown only when JavaScript is turned off in the browser, so a visitor browsing that way still gets a plain list of links to the three projects.
<main>
<h1>Projects</h1>
<div class="project-grid"></div>
<noscript>
<ul>
<li><a href="https://example.com/budget">Budget Tracker</a></li>
<li><a href="https://example.com/recipes">Recipe Cards</a></li>
<li><a href="https://example.com/weather">Weather Now</a></li>
</ul>
</noscript>
<!-- the template from above goes here -->
</main>
import { projects, renderProjects } from "./projects.js";
renderProjects(projects);
The call in js/main.js hands the whole projects array to renderProjects. Save everything and open the Projects page. You see the same three cards as before: Budget Tracker, Recipe Cards and Weather Now, each with its description, laid out by the grid from Chapters 4 and 5. One thing is new: under each description, a list of the project's tags, which the hand-written cards did not have. The difference is where they come from. View source shows an empty grid; the Elements panel shows three article elements inside it, built by the script.
To see the fallback, turn JavaScript off in DevTools: open the Command Menu with Ctrl+Shift+P on Windows or Cmd+Shift+P on macOS, type disable JavaScript, choose it, and reload. The grid stays empty and the list of three links appears. Turn JavaScript back on the same way.
- "Building the HTML as one long string is simpler." It invites topic 3's injection the moment any text comes from outside, and it is hard to read. A
templatefilled withtextContentis both safe and readable. - "Calling render twice is harmless." Without emptying the grid first, the second call adds a second copy of every card.
replaceChildrenmakes each render start clean. - "Content built by JavaScript is invisible to screen readers." It is in the DOM like everything else, and a screen reader reads the DOM. Built from real elements, an
article, a heading, a link, it is as accessible as hand-written HTML.
- The site's projects now come from data. Chapter 11 moves that data into a file of its own, and then onto the network.
- Rendering a page from data is the core idea behind React, Vue and Svelte, which the last chapter introduces. You have now done it by hand.
Knowledge Check
A script calls document.createElement("p") and sets its text. What does the visitor see?
- The new paragraph at the end of the page
- Nothing yet, until the paragraph is appended
- The first paragraph on the page, replaced by it
- An error, since the new paragraph has no parent
Why does renderProjects call grid.replaceChildren() before its loop?
- To create the grid if the page does not have one
- To sort the new cards into alphabetical order
- So a second render does not repeat the cards
- To check that every card has a title set
What happens to the HTML inside a template element when the page loads?
- It is drawn once on the page, as an empty example card
- It is ignored and cannot be used by any script
- It runs once, then is hidden from the page
- It is read but not shown, ready to be copied
Why is filling a template with textContent safe for text from a server?
- The text is never read as HTML, so it cannot run
- The template checks the text for bad tags first
- Text from a server can never contain HTML tags
- Cloning removes any code from the copied card
You got correct