Best Practices — Mastering Collections
Quick Start (30 Seconds)
Before writing any Collections code, ask yourself two questions:
- Do I need one specific element? → Use the Elements Helper (
Elements.myId) - Do I need a group of elements? → Use Collections — then ask which type fits the HTML
// One specific element → Elements Helper
const header = Elements.mainHeader;
// Multiple elements by class → Collections.ClassName
const cards = Collections.ClassName.card;
// Multiple elements by tag → Collections.TagName
const allLinks = Collections.TagName.a;
// Multiple elements by name → Collections.Name
const radioGroup = Collections.Name.plan;The Decision Tree
Use this to quickly choose the right approach:
Need to access DOM elements?
│
├─ Do I know the exact ID?
│ └─ YES → Use Elements Helper
│ Elements.myElementId ✅
│
└─ Do I need a GROUP of elements?
│
├─ Do they share a CSS class? (class="btn", class="card")
│ └─ YES → Collections.ClassName
│ Collections.ClassName.btn ✅
│
├─ Do I want all elements of a tag type? (<p>, <a>, <input>)
│ └─ YES → Collections.TagName
│ Collections.TagName.p ✅
│
├─ Do they share a name attribute? (name="email", name="color")
│ └─ YES → Collections.Name
│ Collections.Name.color ✅
│
└─ Are they loaded dynamically (AJAX/async)?
└─ YES → Collections.waitFor()
await Collections.waitFor('class', 'item') ✅Method Selection Guides
Access Methods
| Scenario | Use | Example |
|---|---|---|
All buttons with class btn | Collections.ClassName | Collections.ClassName.btn |
All <p> tags | Collections.TagName | Collections.TagName.p |
| All radio buttons in group | Collections.Name | Collections.Name.color |
| Element loaded via AJAX | Collections.waitFor() | await Collections.waitFor('class', 'item') |
| Single element by ID | Elements Helper | Elements.submitButton |
Array Methods
| Goal | Use | Why |
|---|---|---|
| Do something with each element | forEach | Side effects on DOM |
| Extract data from elements | map | Get text, attributes, etc. |
| Get a subset | filter | Conditional selection |
| Find the first match | find | Stops at first hit |
| Check if any match | some | Short-circuits — efficient |
| Check if all match | every | Short-circuits — efficient |
| Combine into one value | reduce | Sum, group, build objects |
| Need plain array for sort/splice | toArray | When you need native Array |
DOM Manipulation Methods
| Goal | Use | Chainable |
|---|---|---|
| Add CSS class to all | addClass | ✅ |
| Remove CSS class from all | removeClass | ✅ |
| Toggle CSS class on all | toggleClass | ✅ |
| Set JS property on all | setProperty | ✅ |
| Set HTML attribute on all | setAttribute | ✅ |
| Set CSS styles on all | setStyle | ✅ |
| Add event listener to all | on | ✅ |
| Remove event listener from all | off | ✅ |
Filtering Methods
| Goal | Use | Returns |
|---|---|---|
| Only visible elements | visible() | Plain array |
| Only hidden elements | hidden() | Plain array |
| Only enabled form elements | enabled() | Plain array |
| Only disabled form elements | disabled() | Plain array |
Utility Methods
| Goal | Use | Returns |
|---|---|---|
| First element | first() | Element or undefined |
| Last element | last() | Element or undefined |
| Element at position | at(index) | Element or undefined |
| Check if collection is empty | isEmpty() | boolean |
Do's and Don'ts
✅ DO: Store Collection References
Access the collection once and save it in a variable. Each access is fast (cached), but storing the reference makes code cleaner and avoids even that minimal overhead.
// ✅ Good — access once, use many times
const buttons = Collections.ClassName.btn;
buttons.addClass('initialized');
buttons.on('click', handleClick);
buttons.setStyle({ cursor: 'pointer' });// ❌ Less clean — accessing the same collection multiple times
Collections.ClassName.btn.addClass('initialized');
Collections.ClassName.btn.on('click', handleClick);
Collections.ClassName.btn.setStyle({ cursor: 'pointer' });✅ DO: Chain Related Operations
When multiple operations target the same collection, chain them. It's shorter and makes the relationship between operations obvious.
// ✅ Good — clearly shows these operations are related
Collections.ClassName.card
.addClass('active')
.setStyle({ opacity: '1', transform: 'scale(1)' })
.setAttribute('aria-selected', 'true')
.on('click', handleCardClick);// ❌ Less clear — operations are scattered
const cards = Collections.ClassName.card;
// ... 30 lines of other code ...
cards.addClass('active');
// ... 20 more lines ...
cards.on('click', handleCardClick);✅ DO: Use the Appropriate Array Method
Pick the method designed for your goal — it makes intent clear and runs efficiently.
// ✅ find() — want the first match
const activeItem = items.find(item => item.classList.contains('active'));
// ✅ some() — want to know if any match
const hasErrors = inputs.some(input => !input.validity.valid);
// ✅ every() — want to know if all match
const allFilled = inputs.every(input => input.value.trim() !== '');
// ✅ map() — want to extract data
const values = inputs.map(input => input.value);
// ✅ filter() — want a subset
const enabledInputs = inputs.filter(input => !input.disabled);// ❌ filter()[0] instead of find()
const active = items.filter(item => item.classList.contains('active'))[0];
// filter() checks ALL items, then you grab [0]. find() stops at first match.
// ❌ filter().length > 0 instead of some()
const hasErrors = inputs.filter(i => !i.validity.valid).length > 0;
// filter() checks ALL items. some() stops at first match.✅ DO: Check for Existence Before Using
first(), last(), at(), and find() return undefined when nothing is found. Always check before accessing properties.
// ✅ Check first
const first = items.first();
if (first) {
first.focus();
}
// ✅ Optional chaining — clean one-liner
items.first()?.focus();
items.at(-1)?.scrollIntoView({ behavior: 'smooth' });// ❌ Dangerous — may throw TypeError
items.first().focus(); // Error if collection is empty!
items.at(10).textContent; // Error if index is out of range!✅ DO: Store Event Handler References for Cleanup
If you'll ever need to remove an event listener, you must have a reference to the handler function. Anonymous functions can't be removed.
// ✅ Named handler — can remove later
const handleClick = (e) => processClick(e);
buttons.on('click', handleClick);
// Later, when the component is destroyed:
buttons.off('click', handleClick); // Works!// ❌ Anonymous handler — can never be removed
buttons.on('click', (e) => processClick(e));
// buttons.off('click', ???) — no way to reference this function✅ DO: Use isEmpty() as a Guard
Always check before processing or accessing individual elements.
// ✅ Guard pattern — safe and readable
const items = Collections.ClassName.item;
if (items.isEmpty()) {
showEmptyState();
return; // Exit early — nothing to process
}
// Safe to proceed — we know items exist
const first = items.first();
first.classList.add('selected');✅ DO: Clear Cache After Major DOM Changes
The cache auto-updates for small DOM mutations, but after replacing large sections of HTML (SPA navigation, full content reload), clear it manually.
// ✅ Clear after major DOM replacement
async function loadNewPage(route) {
const html = await fetchPageHTML(route);
document.getElementById('main').innerHTML = html;
Collections.clear(); // Cache is now stale — clear it
initializePage(); // Access fresh collections
}// ❌ Forgetting to clear after major DOM changes
async function loadNewPage(route) {
const html = await fetchPageHTML(route);
document.getElementById('main').innerHTML = html;
// Old cached collections may reference removed elements!
initializePage(); // Using stale cache — potential bugs
}❌ DON'T: Modify DOM During forEach
Removing elements from the DOM while iterating can cause skipped or repeated elements because the collection snapshot might not align with live DOM state.
// ❌ Dangerous — removing while iterating
items.forEach(item => {
if (item.classList.contains('expired')) {
item.remove(); // Modifying DOM mid-iteration
}
});
// ✅ Safe — collect first, then remove
const expired = items.filter(item => item.classList.contains('expired'));
expired.forEach(item => item.remove()); // Iteration already done❌ DON'T: Use Collection Methods on Filter/Map Results
visible(), hidden(), enabled(), disabled(), filter(), and map() return plain arrays — not Collections. Collection-specific methods (addClass, setStyle, on) don't exist on plain arrays.
// ❌ Won't work — visible() returns a plain array
const visible = items.visible();
visible.addClass('processed'); // TypeError: visible.addClass is not a function
// ✅ Use array methods on the result
const visible = items.visible();
visible.forEach(item => item.classList.add('processed'));❌ DON'T: Access Collections Inside Tight Loops
Even though collection access is fast (cache hit), accessing inside a hot loop is unnecessary. Access once outside the loop.
// ❌ Accessing inside loop — avoidable overhead
for (let i = 0; i < 1000; i++) {
Collections.ClassName.btn.forEach(btn => update(btn));
}
// ✅ Access once outside the loop
const buttons = Collections.ClassName.btn;
for (let i = 0; i < 1000; i++) {
buttons.forEach(btn => update(btn));
}❌ DON'T: Assume Collections Auto-Update
Collections are static snapshots. Adding elements to the DOM after you've accessed a collection won't appear in that collection. Access again to get fresh results.
// ❌ Wrong assumption
const items = Collections.ClassName.item;
console.log(items.length); // 5
addNewItemToDOM(); // Adds a new .item element
console.log(items.length); // Still 5 — snapshot doesn't update!
// ✅ Access again to get the updated count
const updatedItems = Collections.ClassName.item;
console.log(updatedItems.length); // 6 — fresh snapshotPerformance Optimization
1. Use find() and some() Instead of filter() When Possible
find() and some() stop as soon as they find a match. filter() always processes every element.
// Slower — filter() processes all 100 items
const firstActive = items.filter(item =>
item.classList.contains('active')
)[0];
// Faster — find() stops at the first match
const firstActive = items.find(item =>
item.classList.contains('active')
);
// Slower — filter().length > 0 processes all 100 items
const hasActive = items.filter(item =>
item.classList.contains('active')
).length > 0;
// Faster — some() stops at the first match
const hasActive = items.some(item =>
item.classList.contains('active')
);2. Combine Multiple Conditions in One Filter Pass
When you need multiple conditions, combine them in a single filter() instead of chaining multiple passes.
// Two passes — processes the collection twice
const visible = inputs.visible();
const filledVisible = visible.filter(i => i.value.trim() !== '');
// One pass — processes once, same result
const filledVisible = inputs.filter(input =>
input.offsetParent !== null && // visible
input.value.trim() !== '' // has value
);For small collections (< 100 elements), this difference is negligible. For large collections, the single-pass approach is noticeably faster.
3. Use Bulk Methods Instead of forEach Loops
Built-in bulk methods (addClass, setStyle, setProperty) are optimized and more readable than manual forEach loops.
// Slower (forEach loop)
items.forEach(item => {
item.classList.add('active');
item.style.opacity = '1';
item.disabled = false;
});
// Faster and cleaner (bulk methods)
items.addClass('active')
.setStyle({ opacity: '1' })
.setProperty('disabled', false);4. Monitor Cache Performance in Development
if (process.env.NODE_ENV === 'development') {
Collections.configure({ enableLogging: true });
// Periodically log cache stats
setInterval(() => {
const stats = Collections.stats();
if (stats.hitRate < 0.7) {
console.warn(`Low cache hit rate: ${(stats.hitRate * 100).toFixed(1)}% — check if collections are being stored`);
}
}, 30000);
}Common Mistakes and How to Fix Them
Mistake 1: Forgetting Bracket Notation for Hyphenated Classes
// ❌ Syntax error — hyphens aren't valid in property names
const cards = Collections.ClassName.user-card;
// ✅ Use bracket notation
const cards = Collections.ClassName['user-card'];
// ✅ Or method call
const cards = Collections.ClassName('user-card');Mistake 2: Calling addClass on a Filter Result
const items = Collections.ClassName.item;
// ❌ visible() returns a plain array, not a Collection
const visible = items.visible();
visible.addClass('shown'); // TypeError!
// ✅ Use forEach on the plain array
const visible = items.visible();
visible.forEach(item => item.classList.add('shown'));Mistake 3: Expecting Collection to Update Automatically
const items = Collections.ClassName.item;
console.log(items.length); // 3
// Add new item to DOM
document.querySelector('.container').insertAdjacentHTML('beforeend',
'<div class="item">New</div>'
);
// ❌ Still 3 — snapshot doesn't auto-update
console.log(items.length); // 3
// ✅ Access again to get fresh snapshot
const freshItems = Collections.ClassName.item;
console.log(freshItems.length); // 4Mistake 4: Using Anonymous Handlers When You Need to Remove Them
const cards = Collections.ClassName.card;
// ❌ Anonymous handler — impossible to remove later
cards.on('click', () => selectCard());
// ✅ Named/stored handler — can be removed
const clickHandler = () => selectCard();
cards.on('click', clickHandler);
// When done:
cards.off('click', clickHandler); // Works!Mistake 5: Not Handling the waitFor() Timeout
// ❌ No error handling — if elements don't load, the app breaks
const items = await Collections.waitFor('class', 'product-item');
initializeItems(items);
// ✅ Always handle timeout errors
try {
const items = await Collections.waitFor('class', 'product-item', 1, 8000);
initializeItems(items);
} catch (error) {
console.error('Products did not load:', error.message);
showFallbackContent();
}Production Checklist
Before shipping Collections-based code, verify:
Access Patterns
- [ ] Using the right collection type for each case (ClassName, TagName, Name)
- [ ] Storing collection references in variables when used multiple times
- [ ] Not accessing collections inside tight loops without a stored reference
- [ ] Using bracket notation for hyphenated class names
Array Methods
- [ ] Using
find()instead offilter()[0] - [ ] Using
some()instead offilter().length > 0 - [ ] Not calling
toArray()unnecessarily when array methods already work - [ ] Not modifying DOM inside
forEach— filter first, then modify
DOM Manipulation
- [ ] Related operations chained in one expression
- [ ] Event handler functions stored in variables for later removal
- [ ] Event listeners removed on component cleanup (no memory leaks)
- [ ] Using bulk methods (
addClass,setStyle) instead of manualforEachloops
Filtering Methods
- [ ] Not calling Collection methods (
addClass) on filter results (plain arrays) - [ ] Caching filter results when used multiple times
Utility Methods
- [ ] Checking return value of
first(),last(),at()before using - [ ] Using
isEmpty()as a guard before accessing elements - [ ] Using negative indices with
at()for "from end" access
Helper Methods
- [ ] Calling
Collections.clear()after major DOM replacements - [ ] Handling
waitFor()timeouts with try/catch - [ ] Using
configure()at app startup with environment-appropriate settings - [ ] Not clearing cache unnecessarily
- [ ] Calling
Collections.destroy()on application shutdown
General
- [ ] Not assuming collections auto-update — re-accessing when fresh data is needed
- [ ] Development logging enabled in dev, disabled in production
Real-World Patterns
Pattern 1: Page Initialization
function initializePage() {
// Get all collections needed for this page
const collections = Collections.getMultiple([
{ type: 'class', value: 'btn' },
{ type: 'class', value: 'card' },
{ type: 'class', value: 'nav-link' },
{ type: 'tag', value: 'input' },
{ type: 'tag', value: 'img' }
]);
// Set up navigation
const navLinks = collections['class:nav-link'];
navLinks.on('click', function() {
navLinks.removeClass('active');
this.classList.add('active');
});
// Set up cards
collections['class:card']
.setStyle({ cursor: 'pointer', transition: 'transform 0.2s' })
.on('mouseenter', function() { this.style.transform = 'translateY(-4px)'; })
.on('mouseleave', function() { this.style.transform = 'translateY(0)'; })
.on('click', handleCardClick);
// Set up buttons
collections['class:btn']
.addClass('initialized')
.on('click', handleButtonClick);
// Set up form inputs
collections['tag:input']
.on('input', handleInputChange)
.on('blur', validateInput);
// Optimize images
collections['tag:img']
.setAttribute('loading', 'lazy')
.setAttribute('decoding', 'async');
console.log('Page initialized');
}Pattern 2: Form Handler
class FormHandler {
constructor(formClass) {
// Store collection references for reuse
this.inputs = Collections.ClassName[`${formClass}-input`];
this.selects = Collections.ClassName[`${formClass}-select`];
this.buttons = Collections.ClassName[`${formClass}-btn`];
// Store handler references for cleanup
this.inputHandler = this.handleInput.bind(this);
this.blurHandler = this.handleBlur.bind(this);
// Attach listeners
this.inputs.on('input', this.inputHandler);
this.inputs.on('blur', this.blurHandler);
}
handleInput(e) {
// Clear error on input
e.target.classList.remove('error');
this.updateSubmitButton();
}
handleBlur(e) {
// Validate on blur
if (!e.target.validity.valid) {
e.target.classList.add('error');
}
}
updateSubmitButton() {
// Enable submit only when all enabled inputs are valid
const enabled = this.inputs.enabled();
const allValid = enabled.every(input => input.validity.valid);
this.buttons.setProperty('disabled', !allValid);
}
validate() {
// Validate visible, enabled inputs
const active = this.inputs
.visible()
.filter(input => !input.disabled);
const errors = active.filter(input => !input.validity.valid);
errors.forEach(input => input.classList.add('error'));
return {
valid: errors.length === 0,
errorCount: errors.length
};
}
getData() {
return this.inputs
.enabled()
.reduce((data, input) => {
if (input.name) data[input.name] = input.value;
return data;
}, {});
}
setLoading(loading) {
if (loading) {
this.inputs.setProperty('disabled', true).addClass('loading');
this.buttons.setProperty('disabled', true)
.setProperty('textContent', 'Saving...')
.addClass('loading');
} else {
this.inputs.setProperty('disabled', false).removeClass('loading');
this.buttons.setProperty('disabled', false)
.setProperty('textContent', 'Save')
.removeClass('loading');
}
}
async submit() {
const { valid } = this.validate();
if (!valid) return false;
this.setLoading(true);
try {
await submitData(this.getData());
console.log('Form submitted successfully');
return true;
} catch (error) {
console.error('Submission failed:', error);
return false;
} finally {
this.setLoading(false);
}
}
destroy() {
// Remove listeners to prevent memory leaks
this.inputs.off('input', this.inputHandler);
this.inputs.off('blur', this.blurHandler);
console.log('FormHandler destroyed');
}
}Pattern 3: Dynamic Content Loader
class ContentLoader {
constructor(containerClass, itemClass) {
this.containerClass = containerClass;
this.itemClass = itemClass;
}
async load(fetchFn) {
// Show loading state
const container = Collections.ClassName[this.containerClass];
container.addClass('loading');
try {
// Fetch data and inject HTML
const data = await fetchFn();
const html = this.renderItems(data);
Collections.ClassName[this.containerClass].forEach(el => {
el.innerHTML = html;
});
// Clear cache — DOM changed
Collections.clear();
// Wait for items to appear
const items = await Collections.waitFor('class', this.itemClass, 1, 5000);
// Initialize items
items.forEach((item, index) => {
item.dataset.index = index;
});
items.on('click', this.handleItemClick.bind(this));
// Remove loading state
Collections.ClassName[this.containerClass]
.removeClass('loading')
.addClass('loaded');
console.log(`Loaded ${items.length} items`);
return items;
} catch (error) {
Collections.ClassName[this.containerClass]
.removeClass('loading')
.addClass('error')
.forEach(el => {
el.textContent = 'Failed to load. Please try again.';
});
console.error('Load failed:', error);
throw error;
}
}
renderItems(data) {
return data.map(item =>
`<div class="${this.itemClass}" data-id="${item.id}">
${item.name}
</div>`
).join('');
}
handleItemClick(e) {
console.log('Item clicked:', e.currentTarget.dataset.id);
}
}Quick Reference
// ACCESS
const buttons = Collections.ClassName.btn; // By class
const links = Collections.ClassName['nav-link']; // Hyphenated class
const paras = Collections.TagName.p; // By tag
const radios = Collections.Name.color; // By name
const items = await Collections.waitFor('class', 'item', 1, 5000); // Async
// ARRAY METHODS
buttons.forEach(btn => console.log(btn.textContent)); // Iterate
buttons.map(btn => btn.textContent); // Extract data
buttons.filter(btn => !btn.disabled); // Subset
buttons.find(btn => btn.id === 'submit'); // First match
buttons.findIndex(btn => btn.id === 'submit'); // Position
buttons.some(btn => btn.disabled); // Any match?
buttons.every(btn => !btn.disabled); // All match?
buttons.reduce((sum, btn) => sum + 1, 0); // Aggregate
buttons.toArray(); // Plain array
// BULK DOM METHODS (all chainable)
buttons.addClass('active')
.removeClass('inactive')
.toggleClass('selected')
.setProperty('disabled', false)
.setAttribute('aria-label', 'Action')
.setStyle({ backgroundColor: '#2563eb', color: 'white' })
.on('click', handleClick)
.off('click', oldHandler);
// FILTERING (return plain arrays)
items.visible(); // Visible elements
items.hidden(); // Hidden elements
items.enabled(); // Enabled form elements
items.disabled(); // Disabled form elements
// UTILITY
items.first(); // First element (or undefined)
items.last(); // Last element (or undefined)
items.at(2); // Third element (0-based)
items.at(-1); // Last (negative index)
items.at(-2); // Second to last
items.isEmpty(); // true if 0 elements
// HELPER METHODS
Collections.stats(); // Cache stats
Collections.clear(); // Clear cache
Collections.destroy(); // Full cleanup
Collections.isCached('class', 'btn'); // Check cache
Collections.getMultiple([ // Multiple at once
{ type: 'class', value: 'btn' },
{ type: 'tag', value: 'input' }
]);
Collections.configure({ enableLogging: true }); // ConfigurationSummary — You've Mastered Collections
You now understand the complete Collections system:
✅ Three access methods — ClassName, TagName, Name — each for a different HTML grouping ✅ Array-like methods — forEach, map, filter, find, some, every, reduce, toArray ✅ Bulk DOM methods — addClass, setStyle, setProperty, on, off — all chainable ✅ Filtering methods — visible(), hidden(), enabled(), disabled() ✅ Utility methods — first(), last(), at(), isEmpty() ✅ Helper methods — stats(), clear(), destroy(), waitFor(), getMultiple(), configure() ✅ Best practices — decision trees, performance tips, common mistakes, production checklist
The key mental model: Every Collections access gives you an Enhanced Collection — something that behaves like an array, has all array methods built in, supports bulk DOM operations, chains naturally, and is automatically cached. Work with groups of elements the same way you'd work with arrays, and let the library handle the performance optimizations.
End of Collections Access Methods Documentation