Enhanced Collections — Array Methods on queryAll() Results
Quick Start (30 seconds)
const items = queryAll('.item');
// Array methods — built in
items.forEach(el => console.log(el.textContent));
items.map(el => el.dataset.id);
items.filter(el => !el.classList.contains('hidden'));
items.find(el => el.dataset.id === '42');
// Navigation helpers
items.first(); // First element
items.last(); // Last element
items.at(-1); // Last element (negative index)
items.isEmpty(); // true if no elements
items.toArray(); // Convert to real ArrayWhat Are Enhanced Collections?
When you call queryAll() or queryAllWithin(), you don't get a raw browser NodeList. Instead, you get an enhanced collection — a wrapper that adds:
- Array methods —
.forEach(),.map(),.filter(),.find(),.some(),.every(),.reduce() - Navigation helpers —
.first(),.last(),.at(),.isEmpty() - Collection .update() — update all elements at once
- Index access —
collection[0],collection[1], etc. - Iteration — works with
for...ofand spread[...]
Simply put, enhanced collections behave like arrays with superpowers.
Why Enhanced Collections?
The Problem with Raw NodeLists
Browser's querySelectorAll() returns a NodeList, which is missing many useful array methods:
// ❌ Raw NodeList — limited methods
const items = document.querySelectorAll('.item');
items.map(el => el.textContent); // ERROR — .map() doesn't exist
items.filter(el => el.disabled); // ERROR — .filter() doesn't exist
items.find(el => el.id === 'main'); // ERROR — .find() doesn't exist
// You have to convert to array first
Array.from(items).map(el => el.textContent); // Works but verboseEnhanced Collections — It Just Works
// ✅ Enhanced collection — array methods built in
const items = queryAll('.item');
items.map(el => el.textContent); // ✅ Works directly
items.filter(el => el.disabled); // ✅ Works directly
items.find(el => el.id === 'main'); // ✅ Works directlyArray Methods
Every enhanced collection has these standard array methods. All elements passed to callbacks are auto-enhanced with .update().
.forEach(callback)
Execute a function for each element:
queryAll('.card').forEach((card, index) => {
card.update({
textContent: `Card ${index + 1}`,
dataset: { position: String(index) }
});
});.map(callback)
Create a new array by transforming each element:
// Get all text contents
const texts = queryAll('.item').map(el => el.textContent);
console.log(texts); // ["Item 1", "Item 2", "Item 3"]
// Get all data attributes
const ids = queryAll('.card').map(el => el.dataset.id);
console.log(ids); // ["1", "2", "3"].filter(callback)
Get only elements that match a condition:
// Get only active items
const active = queryAll('.item')
.filter(el => el.classList.contains('active'));
console.log(active.length); // Number of active items
// Get only visible inputs
const visible = queryAll('input')
.filter(el => !el.hidden);.find(callback)
Find the first element that matches a condition:
// Find the element with a specific data attribute
const target = queryAll('.card')
.find(el => el.dataset.id === '42');
if (target) {
target.update({ classList: { add: ['highlighted'] } });
}.some(callback)
Check if at least one element matches:
const hasDisabled = queryAll('.btn')
.some(el => el.disabled);
console.log(hasDisabled); // true or false.every(callback)
Check if all elements match:
const allValid = queryAll('.form-input')
.every(el => el.value.trim() !== '');
console.log(allValid); // true if all inputs have values.reduce(callback, initialValue)
Accumulate a value across all elements:
// Sum all prices
const total = queryAll('.product')
.reduce((sum, el) => sum + parseFloat(el.dataset.price), 0);
console.log(total); // e.g., 149.97
// Collect all IDs
const allIds = queryAll('.card')
.reduce((ids, el) => [...ids, el.id], []);
console.log(allIds); // ["card1", "card2", "card3"]Navigation Helpers
.first()
Returns the first element (enhanced), or null if empty:
const first = queryAll('.item').first();
first.update({ classList: { add: ['first-item'] } });.last()
Returns the last element (enhanced), or null if empty:
const last = queryAll('.item').last();
last.update({ classList: { add: ['last-item'] } });.at(index)
Returns the element at a specific index. Supports negative indices:
const items = queryAll('.item');
items.at(0); // First element
items.at(1); // Second element
items.at(-1); // Last element
items.at(-2); // Second to lastReturns null if the index is out of range:
items.at(999); // null
items.at(-999); // null.isEmpty()
Returns true if the collection has no elements:
const results = queryAll('.search-result');
if (results.isEmpty()) {
query('#noResults').update({ hidden: false });
} else {
results.forEach(r => r.update({ hidden: false }));
}.toArray()
Converts the collection to a real JavaScript Array:
const arr = queryAll('.item').toArray();
console.log(Array.isArray(arr)); // true
// Useful when you need actual Array methods not on the collection
arr.sort((a, b) => a.textContent.localeCompare(b.textContent));Index Access
You can access elements by numeric index directly:
const cards = queryAll('.card');
cards[0] // First card (enhanced with .update())
cards[1] // Second card
cards[2] // Third card
cards[0].update({ textContent: 'First Card' });Iteration
Enhanced collections work with for...of loops and the spread operator:
for...of
for (const card of queryAll('.card')) {
card.update({ style: { margin: '8px' } });
}Spread Operator
const elements = [...queryAll('.item')];
console.log(Array.isArray(elements)); // trueCollection .update()
The collection itself has an .update() method that applies updates to all elements at once:
// Update every .card element
queryAll('.card').update({
style: { padding: '16px', borderRadius: '8px' },
classList: { add: ['shadow'] }
});This is equivalent to:
queryAll('.card').forEach(card => {
card.update({
style: { padding: '16px', borderRadius: '8px' },
classList: { add: ['shadow'] }
});
});Real-World Examples
Example 1: Filter and Highlight
// Highlight all products over $50
queryAll('.product')
.filter(el => parseFloat(el.dataset.price) > 50)
.forEach(el => {
el.update({
classList: { add: ['premium'] },
style: { borderColor: 'gold' }
});
});Example 2: Collect Form Data
// Get all form field values as an object
const formData = queryAll('#myForm input').reduce((data, input) => {
data[input.name] = input.value;
return data;
}, {});
console.log(formData);
// { username: "Alice", email: "alice@example.com", ... }Example 3: Check If All Fields Valid
const allFilled = queryAll('.required-field')
.every(field => field.value.trim() !== '');
if (allFilled) {
query('#submitBtn').update({ disabled: false });
} else {
query('#submitBtn').update({ disabled: true });
}Summary
| Category | Methods |
|---|---|
| Array methods | .forEach(), .map(), .filter(), .find(), .some(), .every(), .reduce() |
| Navigation | .first(), .last(), .at(index), .isEmpty(), .toArray() |
| Bulk update | .update({...}) — applies to all elements |
| Index access | collection[0], collection[1], etc. |
| Iteration | for...of, spread [...] |
Simple Rule to Remember: Enhanced collections from
queryAll()work like arrays — you get.forEach(),.map(),.filter(), and.find()out of the box. Every element inside is auto-enhanced with.update(). Use.first(),.last(), and.at(-1)for quick access to specific positions.