The Selector Helper
You've learned Elements for finding one element by ID, and Collections for finding groups by class, tag, or name. Now meet the third helper: Selector.
Selector gives you the full power of CSS selectors — combinators, pseudo-classes, attribute selectors, complex chains — anything that querySelector and querySelectorAll support. With caching and .update() built right in.
Quick Start (30 Seconds)
// Find ONE element — any CSS selector
const header = Selector.query('#site-header');
const firstBtn = Selector.query('button');
const emailInput = Selector.query('input[type="email"]');
// Find ALL matching elements
const allBtns = Selector.queryAll('.btn');
const allRequired = Selector.queryAll('input[required]');
const paragraphs = Selector.queryAll('section > p');
// Update what you found
header.update({ textContent: 'Welcome!' });
allBtns.update({ disabled: false });
// Search inside a specific container
const formInput = Selector.Scoped.within('#loginForm', 'input[type="email"]');
const formBtns = Selector.Scoped.withinAll('#loginForm', 'button');
// Bulk update with CSS selectors as keys
Selector.update({
'#hero h1': { textContent: 'New Title' },
'.card': { classList: { add: 'loaded' } },
'input[required]': { classList: { add: 'required-mark' } }
});What is the Selector Helper?
The Selector Helper gives you the full power of CSS selectors — querySelector and querySelectorAll — with caching and .update() added on top.
Here's how it fits alongside the other two helpers:
| Helper | Targets | CSS Power |
|---|---|---|
Elements | One element by ID | IDs only |
Collections | Groups by class / tag / name | Three basic options |
Selector | Any CSS selector | Full CSS: combinators, pseudo-classes, attributes, etc. |
Think of it like this:
Elementsis for named elements you know and controlCollectionsis for groups that share a common traitSelectoris for when you need precision targeting with the full power of CSS
Why Does This Exist?
When Elements and Collections Already Cover Most Cases
For the majority of DOM access, Elements and Collections are the right tools:
// Know the ID? → Elements is the cleanest choice
Elements.submitBtn.update({ disabled: false });
// Need all elements of a class? → Collections is purpose-built
Collections.ClassName.btn.update({ disabled: false });These methods are great when you need: ✅ Fast, straightforward access by ID or class ✅ Clean, readable syntax that maps directly to your HTML structure ✅ The most common patterns in everyday DOM work
When Selector Provides the Most Direct Path
In scenarios where you need complex CSS targeting — relationships between elements, pseudo-classes, attribute combinations — Selector provides the precision that the other helpers can't:
// Need the first button inside a specific form?
Selector.query('#loginForm button[type="submit"]');
// Need all required fields inside one particular section?
Selector.queryAll('#billingSection input[required]');
// Need all active nav links that aren't disabled?
Selector.queryAll('nav li.active > a:not([disabled])');Selector is especially useful when: ✅ You need CSS combinator targeting (descendant, child, sibling) ✅ You need pseudo-class selectors (:first-child, :not(), :focus) ✅ You need attribute selectors ([type="email"], [data-role], [aria-hidden="true"]) ✅ Your selector logic is more complex than a single class or ID
The Choice is Yours:
- Use
Elementswhen you know the element's ID - Use
Collectionswhen you want all elements of a class/tag/name - Use
Selectorwhen you need the full power of CSS targeting - All three work together and complement each other
Mental Model — A Spotlight of Any Shape
Think of CSS selectors like a spotlight on a stage. Elements is a pinpoint spotlight on one known actor (by ID badge). Collections is a broad spotlight covering everyone wearing the same costume (by class). Selector is a spotlight of any shape you can describe in CSS — you define the shape, it illuminates what matches.
Selector.query('form button[type="submit"]')
↓
CSS spotlight: "show me buttons of type submit, inside a form"
↓
Scans the DOM with that shape
↓
Returns the first match (with .update() attached)Selector.query() — Find One Element
Like document.querySelector() — finds the first element that matches the CSS selector. Returns the element (with .update() attached) or null if nothing matches.
Syntax
Selector.query(cssSelector)Examples
// By ID (though Elements would be simpler here)
Selector.query('#myHeader')
// By class — first match only
Selector.query('.primary-btn')
// By attribute
Selector.query('input[type="submit"]')
Selector.query('[data-action="delete"]')
// By relationship (CSS combinators)
Selector.query('form > .submit-area button')
Selector.query('nav li:first-child a')
// Pseudo-classes
Selector.query('input:focus')
Selector.query('li:nth-child(2)')
Selector.query('input:not([disabled])')Using the Result
const input = Selector.query('input[type="email"]');
if (input) {
input.value = '';
input.update({
classList: { add: 'validated' },
setAttribute: { 'aria-invalid': 'false' }
});
}Selector.queryAll() — Find All Matching Elements
Like document.querySelectorAll() — finds all elements matching the selector. Returns an enhanced collection with .update() and array methods.
Syntax
Selector.queryAll(cssSelector)Examples
// All elements with a class
const cards = Selector.queryAll('.card');
// All inputs of a specific type
const textInputs = Selector.queryAll('input[type="text"]');
// Child combinator
const directChildren = Selector.queryAll('section > p');
// Multiple selectors (comma-separated)
const headings = Selector.queryAll('h1, h2, h3');
// Attribute presence check
const withDataId = Selector.queryAll('[data-id]');
// Complex selectors
const activeNavLinks = Selector.queryAll('nav ul li.active > a');Using the Result
const cards = Selector.queryAll('.card');
// Behaves like a collection — same array methods as Collections
console.log(cards.length); // how many
cards.forEach(card => { ... }); // iterate
const texts = cards.map(c => c.textContent);
// Has .update() — applies to all elements
cards.update({
classList: { add: 'animated' },
style: { opacity: '1' }
});
// Navigate
const firstCard = cards.first();
const lastCard = cards.last();Selector.Scoped — Search Inside a Container
Selector.Scoped runs a CSS selector query within a specific container element — not the entire document. This is the equivalent of container.querySelector() and container.querySelectorAll().
Why This Is Useful
Without scoping:
Selector.query('input')
→ searches entire document
→ might find the wrong input if there are many forms on the page
With scoping:
Selector.Scoped.within('#loginForm', 'input')
→ searches ONLY inside #loginForm
→ finds exactly the input you intendedSelector.Scoped.within(container, selector) — One Element
// container can be a CSS selector string or an HTMLElement:
const submitBtn = Selector.Scoped.within('#loginForm', 'button[type="submit"]');
const errorMsg = Selector.Scoped.within('#loginForm', '.error-message');
// Using an element reference as the container:
const form = Elements.loginForm;
const input = Selector.Scoped.within(form, 'input[name="email"]');Selector.Scoped.withinAll(container, selector) — All Matching Elements
// All inputs inside a specific form
const formInputs = Selector.Scoped.withinAll('#signupForm', 'input, select, textarea');
// All active links inside a nav
const activeLinks = Selector.Scoped.withinAll('#mainNav', 'a.active');
// Update them
formInputs.update({ disabled: true });Real Example: Multi-Step Form Validation
function validateStep(stepId) {
// Only validate inputs IN this specific step
const inputs = Selector.Scoped.withinAll('#' + stepId, 'input[required]');
const allFilled = inputs.every(input => input.value.trim() !== '');
// Only update the next button for THIS step
const nextBtn = Selector.Scoped.within('#' + stepId, '.next-btn');
if (nextBtn) {
nextBtn.update({ disabled: !allFilled });
}
}
validateStep('step1');
validateStep('step2');Selector.update() — Bulk Update with CSS Selectors
Updates multiple groups of elements — each targeted by a CSS selector — in a single call. This is the most powerful bulk-update method in the library.
Syntax
Selector.update({
'css-selector-1': { /* update object */ },
'css-selector-2': { /* update object */ }
});Example: Dark Mode Activation
function activateDarkMode() {
Selector.update({
// Update by ID
'#siteHeader': { style: { backgroundColor: '#1a1a2e' } },
// Update by class
'.card': {
classList: { add: 'dark-mode' },
style: { backgroundColor: '#16213e', color: '#eee' }
},
// Update by tag + type
'input, textarea': {
style: { backgroundColor: '#0f3460', color: '#eee', borderColor: '#444' }
},
// Update by relationship
'nav > ul > li > a': {
style: { color: '#e94560' }
}
});
}The Return Value
const results = Selector.update({
'.btn': { disabled: false },
'#nonExistentId': { textContent: 'test' }
});
console.log(results['.btn']);
// { success: true, elements: ..., elementsUpdated: 3 }
console.log(results['#nonExistentId']);
// { success: true, elements: null, elementsUpdated: 0,
// warning: 'No elements found matching selector' }Waiting for Elements — Async Methods
Selector.waitFor(selector, timeout) — Wait for One Element
// Wait up to 5 seconds (default) for an element to appear
const modal = await Selector.waitFor('.modal');
// Custom timeout (3 seconds)
const lazyContent = await Selector.waitFor('#lazy-content', 3000);
lazyContent.update({ classList: { add: 'visible' } });Selector.waitForAll(selector, minCount, timeout) — Wait for Multiple
// Wait until at least 3 .card elements are in the DOM
const cards = await Selector.waitForAll('.card', 3);
cards.forEach(card => card.classList.add('loaded'));
// With custom timeout (10 seconds)
const items = await Selector.waitForAll('.item', 5, 10000);How Does Caching Work in Selector?
The Selector helper caches both query (single) and queryAll (multiple) results by selector string.
Selector.query('#hero h1') called first time:
↓
Creates cache key: "single:#hero h1"
↓
document.querySelector('#hero h1') ← DOM lookup
↓
Element found → enhances with .update() → stores in cache
↓
Returns enhanced element
Selector.query('#hero h1') called again:
↓
Looks up "single:#hero h1" in cache
↓
Found! Is it still in document? → YES ✨
↓
Returns from cache (no DOM lookup)Why the Selector Cache Is More Conservative
Because CSS selectors can depend on many aspects of the DOM (structure, classes, attributes, pseudo-states), Selector uses a more aggressive invalidation strategy:
DOM structural change (nodes added/removed)
→ Entire cache cleared (can't know which selectors are affected)
Attribute change (id, class, style, hidden)
→ Selectors that reference those attributes are invalidatedThe Selector cache clears more readily than Elements/Collections — because CSS selectors can be affected by so many DOM changes. This keeps results accurate.
Cache Utilities
// Check performance
const stats = Selector.stats();
console.log(stats.hitRate); // 0.0 to 1.0
console.log(stats.selectorBreakdown);
// { id: 5, class: 12, tag: 3, attribute: 2, complex: 1 }
// Shows what types of selectors you're using most
// Clear cache
Selector.clear();Comparing All Three Helpers
Let's say you want to find a submit button. Here's how each helper approaches it:
// 1. You know the ID → Elements (fastest, simplest)
Elements.submitBtn
// → Direct getElementById. Use this when you have an id.
// 2. You know the class → Collections (fast, great for groups)
Collections.ClassName['submit-btn'].first()
// → getElementsByClassName. Optimized in browsers.
// 3. You need CSS targeting → Selector (most flexible)
Selector.query('form button[type="submit"]')
// → querySelector. Handles any CSS relationship.The simple rule:
- Know the ID? →
Elements - Need all elements of a class/tag/name? →
Collections - Need complex CSS targeting? →
Selector
Common Mistakes to Avoid
❌ Using query() When You Need queryAll()
// Wrong — query returns ONE element only
const btns = Selector.query('.btn');
btns.forEach(btn => btn.textContent = 'Click'); // ❌ forEach doesn't exist on one element
// Right — use queryAll for multiple
const btns = Selector.queryAll('.btn');
btns.forEach(btn => btn.textContent = 'Click'); // ✅❌ Not Checking for null After query()
// Risky — query returns null if nothing matches
const el = Selector.query('.might-not-exist');
el.update({ textContent: 'test' }); // ❌ TypeError if el is null
// Good — check first
const el = Selector.query('.might-not-exist');
if (el) {
el.update({ textContent: 'test' }); // ✅
}
// Also good — optional chaining
Selector.query('.might-not-exist')?.update({ textContent: 'test' }); // ✅❌ Using Selector When Elements Would Be Simpler
// Over-engineered — using Selector for an ID lookup
Selector.query('#submitBtn').update({ textContent: 'Done' });
// Simpler — use Elements for IDs
Elements.submitBtn.update({ textContent: 'Done' }); // ✅ More readable❌ Using Collections Syntax Inside Selector
// Wrong — "class:btn" is Collections syntax, not valid CSS
Selector.query('class:btn'); // ❌ Invalid selector
// Right — use proper CSS class selector
Selector.query('.btn'); // ✅
Selector.queryAll('.btn'); // ✅ for allReal-World Example: Scoped Form Validation
async function submitForm(formId) {
// Get all required fields INSIDE this specific form
const requiredInputs = Selector.Scoped.withinAll('#' + formId, 'input[required], textarea[required]');
// Validate each one
const errors = [];
requiredInputs.forEach(input => {
if (!input.value.trim()) {
errors.push(input.name || input.id || 'Unknown field');
// Mark the field as invalid
input.update({
classList: { add: 'field-error' },
setAttribute: { 'aria-invalid': 'true' }
});
} else {
// Mark as valid
input.update({
classList: { remove: 'field-error' },
setAttribute: { 'aria-invalid': 'false' }
});
}
});
if (errors.length > 0) {
// Show error banner — scoped to this form
const errorBanner = Selector.Scoped.within('#' + formId, '.error-banner');
if (errorBanner) {
errorBanner.update({
textContent: `Please fill in: ${errors.join(', ')}`,
style: { display: 'block' }
});
}
return false;
}
return true;
}Summary — Key Takeaways
Selector.query(css)— finds the first element matching a CSS selector; returns element ornullSelector.queryAll(css)— finds all elements matching; returns enhanced collection with.update()and array methodsSelector.Scoped.within(container, css)— search inside a specific container; scoped to avoid accidental matchesSelector.Scoped.withinAll(container, css)— find all matches inside a containerSelector.update({})— bulk update using CSS selectors as keys; returns per-selector results- Caching is smart — results are cached by selector string; cache clears when DOM structure changes
waitFor/waitForAll— async methods to wait for dynamically injected content- Full CSS power — combinators, pseudo-classes, attribute selectors, complex chains — anything valid CSS can do
- Use Selector when you need complex targeting; when
Elements(ID) orCollections(class/tag/name) aren't precise enough
What's Next?
Now let's go deep on the thing that ties all three helpers together: the .update() method — the universal DOM update system that every element and collection receives.
Continue to 05 — The .update() Method →