Skip to content

Sponsor

Sponsor

Best Practices — Mastering Collections

Quick Start (30 Seconds)

Before writing any Collections code, ask yourself two questions:

  1. Do I need one specific element? → Use the Elements Helper (Elements.myId)
  2. Do I need a group of elements? → Use Collections — then ask which type fits the HTML
javascript
// 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

ScenarioUseExample
All buttons with class btnCollections.ClassNameCollections.ClassName.btn
All <p> tagsCollections.TagNameCollections.TagName.p
All radio buttons in groupCollections.NameCollections.Name.color
Element loaded via AJAXCollections.waitFor()await Collections.waitFor('class', 'item')
Single element by IDElements HelperElements.submitButton

Array Methods

GoalUseWhy
Do something with each elementforEachSide effects on DOM
Extract data from elementsmapGet text, attributes, etc.
Get a subsetfilterConditional selection
Find the first matchfindStops at first hit
Check if any matchsomeShort-circuits — efficient
Check if all matcheveryShort-circuits — efficient
Combine into one valuereduceSum, group, build objects
Need plain array for sort/splicetoArrayWhen you need native Array

DOM Manipulation Methods

GoalUseChainable
Add CSS class to alladdClass
Remove CSS class from allremoveClass
Toggle CSS class on alltoggleClass
Set JS property on allsetProperty
Set HTML attribute on allsetAttribute
Set CSS styles on allsetStyle
Add event listener to allon
Remove event listener from alloff

Filtering Methods

GoalUseReturns
Only visible elementsvisible()Plain array
Only hidden elementshidden()Plain array
Only enabled form elementsenabled()Plain array
Only disabled form elementsdisabled()Plain array

Utility Methods

GoalUseReturns
First elementfirst()Element or undefined
Last elementlast()Element or undefined
Element at positionat(index)Element or undefined
Check if collection is emptyisEmpty()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.

javascript
// ✅ Good — access once, use many times
const buttons = Collections.ClassName.btn;
buttons.addClass('initialized');
buttons.on('click', handleClick);
buttons.setStyle({ cursor: 'pointer' });
javascript
// ❌ 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' });

When multiple operations target the same collection, chain them. It's shorter and makes the relationship between operations obvious.

javascript
// ✅ 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);
javascript
// ❌ 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.

javascript
// ✅ 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);
javascript
// ❌ 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.

javascript
// ✅ Check first
const first = items.first();
if (first) {
  first.focus();
}

// ✅ Optional chaining — clean one-liner
items.first()?.focus();
items.at(-1)?.scrollIntoView({ behavior: 'smooth' });
javascript
// ❌ 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.

javascript
// ✅ 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!
javascript
// ❌ 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.

javascript
// ✅ 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.

javascript
// ✅ 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
}
javascript
// ❌ 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.

javascript
// ❌ 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.

javascript
// ❌ 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.

javascript
// ❌ 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.

javascript
// ❌ 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 snapshot

Performance 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.

javascript
// 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.

javascript
// 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.

javascript
// 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

javascript
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

javascript
// ❌ 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

javascript
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

javascript
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); // 4

Mistake 4: Using Anonymous Handlers When You Need to Remove Them

javascript
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

javascript
// ❌ 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 of filter()[0]
  • [ ] Using some() instead of filter().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 manual forEach loops

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

javascript
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

javascript
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

javascript
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

javascript
// 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 });   // Configuration

Summary — You've Mastered Collections

You now understand the complete Collections system:

Three access methodsClassName, TagName, Name — each for a different HTML grouping ✅ Array-like methodsforEach, map, filter, find, some, every, reduce, toArrayBulk DOM methodsaddClass, setStyle, setProperty, on, off — all chainable ✅ Filtering methodsvisible(), hidden(), enabled(), disabled()Utility methodsfirst(), last(), at(), isEmpty()Helper methodsstats(), 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