Reactive With Enhancers
Quick Demonstration
const leaderboard = state({
players: ['Alice', 'Bob', 'Charlie'],
scores: [2400, 1850, 1200]
});
effect(() => {
// Array distribution: each player gets their name, each gets their score
ClassName.player.update({
textContent: leaderboard.players // ['Alice', 'Bob', 'Charlie'] distributed across .player elements
});
ClassName.score.update({
textContent: leaderboard.scores // [2400, 1850, 1200] distributed across .score elements
});
});
// Update player scores — all three score elements update automatically
leaderboard.scores = [2500, 1900, 1300];One state change. Three elements updated. Each gets the right value from the array.
What Are Enhancers?
Enhancers are modules that extend the DOM Helpers core with more powerful update patterns. They live in src/02_enhancers/ and add capabilities on top of the .update() foundation.
The key enhancer features are:
1. Bulk Property Updaters
Update multiple elements by ID using a single, expressive call.
// Instead of: 3 separate getElementById + update calls
Elements.textContent({
'user-name': app.name,
'user-score': app.score,
'user-rank': app.rank
});2. Array Distribution Across Collections
Distribute an array of values across matching elements — each element gets its own value from the array.
// ClassName.card represents all elements with class="card"
// textContent array is distributed: card[0] gets titles[0], card[1] gets titles[1], etc.
ClassName.card.update({
textContent: ['Product A', 'Product B', 'Product C']
});3. Index-Based Collection Updates
Target specific elements within a collection by index — including negative indices.
// Update only the first and last elements
ClassName.item.update({
[0]: { classList: { add: 'first' } },
[-1]: { classList: { add: 'last' } },
classList: { remove: 'placeholder' } // Applied to ALL
});4. Global Collection Shortcuts
Access collections of elements by class, tag, or name using clean shorthand syntax.
ClassName.button[0] // First element with class="button"
TagName.li[-1] // Last <li> element
Name.username[0] // First element with name="username"
Id('myButton') // Element with id="myButton"Syntax
Global Collection Shortcuts
// Access by class name
ClassName.card // All elements with class="card"
ClassName.card[0] // First .card element
ClassName.card[-1] // Last .card element (negative index)
// Access by tag name
TagName.li // All <li> elements
TagName.li[2] // Third <li>
// Access by input name attribute
Name.email // All elements with name="email"
// Element by ID (shorthand)
Id('myButton') // Same as document.getElementById('myButton')
Id.multiple('a', 'b', 'c') // Get several elementsBulk Property Methods on Elements
// Set textContent for multiple elements by ID
Elements.textContent({ id1: 'text', id2: 'text' });
// Set innerHTML
Elements.innerHTML({ id1: '<b>Bold</b>', id2: 'plain' });
// Set style objects per element
Elements.style({
id1: { color: 'red', fontWeight: 'bold' },
id2: { color: 'blue' }
});
// Set classList operations per element
Elements.classes({
id1: { add: 'active', remove: 'inactive' },
id2: { toggle: ['selected', true] }
});
// Set attributes per element
Elements.attrs({
id1: { 'aria-label': 'Main button', disabled: false },
id2: { 'data-id': '42' }
});
// Set dataset per element
Elements.dataset({
id1: { category: 'featured', rank: '1' },
id2: { category: 'regular', rank: '2' }
});
// Generic property
Elements.prop('disabled', {
'submit-btn': true,
'cancel-btn': false
});Array Distribution on Collections
// Array values are distributed across elements in order
ClassName.item.update({
textContent: ['First', 'Second', 'Third'], // Each element gets its value
style: { color: ['red', 'blue', 'green'] } // Each element gets its color
});
// Beyond array length: last value repeats
// If there are 5 elements but only 3 values:
// element[0] → 'First', element[1] → 'Second', element[2+] → 'Third'Index-Based Updates on Collections
// Numeric keys target specific elements; string keys are bulk
collection.update({
[0]: { textContent: 'First item only' },
[1]: { style: { color: 'blue' } },
[-1]: { classList: { add: 'last-item' } }, // Last element
classList: { remove: 'loading' } // Applied to ALL
});Why Do Enhancers Exist?
The Problem: Verbose Collection Updates
Without enhancers, updating multiple elements in a collection requires manual loops:
// Old way — verbose and error-prone
effect(() => {
const titles = ['Alice', 'Bob', 'Charlie'];
const cards = document.querySelectorAll('.player-card');
// Loop manually
cards.forEach((card, index) => {
const nameEl = card.querySelector('.player-name');
if (nameEl) {
nameEl.textContent = titles[index] || titles[titles.length - 1];
}
});
});The Enhancer Solution: Array Distribution
// Enhancer way — array distribution does the work
effect(() => {
ClassName['player-name'].update({
textContent: leaderboard.players // Array distributed automatically
});
});Same result. No loop. The enhancer handles distribution.
The Problem: Repetitive Bulk ID Updates
// Old way — one call per element
effect(() => {
Id('user-name').update({ textContent: user.name });
Id('user-email').update({ textContent: user.email });
Id('user-role').update({ textContent: user.role });
Id('user-bio').update({ textContent: user.bio });
Id('user-joined').update({ textContent: user.joinDate });
});The Enhancer Solution: Bulk Update
// Enhancer way — one organized block
effect(() => {
Elements.textContent({
'user-name': user.name,
'user-email': user.email,
'user-role': user.role,
'user-bio': user.bio,
'user-joined': user.joinDate
});
});One structured block. Easy to read. Easy to extend.
Mental Model
Array Distribution: A Postal Route
Think of array distribution like a postal worker delivering mail:
.player-name elements: [Card 1] [Card 2] [Card 3] [Card 4]
↓ ↓ ↓ ↓
players array: 'Alice' 'Bob' 'Charlie' 'Charlie'
↑
(repeats last value
when array runs out)Each element gets its value from the matching array position. When the array is shorter than the element count, the last value is used for all remaining elements.
Index-Based Updates: Targeted Mail
.nav-link elements: [link 0] [link 1] [link 2] [link 3]
↓
Index update [1]: skip update skip skip
↓
Bulk update: update update update update
↓
Result: link 1 gets BOTH updates applied (index overrides bulk)Index-based updates target specific elements. Bulk updates apply to all. When both are present, the bulk runs first, then indexes override.
How It Works Internally
Array Distribution Flow
collection.update({ textContent: ['A', 'B', 'C'] })
↓
1. Detect: textContent is an array — use array distribution
↓
2. For element 0: get 'A' (array[0])
For element 1: get 'B' (array[1])
For element 2: get 'C' (array[2])
For element 3+: get 'C' (last value, repeated)
↓
3. Apply each value to corresponding elementIndex-Based Update Flow
collection.update({ [0]: {...}, [1]: {...}, classList: {...} })
↓
1. Separate: numeric keys → indexUpdates, string keys → bulkUpdates
↓
2. Phase 1: Apply bulkUpdates to ALL elements
↓
3. Phase 2: Apply indexUpdates to specific elements
(Overrides bulk where index was specified)
↓
4. Support negative indices: [-1] → elements.length - 1Basic Usage: Enhancers + Reactive
Example 1 — Scores Leaderboard
<ol id="leaderboard">
<li class="player-row">
<span class="player-name"></span>
<span class="player-score"></span>
</li>
<!-- More rows... -->
</ol>const leaderboard = state({
players: [
{ name: 'Alice', score: 2400 },
{ name: 'Bob', score: 1850 },
{ name: 'Charlie', score: 1200 }
]
});
effect(() => {
const names = leaderboard.players.map(p => p.name);
const scores = leaderboard.players.map(p => p.score.toLocaleString());
// Distribute names and scores across elements
ClassName['player-name'].update({ textContent: names });
ClassName['player-score'].update({ textContent: scores });
});
// Update a score — display updates automatically
function updateScore(index, newScore) {
set(leaderboard, {
players: prev => prev.map((p, i) =>
i === index ? { ...p, score: newScore } : p
)
});
}Example 2 — User Profile With Bulk Updates
const user = state({
name: '',
email: '',
bio: '',
role: '',
avatar: '',
joinDate: '',
isVerified: false,
isAdmin: false
});
effect(() => {
// Bulk text update — all at once
Elements.textContent({
'profile-name': user.name,
'profile-email': user.email,
'profile-bio': user.bio,
'profile-role': user.role,
'profile-joined': user.joinDate
});
// Avatar
Id('profile-avatar').update({
src: user.avatar,
alt: `${user.name}'s profile photo`
});
// Bulk class update — verified badge, admin badge
Elements.classes({
'verified-badge': { toggle: ['visible', user.isVerified] },
'admin-badge': { toggle: ['visible', user.isAdmin] },
'profile-name': { add: user.isAdmin ? 'admin-name' : '', remove: user.isAdmin ? '' : 'admin-name' }
});
});Example 3 — Navigation With Active State
const router = state({ currentPage: 'home' });
const pages = ['home', 'about', 'products', 'contact'];
effect(() => {
// For each nav link: is it the current page?
const activeStates = pages.map(page => ({
classList: {
toggle: ['active', page === router.currentPage]
},
setAttribute: {
'aria-current': page === router.currentPage ? 'page' : 'false'
}
}));
// Apply per-element updates using index-based approach
const navLinks = document.querySelectorAll('.nav-link');
navLinks.update(
activeStates.reduce((acc, update, i) => {
acc[i] = update;
return acc;
}, {})
);
});
// Click any nav link
document.querySelectorAll('.nav-link').forEach((link, index) => {
link.addEventListener('click', (e) => {
e.preventDefault();
router.currentPage = pages[index];
});
});Example 4 — Product Grid
<div id="product-grid">
<div class="product-card">
<img class="product-img" src="" alt="">
<h3 class="product-title"></h3>
<p class="product-price"></p>
<span class="product-tag"></span>
</div>
<!-- More cards... -->
</div>const catalog = state({ products: [] });
effect(() => {
const { products } = catalog;
if (!products.length) return;
// Distribute each data field across all product card elements
ClassName['product-title'].update({
textContent: products.map(p => p.name)
});
ClassName['product-price'].update({
textContent: products.map(p => `$${p.price.toFixed(2)}`)
});
ClassName['product-tag'].update({
textContent: products.map(p => p.tag || ''),
hidden: products.map(p => !p.tag)
});
// Update images with both src and alt per element
const images = document.querySelectorAll('.product-img');
images.update({
src: products.map(p => p.image),
alt: products.map(p => `Photo of ${p.name}`)
});
});
// Load products
async function loadProducts() {
const data = await fetch('/api/products').then(r => r.json());
catalog.products = data;
}Example 5 — Form with Index-Based Validation
const form = state({
fields: ['', '', ''], // 3 input fields
errors: ['', '', ''], // One error per field
labels: ['Name', 'Email', 'Message']
});
effect(() => {
// Set labels on all label elements
ClassName['field-label'].update({
textContent: form.labels // Array distributed
});
// Set error messages — each field gets its own error
ClassName['field-error'].update({
textContent: form.errors,
hidden: form.errors.map(e => !e) // Hidden if empty
});
// Highlight invalid fields
ClassName['field-input'].update({
classList: {
// Can't use array in classList toggle directly — use index updates
}
});
// Use index-based updates for per-field class toggling
const inputs = document.querySelectorAll('.field-input');
inputs.update(
form.errors.reduce((updates, error, i) => {
updates[i] = { classList: { toggle: ['invalid', !!error] } };
return updates;
}, {})
);
});Deep Dive: First, Last, and Middle Elements
Targeting Specific Collection Elements
// Style the first and last items specially — leave middle untouched
ClassName.item.update({
style: { padding: '10px' }, // ALL items
[0]: { // First item only
style: { borderRadius: '8px 8px 0 0', marginTop: '0' },
classList: { add: 'first-item' }
},
[-1]: { // Last item only
style: { borderRadius: '0 0 8px 8px', marginBottom: '0' },
classList: { add: 'last-item' }
}
});Reacting to Collection Size Changes
const list = state({ items: [] });
effect(() => {
const { items } = list;
if (items.length === 0) {
Elements.update({
empty-state: { hidden: false },
list-container: { hidden: true }
});
return;
}
Elements.update({
empty-state: { hidden: true },
list-container: { hidden: false }
});
// Re-render list items
Id('list-container').update({ innerHTML: items.map(item => `<div class="list-item">${item.text}</div>`).join('') });
// After re-render, style first and last
ClassName['list-item'].update({
[0]: { classList: { add: 'first' } },
[-1]: { classList: { add: 'last' } }
});
});Deep Dive: Mixed Updates (Bulk + Index)
The real power comes from combining bulk and index updates in one call:
const quiz = state({
questions: [
{ text: 'What is 2+2?', answered: false, correct: null },
{ text: 'Capital of France?', answered: true, correct: true },
{ text: 'Speed of light?', answered: true, correct: false }
]
});
effect(() => {
const { questions } = quiz;
// BULK: Set question text for all
ClassName['question-text'].update({
textContent: questions.map(q => q.text)
});
// BULK + INDEX combined: base classes for all, special for answered ones
const questionCards = document.querySelectorAll('.question-card');
const updates = {
// Bulk: remove all status classes first
classList: { remove: ['answered-correct', 'answered-wrong', 'unanswered'] }
};
// Add index-based updates for answered questions
questions.forEach((q, i) => {
if (q.answered) {
updates[i] = {
classList: {
add: q.correct ? 'answered-correct' : 'answered-wrong'
}
};
} else {
updates[i] = { classList: { add: 'unanswered' } };
}
});
questionCards.update(updates);
});Summary
The enhancer modules add three major capabilities to Reactive + Core:
Bulk Property Methods (Elements.*)
Elements.textContent({ id1: val1, id2: val2 }); // Many IDs, one block
Elements.style({ id1: {...}, id2: {...} });
Elements.classes({ id1: {...}, id2: {...} });Best for: Updating many elements by ID in effects — replaces multiple getElementById calls.
Array Distribution (on collections)
ClassName.card.update({
textContent: ['Value A', 'Value B', 'Value C'] // Each card gets its value
});Best for: Distributing reactive state arrays across multiple DOM elements without loops.
Index-Based Updates (on collections)
collection.update({
classList: { remove: 'loading' }, // ALL elements
[0]: { classList: { add: 'first' } }, // First only
[-1]: { classList: { add: 'last' } } // Last only
});Best for: Applying different updates to specific positions in a collection while also setting a base state for all elements.
Global Shortcuts
ClassName.button[0] // First .button element
TagName.li[-1] // Last <li>
Id('submit-btn') // Element by IDBest for: Cleaner element access syntax without verbose document.getElementsByClassName calls.
What's Next?
Enhancers make updating the DOM expressive and efficient. The final piece is Conditions — a system for declaratively rendering different DOM states based on reactive state values.
Continue to: 13 — Reactive With Conditions