Topic 02

Finding Elements

JavaScriptHands-on

Before a script can change a heading or hide a panel, it has to get hold of that element in the DOM. Walking down the tree from the body, child by child, would work until the day the HTML changes. Instead, you describe the element you want and let the browser search for it. The method that does this, querySelector, takes a CSS selector, the same kind you wrote in Chapter 3, and hands back the first element that matches.

It is like asking someone to search a building for "the first room with a blue door", or for "every room with a blue door". The description is the selector. If there is no such room, they come back empty-handed and say so; the search itself did not fail. In JavaScript that empty-handed answer is the value null, and using it by mistake causes one of the most common errors on the web.

How Do You Find an Element?

document.querySelector searches the whole page in the order the elements appear in the HTML and returns the first one that matches. Any CSS selector works: an element name, a class with its dot, an id with its hash sign, or a combination such as nav a, which means the links inside the navigation.

document.querySelectorAll returns every match, as a NodeList: a list of elements in page order. A NodeList works with for...of, so you can loop over the matches the way you looped over arrays in Chapter 7. It is not a true array, though. map and filter are not available on it until you turn it into one with Array.from.

Try both on the site's Projects page, which has the three hand-written cards from Chapter 4. Open the page through the preview server, open the Console, and type these lines one at a time.

Console
const grid = document.querySelector(".project-grid");
grid.children.length
const cards = document.querySelectorAll(".project-card");
cards.length
Array.from(cards).map((card) => card.tagName)
Console output
undefined
3
undefined
3
(3) ['ARTICLE', 'ARTICLE', 'ARTICLE']

The first line finds the grid and keeps it under the name grid. The Console answers undefined after any line that only creates a name, so you can ignore those two answers. The second line counts the grid's children: three. The third finds every element with the class project-card, and the fourth shows there are three of them. The last line converts the NodeList into an array and uses map to list each card's element name: three article elements, as Chapter 2 suggested for a piece that stands on its own.

Every element has these two methods too, not only document. Calling grid.querySelector("a") searches only inside the grid and returns its first link. Searching inside one element is how a script finds the title of one card among many, and topic 5 relies on it.

What Comes Back When Nothing Matches?

When no element matches, querySelector returns null, the value that means "nothing, on purpose" from Chapter 6. That on its own is not an error. The error comes on the next line, when the script uses null as though it were an element. Here the selector has a typo: projects with an s.

Console
document.querySelector(".projects-grid")
document.querySelector(".projects-grid").children
Console output
null
Uncaught TypeError: Cannot read properties of null (reading 'children')

The first line answers null: nothing on the page has that class. The second tries to read children from that null and stops with a red TypeError, the error from Chapter 6 that says the program cannot read properties of null, and names the property it was trying to read. When you see that message in your own code, the value on the left of the dot came from a search that found nothing.

There are three usual causes. The selector has a typo, often a missing dot before a class name. The element is on a different page from the one open, because one script is shared by several pages. Or the script ran before the element existed, which is the subject of the next section.

Did the selector match?
An element matches→you get the first match
Nothing matches: is the selector spelled right?→fix the selector
Nothing matches: is the element on this page?→guard with if
Nothing matches: did the script run too early?→defer, or a module

Why Does Timing Matter?

The browser reads your HTML from top to bottom, and it runs a plain script the moment it reaches the script element. A plain script in the head therefore runs before the browser has read the body. At that moment the heading, the grid and the footer are not in the DOM yet, so every search for them returns null.

To see it, make a practice script, js/early.js, that logs the page's h1, and load it in the head of index.html without defer.

js/early.js
console.log(document.querySelector("h1"));
Fragment
<head>
  <script src="js/early.js"></script>
</head>

Reload the page and the Console shows null, even though the heading is plainly on the screen. Add defer to that script element, reload, and the Console shows the h1 element itself, which you can expand and hover to highlight on the page. The heading was there all along; what changed is the moment the script ran. Remove the practice script when you are done.

This is why the site's scripts can find everything. Chapter 6 loaded them with defer, and Chapter 7 made js/main.js a module, which is deferred automatically. Both wait until the whole HTML has been read before they run.

How Do You Guard an Optional Element?

The site loads the same js/main.js on all three pages, but the project grid exists only on the Projects page. On the Home page, querySelector(".project-grid") returns null, and a line that reads from it without checking stops the whole script with that TypeError. Chapter 6 showed that one error stops the rest of a script, so every line after it would stop working on the Home page too.

The fix is a guard: an if that checks the element was found before using it. Add these lines at the end of js/main.js.

js/main.js
const grid = document.querySelector(".project-grid");

if (grid) {
  console.log(`This page shows ${grid.children.length} projects.`);
}

On the Projects page the Console shows "This page shows 3 projects." On the Home and Contact pages it shows nothing, and no error. The guard works because null counts as false in an if, one of the falsy values from Chapter 6. For a single read, the ?. operator from Chapter 7 does the same job in one line: it gives undefined instead of an error. These lines are only a check, so remove them once you have seen both results.

Guard only where an element is genuinely optional. When an element must be there, a guard would hide a real mistake; letting the error appear is how you find the typo.

Common Confusions
  • "querySelectorAll returns an array." It returns a NodeList, which works with for...of but has no map or filter. Convert it with Array.from when you need those.
  • "null means the page is broken." It means the search found nothing. Check the selector, the page you are on, and when the script ran.
  • "The class name needs no dot." querySelector takes a CSS selector, so a class needs its dot. Without it, the browser looks for an element with that name, finds none, and returns null.
Why It Matters
  • Finding an element is the first step of every interaction in the rest of the book.
  • Recognising the "cannot read properties of null" error, and its three causes, turns an hour of confusion into a minute's check.

Knowledge Check

A page has three paragraphs with the class note. What does document.querySelector(".note") return?

  • A list of all three matching paragraphs
  • The last matching paragraph on the page
  • The first matching paragraph on the page
  • null, because more than one paragraph matches

What does querySelector return when no element matches the selector?

  • The value null, meaning nothing was found
  • An error that stops the script on that line
  • An empty list with a length of zero
  • The body element, as a safe default

A plain script in the head, with no defer, searches for the page's h1 and gets null. Why?

  • Scripts in the head cannot see the page
  • The h1 needs an id before it can be found
  • The selector needs a dot in front of the name
  • The script ran before the h1 was read

Why does the site's code check if (grid) before using the project grid?

  • The grid might still be loading from the server
  • The same script runs on pages without a grid
  • Checking first makes the search run faster
  • Modules cannot search for elements by class

You got correct