DOM Manipulation
Master how to manipulate the Document Object Model using JavaScript.
Content
Selecting DOM Elements
Versions:
Watch & Learn
Selecting DOM Elements — The No-Chill Selector Breakdown
You learned how the event loop queues up things, and how callbacks arrive fashionably late to the party. Now let’s decide who they talk to.
If the DOM is the party, selecting elements is handing out name tags. This lesson tells you how to find the people you care about, how to do it fast, and how to avoid accidentally talking to the ficus.
Why this matters (and how it ties to what you already know)
You recently studied asynchronous JavaScript: event loop, callbacks, promises, and best practices. Those are the ways code runs. But before any event listener, setTimeout handler, or fetch callback can manipulate the UI, it needs to find the DOM node to touch. If you misunderstand selectors, your callbacks will be queued perfectly and then do nothing useful when fired.
- Selecting happens synchronously — you call a selector, you get a reference now. But interacting with a node might need DOM to be ready, which is where DOMContentLoaded and window.onload (remember async topics) come in.
The selector toolbox — quick tour
1) document.getElementById('id')
- Fast, returns a single element or null.
- Use for unique ids, VIP pass style.
Code example:
const title = document.getElementById('page-title');
if (title) title.textContent = 'Hello world';
2) document.getElementsByClassName('class')
- Returns an HTMLCollection (live). Useful for class-based groups.
- Live means if DOM changes, the collection updates automatically. Sounds cool, but can bite performance.
const items = document.getElementsByClassName('todo-item');
3) document.getElementsByTagName('tag')
- Another live HTMLCollection, for tag names.
4) document.querySelector(selector)
- Returns the first element matching a CSS selector. Super flexible.
- Great for scoped, specific queries.
const primaryBtn = document.querySelector('.btn.primary');
5) document.querySelectorAll(selector)
- Returns a static NodeList of everything that matches. Not live. You get a snapshot.
const links = document.querySelectorAll('nav a');
6) element.closest(selector)
- Walks up ancestors and returns closest match. Very handy when handling events delegated to parents.
const clickedCard = event.target.closest('.card');
7) element.matches(selector)
- Test if an element matches a selector. Good for conditionals in event delegation.
if (e.target.matches('.delete-btn')) { /* do deletion */ }
Live vs static collections — know the difference
| Method | Returns | Live? | Good for |
|---|---|---|---|
| getElementById | Element or null | No | Unique lookup |
| getElementsByClassName | HTMLCollection | Yes | Old-school fast loops (but be careful) |
| getElementsByTagName | HTMLCollection | Yes | Whole-tag queries |
| querySelector | Element or null | No | First match, CSS power |
| querySelectorAll | NodeList | No (static) | Snapshot of matches |
Quick rule: prefer querySelector/querySelectorAll unless you need a micro-optimized path or are working with legacy code.
Performance tips (because waiting for reflows is a mood killer)
- Cache selectors: If you query the same selector in a loop or callback repeatedly, store it in a variable.
- Limit scope: Do rootElement.querySelectorAll('.item') instead of document.querySelectorAll('.item') when possible.
- Avoid heavy selectors in loops: Don’t run document.querySelectorAll('.x .y .z') inside thousands of iterations.
- Be mindful of live collections: HTMLCollection updates can make loops behave weirdly.
Example bad -> good:
// Bad: querying inside loop
for (let i = 0; i < itemsCount; i++) {
const el = document.querySelectorAll('.row')[i];
// ...
}
// Good: query once
const rows = document.querySelectorAll('.row');
for (let i = 0; i < rows.length; i++) {
const el = rows[i];
// ...
}
Event delegation and selectors — the power move
Remember the event loop and how callbacks get fired? Combine that with delegation: attach one listener to a parent and use selectors to detect which child was interacted with. This reduces listeners and improves memory use.
// Delegation example
const list = document.getElementById('todo-list');
list.addEventListener('click', function (e) {
const btn = e.target.closest('.remove-btn');
if (!btn) return;
const item = btn.closest('.todo-item');
item.remove(); // quick, and this will run on event loop tick
});
Question time: Why does using closest inside the listener work reliably even when list items are added later? Because the listener is on the parent and selection happens at event time — the DOM reference is resolved at callback runtime, which fits nicely with async flows you already studied.
Common mistakes and how to avoid them
- Trying to select elements before DOM is ready. Fix: run code on DOMContentLoaded or place scripts at bottom of body.
- Confusing NodeList vs HTMLCollection — expect different behaviors for live updates.
- Using overly general selectors and accidentally grabbing dozens of nodes you did not mean to manipulate.
- Not handling null returns from querySelector or getElementById. Always guard.
Mini cheat sheet
- Use getElementById for unique elements you touch often.
- Use querySelector for targeted, readable queries.
- Use querySelectorAll and Array.from(...) if you need array methods.
- Use closest and matches for delegation.
- Cache, limit scope, prefer static NodeLists unless you specifically need a live collection.
Closing with a truth bomb
Selecting DOM elements is simple. Selecting the right elements at the right time, in a way that plays nicely with the event loop and future DOM changes, is where the craft lives.
Key takeaways:
- Selectors are the handshake between your logic and the page. Choose them intentionally.
- Think about timing: selection is synchronous but DOM readiness and event firing are asynchronous; use DOMContentLoaded or delegation wisely.
- Prefer readable, scoped selectors and cache where performance matters.
Go forth. Give the right people name tags. Avoid talking to the ficus.
Version notes: this builds on your understanding of async JS by focusing on the synchronous step that usually runs before your async callbacks. Next, we will actually mutate nodes and explore reflows, paints, and how to minimize layout thrashing — yes, we will fight the browser and win.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!