Q1With 3 li elements that have data-id, what does querySelector("[data-id]") return?
The DOM Tree and Selecting Elements — querySelector and querySelectorAll
Covers querySelector and querySelectorAll for getting elements from the DOM tree: searching by class, id, and data attribute, turning a NodeList into an array, and ?. for missing elements.
Say you want to read the prices of every product in a product list. Selecting by id, like #total, only works for elements that have an id, so it doesn't suit a list of products that all share the same structure.
This article covers selectors for finding elements by conditions other than an id, querySelectorAll for collecting every match, and how to read an element that might not be there.
Getting a Single Element — querySelector and Selectors
Now suppose you want only the heading of the important notice in a list of notices. The list is made up of identical li elements, none of which has an id. An id is a name that only one element on a page can have, so it can't serve as a marker for a kind of item, such as "important notices".
A selector (a string describing the element you're looking for) specifies elements by tag name, as in li, by id, as in #news, or by class (a name you give to every element of the same kind), as in .notice. document.querySelector(selector) returns the first element in the DOM tree that matches.
// Set up a list of notices (an element can have several classes, separated by spaces)
document.body.innerHTML = `<ul id="news">
<li class="notice">Shipping fee update</li>
<li class="notice important">Unscheduled maintenance</li>
<li class="notice">New fall products</li>
</ul>`;
// Find by tag name / by class (the class name goes after the .)
console.log(document.querySelector("li").textContent); // Shipping fee update
console.log(document.querySelector(".notice").textContent); // Shipping fee update
console.log(document.querySelector(".important").textContent); // Unscheduled maintenance
"li"and".notice"— both stop searching here- Text is "Shipping fee update"
".important"— stops searching here- Text is "Unscheduled maintenance"
- Text is "New fall products"
- None of the three selectors gets this far
".notice" stops searching at the first one. What you get back is the first element that matches.The second li has two classes, so it matches both .notice and .important. To get only the second one, write a condition that nothing but the second one matches, such as .important.
Going Through a Whole List — querySelectorAll and NodeList
What if you want the headings of all the notices at once? querySelector stops searching at the first match, so however many times you call it with the same selector, you only get "Shipping fee update" and never reach the second one or beyond.
document.querySelectorAll(selector) returns every matching element in a NodeList (an array-like object that holds the elements it found in DOM tree order). You can read how many there are with length and handle them one at a time with forEach.
document.body.innerHTML = `<ul id="news">
<li class="notice">Shipping fee update</li>
<li class="notice important">Unscheduled maintenance</li>
<li class="notice">New fall products</li>
</ul>`;
// Get every matching element as a NodeList
const notices = document.querySelectorAll(".notice");
console.log(notices.length); // 3
notices.forEach((notice) => console.log(notice.textContent));
// Shipping fee update
// Unscheduled maintenance
// New fall products
// notices.map(...) is a TypeError. Turn it into an array before calling map
const titles = [...notices].map((notice) => notice.textContent);
console.log(titles.join(" / ")); // Shipping fee update / Unscheduled maintenance / New fall products
Even as a NodeList, you can read a single element by its index, as in notices[1]. The array made with spread syntax is a separate object, so notices stays a NodeList after the conversion, and you can keep using it with forEach.
Pointing to One Item by Its Number — data Attributes and Attribute Selectors
Suppose you need just the notice numbered N-103. If the same notices also appear in another section of the page, their ids would be duplicated, so an id won't work. A class is a name for grouping elements of the same kind, so it isn't a good place to store a number either.
A data attribute (an attribute whose name starts with data-) can hold any custom value you like, such as a number. Selectors that search by attribute go in square brackets: [data-id] matches elements that have the attribute, and [data-id="N-103"] matches elements whose value matches as well.
document.body.innerHTML = `<ul id="news">
<li class="notice" data-id="N-101">Shipping fee update</li>
<li class="notice important" data-id="N-102">Unscheduled maintenance</li>
<li class="notice" data-id="N-103">New fall products</li>
</ul>`;
// Search only by whether the attribute exists (all 3 match)
console.log(document.querySelector("[data-id]").textContent); // Shipping fee update
// Search for an element whose value matches too (the value is in ", so wrap the whole selector in ')
console.log(document.querySelector('[data-id="N-103"]').textContent); // New fall products
[data-id] without a value matches all three and returns the first, while [data-id="N-103"], with the value, points only to the one whose number matches. The table below lists the selectors used in this article and the element each one returns from the notice list.
| Selector | Matches | Returned from the notices |
|---|---|---|
| li | Tag name is li | 1st li (Shipping fee update) |
| .important | class includes important | 2nd li (Unscheduled maintenance) |
| #news | id is news | The ul element |
| [data-id] | Has a data-id attribute | 1st li (Shipping fee update) |
| [data-id="N-103"] | data-id is N-103 | 3rd li (New fall products) |
Values Starting with a Digit Fail Without Quotes
Some values work without quotes, like [data-id=N-103], but a value that starts with a digit, like [data-id=102], can't be read as a selector, and the line throws an "is not a valid selector" error. Always wrap values in ".
Keeping Going When an Element Is Missing — null and ?.
Think about a home page that shows a heading only when there's an urgent notice. On days without one, the element doesn't exist, so the line that reads the heading's text throws a TypeError, and nothing after it runs.
When no element matches, querySelector returns null. If you add optional chaining, ?., to the result before reading .textContent, it returns undefined without reading any further when the value is null, and adding ?? after it lets you set a default.
// A list with no urgent notice (class is urgent) yet
document.body.innerHTML = `<ul><li class="notice">New fall products</li></ul>`;
const urgent = document.querySelector(".urgent");
console.log(urgent); // null
// urgent.textContent would throw a TypeError
console.log(urgent?.textContent); // undefined
console.log(urgent?.textContent ?? "None"); // None
// In a list that has an urgent notice, compare with and without the .
document.body.innerHTML = `<ul><li class="urgent">Server outage</li></ul>`;
console.log(document.querySelector(".urgent")?.textContent ?? "None"); // Server outage
console.log(document.querySelector("urgent")?.textContent ?? "None"); // None
Only use ?. on elements that may be missing on some days, like the urgent notice. If you add it to elements that are always there, you won't notice typos, so leave it off, let the code throw a TypeError, and use the line number to track down the cause.
A NodeList with 0 Elements Isn't null
Even when nothing matches, querySelectorAll doesn't return null; it returns a NodeList whose length is 0. Calling forEach on it doesn't throw an error and simply does nothing, so to find out that nothing matched, check length === 0 rather than using ?..
Knowledge Check
Answer each question one by one.
Q2What happens if you call map directly on the return value of querySelectorAll(".notice")?
Q3When the element doesn't exist, what is the value of querySelector(".sale-price")?.textContent?