Property Handlers
Quick Start (30 seconds)
Conditions.whenState(
() => app.status,
{
'active': {
'#card': {
textContent: 'Active', // text
className: 'card card-active', // class string
classList: { add: 'highlight', remove: 'dim' }, // class operations
style: { color: 'green', fontWeight: 'bold' }, // CSS styles
setAttribute: { 'data-state': 'active', 'aria-pressed': 'true' },
hidden: false // direct property
}
},
'inactive': {
'#card': {
textContent: 'Inactive',
className: 'card card-inactive',
classList: { remove: 'highlight', add: 'dim' },
style: { color: 'gray', fontWeight: 'normal' },
setAttribute: { 'data-state': 'inactive', 'aria-pressed': 'false' },
hidden: false
}
}
}
);One update object. Every kind of DOM change.
What Are Property Handlers?
Inside each condition block, you write update objects for each selector:
{
'loading': {
'#panel': { hidden: false, textContent: 'Loading…', className: 'panel loading' }
}
}The keys inside that update object — hidden, textContent, className — are processed by property handlers. Just as condition matchers decide how to interpret condition keys, property handlers decide how to apply each update property to a DOM element.
The system checks each property key against a registry of handlers:
Property key: 'style'
→ Is it 'style'? YES → handle as CSS properties object
Property key: 'classList'
→ Is it 'classList'? YES → handle as class operations (add/remove/toggle)
Property key: 'hidden'
→ Not a special key
→ Is it an event name? No
→ Is it a known native property? YES → assign directly
Property key: 'customProp'
→ None of the above → fallback → direct property assignmentThis is what makes the update object so expressive — one uniform syntax handles everything from text to styles to events to arbitrary properties.
All Built-In Property Handlers
Handler Priority Order
1. style — key === 'style'
2. classList — key === 'classList'
3. setAttribute — key === 'setAttribute'
4. removeAttribute — key === 'removeAttribute'
5. dataset — key === 'dataset'
6. addEventListener — key === 'addEventListener'
7. removeEventListener — key === 'removeEventListener'
8. eventProperty — key starts with 'on' (e.g., 'onclick', 'onchange')
9. nativeProperty — key is a known DOM property
10. fallback — anything else → direct assignment1. style — Inline CSS Properties
Applies individual CSS properties to the element. Only changed properties are updated.
// Set multiple CSS properties at once
{ style: { color: 'red', backgroundColor: '#fff', fontSize: '16px' } }
// Use camelCase for CSS properties
{ style: { fontWeight: 'bold', borderRadius: '8px', zIndex: '100' } }
// Remove a property by setting it to empty string or null
{ style: { display: '', color: null } }In a whenState() context:
Conditions.whenState(
() => alert.type,
{
'info': {
'#alert-box': {
style: { backgroundColor: '#e3f2fd', borderColor: '#1976d2', color: '#0d47a1' }
}
},
'error': {
'#alert-box': {
style: { backgroundColor: '#ffebee', borderColor: '#d32f2f', color: '#b71c1c' }
}
},
'success': {
'#alert-box': {
style: { backgroundColor: '#e8f5e9', borderColor: '#388e3c', color: '#1b5e20' }
}
}
}
);Key details:
- Values are CSS strings (
'16px','bold','#fff') - Property names are camelCase (
backgroundColor, notbackground-color) - Only changed properties are touched — other style properties on the element remain
2. classList — Class Operations
Provides fine-grained control over CSS classes without replacing the entire className string.
// Add one or more classes
{ classList: { add: 'active' } }
{ classList: { add: ['active', 'highlighted'] } }
// Remove one or more classes
{ classList: { remove: 'inactive' } }
{ classList: { remove: ['inactive', 'dim', 'hidden'] } }
// Toggle with a condition
{ classList: { toggle: ['visible', isVisible] } }
// → adds 'visible' if isVisible is true, removes it if false
// Replace one class with another
{ classList: { replace: ['old-class', 'new-class'] } }
// Multiple operations together
{
classList: {
add: 'active',
remove: ['inactive', 'loading'],
toggle: ['highlighted', someCondition]
}
}In a whenState() context:
Conditions.whenState(
() => tab.active,
{
'home': { '.nav-link': { classList: { remove: 'active' } }, '#nav-home': { classList: { add: 'active' } } },
'profile': { '.nav-link': { classList: { remove: 'active' } }, '#nav-profile': { classList: { add: 'active' } } },
'settings': { '.nav-link': { classList: { remove: 'active' } }, '#nav-settings': { classList: { add: 'active' } } }
}
);Key details:
addandremoveaccept a string or array of stringstoggle: [className, condition]— the second element is the boolean conditionreplace: [from, to]— replacesfromwithtoiffromexists- Unlike
className, this does not replace the full class list — it surgically adds/removes
3. setAttribute — HTML Attributes
Sets one or more HTML attributes on the element.
// Set one attribute
{ setAttribute: { 'aria-label': 'Close dialog' } }
// Set multiple attributes
{
setAttribute: {
'role': 'alert',
'aria-live': 'polite',
'aria-atomic': 'true',
'data-id': '42',
'tabindex': '0'
}
}In a whenState() context:
Conditions.whenState(
() => modal.isOpen,
{
'true': {
'#modal': {
setAttribute: { 'aria-hidden': 'false', 'aria-modal': 'true' },
hidden: false
},
'#overlay': { hidden: false }
},
'false': {
'#modal': {
setAttribute: { 'aria-hidden': 'true' },
hidden: true
},
'#overlay': { hidden: true }
}
}
);Key details:
- All attribute values are strings (as per the HTML spec)
- Use
setAttributefor ARIA attributes,data-*attributes, and any non-property attributes - For boolean attributes like
disabled, prefer direct property assignment ({ disabled: true }) oversetAttribute
4. removeAttribute — Remove HTML Attributes
Removes one or more HTML attributes from the element.
// Remove one attribute
{ removeAttribute: 'disabled' }
// Remove multiple attributes
{ removeAttribute: ['disabled', 'readonly', 'aria-invalid'] }In a whenState() context:
Conditions.whenState(
() => input.hasError,
{
'true': {
'#email-input': {
setAttribute: { 'aria-invalid': 'true', 'aria-describedby': 'email-error' },
classList: { add: 'input-error' }
}
},
'false': {
'#email-input': {
removeAttribute: ['aria-invalid', 'aria-describedby'],
classList: { remove: 'input-error' }
}
}
}
);Key details:
- Accepts a string (single attribute) or array of strings (multiple)
- Removing a non-existent attribute is safe — no error thrown
5. dataset — Data Attributes
Sets data-* attributes through the element's dataset property. Values are assigned as element.dataset.key = value.
// Set data attributes
{ dataset: { userId: '42', role: 'admin', lastSeen: '2024-01-01' } }
// → sets data-user-id="42", data-role="admin", data-last-seen="2024-01-01"Key naming: Use camelCase in the object — the browser automatically converts to kebab-case in the HTML.
{ dataset: { itemId: '5' } }
// → element.dataset.itemId = '5'
// → HTML: data-item-id="5"In a whenState() context:
Conditions.whenState(
() => user.role,
{
'admin': {
'#user-card': {
dataset: { role: 'admin', accessLevel: '3' },
classList: { add: 'admin-highlight' }
}
},
'user': {
'#user-card': {
dataset: { role: 'user', accessLevel: '1' },
classList: { remove: 'admin-highlight' }
}
}
}
);6. addEventListener — Attach Event Listeners
Adds event listeners to the element. Duplicate prevention is automatic — the same function will not be added twice even if the condition re-applies.
// Attach one listener
{ addEventListener: { click: handleClick } }
// Attach multiple listeners
{
addEventListener: {
click: handleClick,
mouseover: handleHover,
focus: handleFocus
}
}In a whenState() context:
Conditions.whenState(
() => form.mode,
{
'editing': {
'#save-btn': {
addEventListener: { click: saveChanges },
disabled: false
},
'#cancel-btn': {
addEventListener: { click: cancelEditing },
disabled: false
}
},
'viewing': {
'#save-btn': { disabled: true },
'#cancel-btn': { disabled: true }
}
}
);Key details:
- The system tracks which
(element, event, handler)triples have been registered — the same handler is never registered twice - Listeners added via this method persist across condition changes (they're not removed when the condition changes unless you use
removeEventListener) - For listeners that should be conditionally active, use
removeEventListenerin the other state
7. removeEventListener — Remove Event Listeners
Removes event listeners previously added to the element.
{ removeEventListener: { click: handleClick } }
{ removeEventListener: { click: handleClick, mouseover: handleHover } }In a whenState() context — toggling interactivity:
Conditions.whenState(
() => overlay.blocking,
{
'true': {
'#action-btn': {
removeEventListener: { click: handleAction },
classList: { add: 'btn-disabled' }
}
},
'false': {
'#action-btn': {
addEventListener: { click: handleAction },
classList: { remove: 'btn-disabled' }
}
}
}
);8. eventProperty — Inline Event Handlers (on* properties)
Handles properties that start with on — the inline event handler API (onclick, onchange, oninput, etc.).
{ onclick: handleClick }
{ onchange: handleChange }
{ oninput: handleInput }
{ onfocus: handleFocus }
{ onblur: handleBlur }These are set directly as element properties: element.onclick = handler.
In a whenState() context:
Conditions.whenState(
() => widget.mode,
{
'interactive': {
'#widget': { onclick: handleWidgetClick, onkeydown: handleKeyDown }
},
'readonly': {
'#widget': { onclick: null, onkeydown: null }
}
}
);Key details:
- These are element properties (not addEventListener) — setting to
nullremoves the handler - Only one handler per event type (unlike
addEventListenerwhich allows multiple) - Prefer
addEventListenerfor most event handling; useon*properties when you specifically need the single-handler behavior
9. nativeProperty — Direct DOM Properties
Handles recognized native DOM properties directly — properties that exist directly on the element object rather than as HTML attributes.
Common native properties handled this way:
// Boolean properties
{ disabled: true }
{ hidden: false }
{ checked: true }
{ readOnly: true }
{ required: true }
{ multiple: true }
// String properties
{ textContent: 'Hello World' }
{ innerText: 'Formatted Text' }
{ innerHTML: '<strong>Bold</strong>' }
{ value: 'input text' }
{ placeholder: 'Enter text…' }
{ src: '/images/photo.jpg' }
{ href: '/page' }
{ alt: 'Description' }
{ className: 'card active' }
{ id: 'my-id' }
{ title: 'Tooltip text' }
// Numeric properties
{ tabIndex: 0 }
{ maxLength: 100 }
{ rows: 5 }
{ cols: 40 }
{ min: 0 }
{ max: 100 }
{ step: 1 }In a whenState() context:
Conditions.whenState(
() => step.current,
{
'1': {
'#next-btn': { disabled: false, textContent: 'Next: Details' },
'#back-btn': { disabled: true },
'#progress': { value: 1, max: 3 }
},
'2': {
'#next-btn': { disabled: false, textContent: 'Next: Review' },
'#back-btn': { disabled: false },
'#progress': { value: 2, max: 3 }
},
'3': {
'#next-btn': { disabled: false, textContent: 'Submit' },
'#back-btn': { disabled: false },
'#progress': { value: 3, max: 3 }
}
}
);Key details:
- These are compared against their previous values — only set if the value actually changed
- For
textContent,innerHTML, and similar, change detection prevents unnecessary DOM reflows
10. fallback — Arbitrary Property Assignment
If none of the above handlers claim a property key, the fallback handler assigns it directly as an element property.
// Any property not recognized by other handlers
{ customProperty: 'value' }
{ scrollTop: 0 }
{ selectionStart: 0 }
{ indeterminate: true } // checkbox indeterminate stateThe fallback uses direct assignment: element[key] = value.
Using Multiple Handlers Together
The real power is combining handlers in one update object:
Conditions.whenState(
() => notification.type,
{
'success': {
'#toast': {
textContent: 'Saved successfully!', // nativeProperty
className: 'toast toast-success', // nativeProperty
setAttribute: { role: 'status' }, // setAttribute
style: { borderLeftColor: '#4caf50' }, // style
hidden: false // nativeProperty
}
},
'error': {
'#toast': {
textContent: 'Something went wrong.', // nativeProperty
className: 'toast toast-error', // nativeProperty
setAttribute: { role: 'alert', // setAttribute
'aria-live': 'assertive' },
style: { borderLeftColor: '#f44336' }, // style
hidden: false // nativeProperty
}
},
'idle': {
'#toast': {
hidden: true // nativeProperty
}
}
}
);One block handles text, class, ARIA, style, and visibility — all at once, all correctly.
Complete Handler Reference
// style
{ style: { color: 'red', fontSize: '16px' } }
// classList
{ classList: { add: 'cls', remove: 'old', toggle: ['active', bool], replace: ['a', 'b'] } }
// setAttribute
{ setAttribute: { 'aria-label': 'Close', role: 'button' } }
// removeAttribute
{ removeAttribute: ['disabled', 'aria-invalid'] }
// dataset
{ dataset: { userId: '5', category: 'admin' } }
// addEventListener
{ addEventListener: { click: fn, focus: fn2 } }
// removeEventListener
{ removeEventListener: { click: fn } }
// eventProperty (inline handlers)
{ onclick: handleClick }
{ onchange: null } // remove by setting null
// nativeProperty (direct assignment with change detection)
{ textContent: 'text' }
{ innerHTML: '<b>bold</b>' }
{ hidden: true }
{ disabled: false }
{ checked: true }
{ value: 'input text' }
{ className: 'card active' }
{ src: '/img.jpg' }
// fallback (anything else)
{ indeterminate: true }
{ scrollTop: 0 }What Happens When a Property Is Unknown
If you write a property that no handler recognizes and the fallback doesn't produce a sensible result, the assignment simply sets element.customKey = value. This is safe — browsers ignore unknown properties gracefully. No error is thrown.
Summary
- Property handlers are the bridge between the update objects in your conditions and the actual DOM API calls
- 10 built-in handlers cover every common DOM update type: CSS styles, class operations, HTML attributes, data attributes, event listeners, inline event properties, and direct element properties
stylehandles granular CSS property updates — only changed properties are touchedclassListenables surgical class operations without replacing the full class stringsetAttribute/removeAttributemanage HTML attribute presencedatasetsetsdata-*attributes through thedatasetAPIaddEventListener/removeEventListenermanage event listener registration with automatic duplicate preventionnativePropertyhandles direct element properties (disabled,hidden,textContent,value, etc.) with change detectionfallbackcatches anything unrecognized via direct property assignment- Handlers can be freely combined in a single update object — one block can set text, style, class, aria, and events all at once
Continue to: 05 — apply() and watch()