Event Modifiers & Patterns
Event modifiers control event behavior declaratively in HTML: debounce, throttle, key filtering, click-outside detection, and more. This page also covers custom DOM events and form event handling patterns.
Event Modifiers
WildflowerJS provides event modifiers to control event behavior declaratively in your HTML:
preventDefault(), debouncing, and key filtering without writing boilerplate JavaScript code.
Basic Modifiers
| Modifier | Effect | Example |
|---|---|---|
data-event-prevent |
Calls event.preventDefault() |
<form data-action="submit:save" data-event-prevent> |
data-event-stop |
Calls event.stopPropagation() |
<button data-action="handleClick" data-event-stop> |
data-event-once |
Handler fires only once, then auto-removes | <button data-action="initialize" data-event-once> |
data-event-self |
Only fires if event.target is the element itself (not bubbled from children) |
<div data-action="closePanel" data-event-self> |
data-event-capture |
Uses capture phase instead of bubble phase | <div data-action="intercept" data-event-capture> |
data-event-passive |
Marks listener as passive (improves scroll performance) | <div data-action="scroll:track" data-event-passive> |
Advanced Modifiers
| Modifier | Effect | Example |
|---|---|---|
data-event-outside |
Fires when clicking outside the element (great for dropdowns, modals) | <div data-action="closeDropdown" data-event-outside> |
data-event-if="condition" |
Only fires if condition evaluates to true | <button data-action="save" data-event-if="isValid"> |
data-event-delay="ms" |
Delays handler execution by specified milliseconds | <button data-action="save" data-event-delay="100"> |
data-event-outside modifier is especially useful for closing dropdowns, modals, and popovers when users click elsewhere. No JavaScript required, just add the attribute!
Timing Modifiers
Control how frequently event handlers are called:
| Modifier | Effect | Example |
|---|---|---|
data-event-debounce="ms" |
Delays handler until input stops for specified ms | <input data-action="input:search" data-event-debounce="300"> |
data-event-throttle="ms" |
Limits handler to once per specified ms | <div data-action="scroll:handleScroll" data-event-throttle="200"> |
Keyboard Modifiers
Filter keyboard events by specific keys:
| Modifier | Effect | Example |
|---|---|---|
data-event-key-enter |
Only triggers on Enter key | <input data-action="keyup:submit" data-event-key-enter> |
data-event-key-escape |
Only triggers on Escape key | <input data-action="keyup:cancel" data-event-key-escape> |
data-event-key-tab |
Only triggers on Tab key | <input data-action="keydown:handleTab" data-event-key-tab> |
data-event-key-space |
Only triggers on Space key | <div data-action="keyup:toggle" data-event-key-space> |
data-event-key-up |
Only triggers on Arrow Up | <select data-action="keydown:prev" data-event-key-up> |
data-event-key-down |
Only triggers on Arrow Down | <select data-action="keydown:next" data-event-key-down> |
data-event-key-left |
Only triggers on Arrow Left | <div data-action="keydown:prev" data-event-key-left> |
data-event-key-right |
Only triggers on Arrow Right | <div data-action="keydown:next" data-event-key-right> |
Modifier Key Combinations
Filter by modifier keys (Ctrl, Alt, Shift, Meta/Command):
| Modifier | Effect | Example |
|---|---|---|
data-event-key-ctrl |
Requires Ctrl key to be held | <input data-action="keydown:selectAll" data-event-key-ctrl> |
data-event-key-alt |
Requires Alt key to be held | <input data-action="keydown:altAction" data-event-key-alt> |
data-event-key-shift |
Requires Shift key to be held | <input data-action="keydown:shiftAction" data-event-key-shift> |
data-event-key-meta |
Requires Meta/Command key (Mac) | <input data-action="keydown:cmdAction" data-event-key-meta> |
data-event-key-ctrl+s |
Ctrl+S combination | <div data-action="keydown:save" data-event-key-ctrl+s> |
data-event-key-ctrl+z |
Ctrl+Z combination (undo) | <div data-action="keydown:undo" data-event-key-ctrl+z> |
data-event-key-meta+s |
Command+S on Mac | <div data-action="keydown:save" data-event-key-meta+s> |
Combining Modifiers
Multiple modifiers can be used together:
<!-- Prevent default and debounce search input -->
<input type="text"
data-action="input:search"
data-event-prevent
data-event-debounce="300">
<!-- Submit form on Enter, prevent default -->
<input type="text"
data-action="keyup:submitSearch"
data-event-key-enter
data-event-prevent>
<!-- Throttled scroll handler that stops propagation -->
<div data-action="scroll:handleScroll"
data-event-throttle="100"
data-event-stop>
</div>
<!-- Save with Ctrl+S or Cmd+S (cross-platform) -->
<div data-action="keydown:save" data-event-key-ctrl+s data-event-prevent></div>
<div data-action="keydown:save" data-event-key-meta+s data-event-prevent></div>
Event Modifiers Demo
Try the interactive example below to see how event modifiers work:
<div data-component="event-modifiers-demo">
<div class="row">
<!-- Debounce Demo -->
<div class="col-md-6 mb-4">
<h5>Debounced Search</h5>
<p class="small text-muted">Uses <code>data-event-debounce="400"</code> to wait for typing to stop</p>
<input type="text"
data-action="input:handleSearch"
data-event-debounce="400"
placeholder="Type to search..."
class="form-control mb-2">
<div class="small">
<strong>Search calls:</strong> <span data-bind="searchCount" class="badge bg-primary"></span>
<span class="text-muted ms-2">(fires 400ms after you stop typing)</span>
</div>
<div class="small mt-1" data-show="lastSearch">
<strong>Last search:</strong> "<span data-bind="lastSearch"></span>"
</div>
</div>
<!-- Once Demo -->
<div class="col-md-6 mb-4">
<h5>One-Time Action</h5>
<p class="small text-muted">Uses <code>data-event-once</code> to fire only once</p>
<button data-action="handleOnce"
data-event-once
class="btn btn-warning mb-2">
Click Me (Only Works Once!)
</button>
<div class="small">
<strong>Clicks registered:</strong> <span data-bind="onceCount" class="badge bg-warning text-dark"></span>
<span class="text-muted ms-2">(max 1, button auto-disables)</span>
</div>
</div>
</div>
<div class="row">
<!-- Outside Click Demo -->
<div class="col-md-6 mb-4">
<h5>Click Outside Detection</h5>
<p class="small text-muted">Uses <code>data-event-outside</code> to detect clicks outside</p>
<div class="position-relative">
<button data-action="toggleDropdown" class="btn btn-info">
Toggle Dropdown
</button>
<div data-show="dropdownOpen"
data-action="closeDropdown"
data-event-outside
class="position-absolute mt-1 p-3 border rounded bg-light shadow-sm"
style="z-index: 100; min-width: 200px;">
<strong>Dropdown Menu</strong>
<p class="small mb-2">Click outside this box to close it!</p>
<button class="btn btn-sm btn-secondary">Menu Item 1</button>
<button class="btn btn-sm btn-secondary ms-1">Menu Item 2</button>
</div>
</div>
<div class="small mt-2">
<strong>Outside clicks detected:</strong> <span data-bind="outsideClicks" class="badge bg-info"></span>
</div>
</div>
<!-- Conditional Event Demo -->
<div class="col-md-6 mb-4">
<h5>Conditional Events</h5>
<p class="small text-muted">Uses <code>data-event-if="isUnlocked"</code> to conditionally fire</p>
<div class="form-check mb-2">
<input type="checkbox" data-model="isUnlocked" class="form-check-input" id="unlock-check">
<label class="form-check-label" for="unlock-check">
Unlock the button
</label>
</div>
<button data-action="handleConditional"
data-event-if="isUnlocked"
class="btn mb-2"
data-bind-class="isUnlocked ? 'btn btn-success' : 'btn btn-secondary'">
<span data-bind="isUnlocked ? 'Click Me! (Unlocked)' : 'Locked - Toggle Above'"></span>
</button>
<div class="small">
<strong>Successful clicks:</strong> <span data-bind="conditionalClicks" class="badge bg-success"></span>
<span class="text-muted ms-2">(only fires when unlocked)</span>
</div>
</div>
</div>
<div class="row">
<!-- Delay Demo -->
<div class="col-md-6 mb-4">
<h5>Delayed Execution</h5>
<p class="small text-muted">Uses <code>data-event-delay="1000"</code> to delay by 1 second</p>
<button data-action="handleDelayed"
data-event-delay="1000"
class="btn btn-primary mb-2">
Click for Delayed Action
</button>
<div class="small">
<strong>Actions fired:</strong> <span data-bind="delayedCount" class="badge bg-primary"></span>
<span class="text-muted ms-2">(click and wait 1 second)</span>
</div>
</div>
<!-- Self Demo -->
<div class="col-md-6 mb-4">
<h5>Self-Only Events</h5>
<p class="small text-muted">Uses <code>data-event-self</code> to ignore clicks bubbling from children</p>
<div class="d-flex gap-2 mb-2">
<!-- Without data-event-self -->
<div data-action="handleWithoutSelf"
class="flex-grow-1 rounded btn-primary p-2 text-center"
style="cursor: pointer;">
<div class="small mb-1">Without self modifier</div>
<button data-action="handleChildClick" class="btn btn-sm btn-light">
Child Button
</button>
</div>
<!-- With data-event-self -->
<div data-action="handleSelfClick"
data-event-self
class="flex-grow-1 rounded btn-info p-2 text-center"
style="cursor: pointer;">
<div class="small mb-1">With data-event-self</div>
<button data-action="handleChildClick2" class="btn btn-sm btn-light">
Child Button
</button>
</div>
</div>
<div class="small">
<strong>Left parent:</strong> <span data-bind="withoutSelfClicks" class="badge btn-primary"></span>
<strong class="ms-2">Right parent:</strong> <span data-bind="selfClicks" class="badge btn-info"></span>
<strong class="ms-2">Button clicks:</strong> <span data-bind="childClicks" class="badge bg-secondary"></span>
</div>
<div class="small text-muted">
Click the background area (not the button) in both boxes - left parent fires, right parent doesn't (self modifier blocks bubbled clicks from children)
</div>
</div>
</div>
<!-- Reset Button -->
<div class="text-center mt-3 pt-3 border-top">
<button data-action="resetDemo" class="btn btn-secondary">
Reset All Counters
</button>
</div>
</div>
wildflower.component('event-modifiers-demo', {
state: {
// Debounce demo
searchCount: 0,
lastSearch: '',
// Once demo
onceCount: 0,
// Outside click demo
dropdownOpen: false,
outsideClicks: 0,
// Conditional demo
isUnlocked: false,
conditionalClicks: 0,
// Delay demo
delayedCount: 0,
// Self demo
selfClicks: 0,
withoutSelfClicks: 0,
childClicks: 0
},
// Debounced search - only fires 400ms after typing stops
handleSearch(event, element) {
this.searchCount++
this.lastSearch = event.target.value
console.log('Search executed for:', event.target.value)
},
// One-time handler - only fires once ever
handleOnce() {
this.onceCount++
console.log('One-time action fired!')
},
// Outside click detection
toggleDropdown() {
this.dropdownOpen = !this.dropdownOpen
},
closeDropdown() {
if (this.dropdownOpen) {
this.dropdownOpen = false
this.outsideClicks++
console.log('Dropdown closed via outside click')
}
},
// Conditional event - only fires when isUnlocked is true
handleConditional() {
this.conditionalClicks++
console.log('Conditional click registered!')
},
// Delayed execution - handler fires 1 second after click
handleDelayed() {
this.delayedCount++
console.log('Delayed action executed after 1 second')
},
// Self-only demo - comparing with and without data-event-self
handleWithoutSelf() {
this.withoutSelfClicks++
console.log('Parent WITHOUT self: fired (including from child clicks)')
},
handleSelfClick() {
this.selfClicks++
console.log('Parent WITH self: fired (only direct clicks)')
},
handleChildClick() {
this.childClicks++
console.log('Child button clicked (left box)')
},
handleChildClick2() {
this.childClicks++
console.log('Child button clicked (right box)')
},
// Reset all counters
resetDemo() {
this.searchCount = 0
this.lastSearch = ''
this.onceCount = 0
this.dropdownOpen = false
this.outsideClicks = 0
this.isUnlocked = false
this.conditionalClicks = 0
this.delayedCount = 0
this.selfClicks = 0
this.withoutSelfClicks = 0
this.childClicks = 0
}
})
Debouncing Input Handlers
For two-way bindings, debounce the action that consumes the state rather than the state update itself. Pair data-action with data-event-debounce:
<!-- State updates immediately; onSearch runs 300ms after the last keystroke -->
<input type="text"
data-model="searchQuery"
data-action="input:onSearch"
data-event-debounce="300"
placeholder="Type to search...">
This is particularly useful for search inputs where you want to wait for the user to stop typing before triggering expensive operations.
Custom Events and Communication
Create custom events for component communication:
<div>
<!-- Event Publisher Component -->
<div data-component="event-publisher" class="mb-4">
<p class="text-muted">Publish custom events that other components can listen to.</p>
<div class="row">
<div class="col-md-6">
<h5>Predefined Events</h5>
<button data-action="publishMessage" class="btn btn-primary me-2 mb-2">
Send Message Event
</button>
<button data-action="publishData" class="btn btn-secondary me-2 mb-2">
Send Data Event
</button>
<button data-action="publishAlert" class="btn btn-warning mb-2">
Send Alert Event
</button>
</div>
<div class="col-md-6">
<h5>Custom Events</h5>
<div class="mb-2">
<input type="text"
data-model="customMessage"
placeholder="Enter custom message..."
class="form-control mb-2">
</div>
<div class="mb-2">
<select data-model="eventType" class="form-select mb-2">
<option value="info">Info Event</option>
<option value="warning">Warning Event</option>
<option value="success">Success Event</option>
<option value="error">Error Event</option>
</select>
</div>
<button data-action="publishCustom"
class="btn btn-info"
data-bind-class="customButtonClass">
Send Custom Event
</button>
</div>
</div>
<div class="mt-3">
<small class="text-muted">
Published events: <span data-bind="publishedCount" class="badge bg-primary"></span>
</small>
</div>
</div>
<!-- Event Subscriber Component -->
<div data-component="event-subscriber" class="border-top pt-4">
<p class="text-muted">Listening for custom events from the publisher above.</p>
<div class="row">
<div class="col-md-8">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5>Received Events (<span data-bind="eventCount"></span>):</h5>
<button data-action="clearEvents" class="btn btn-sm btn-danger">
Clear Events
</button>
</div>
<div class="border rounded p-2"
style="height: 200px; overflow-y: auto; background-color: var(--card-bg);">
<div data-list="receivedEvents">
<template>
<div class="d-flex justify-content-between align-items-start py-2 border-bottom">
<div class="flex-grow-1">
<div>
<span class="badge" data-bind-class="typeBadgeClass">
<span data-bind="type"></span>
</span>
<strong data-bind="message" class="ms-2"></strong>
</div>
<small class="text-muted" data-show="data">
Data: <code data-bind="dataPreview"></code>
</small>
</div>
<small class="text-muted"><span data-bind="timestamp"></span></small>
</div>
</template>
</div>
<div data-show="isEmpty" class="text-center text-muted py-4">
No events received yet. Use the publisher above to send some events.
</div>
</div>
</div>
<div class="col-md-4">
<h5>Event Statistics</h5>
<div class="p-3 border rounded" style="background-color: var(--card-bg);">
<p><strong>Total Events:</strong> <span data-bind="eventCount"></span></p>
<p><strong>Message Events:</strong> <span data-bind="messageCount"></span></p>
<p><strong>Data Events:</strong> <span data-bind="dataCount"></span></p>
<p><strong>Custom Events:</strong> <span data-bind="customCount"></span></p>
<p><strong>Last Event:</strong> <span data-bind="lastEventTime"></span></p>
</div>
</div>
</div>
</div>
</div>
// Event Publisher Component
wildflower.component('event-publisher', {
state: {
customMessage: '',
eventType: 'info',
publishedCount: 0
},
computed: {
customButtonClass() {
return this.customMessage.trim() ? 'btn btn-info' : 'btn btn-info disabled'
}
},
publishMessage() {
const event = new CustomEvent('app:message', {
detail: {
message: 'Hello from the publisher component!',
timestamp: new Date().toLocaleTimeString(),
source: 'publisher'
}
})
document.dispatchEvent(event)
this.publishedCount++
console.log('Published message event:', event.detail)
},
publishData() {
const sampleData = {
id: Math.random().toString(36).substr(2, 9),
value: Math.floor(Math.random() * 100),
type: 'sample_data',
items: ['item1', 'item2', 'item3'],
metadata: {
version: '1.0',
created: new Date().toISOString()
}
}
const event = new CustomEvent('app:data', {
detail: {
data: sampleData,
timestamp: new Date().toLocaleTimeString(),
source: 'publisher'
}
})
document.dispatchEvent(event)
this.publishedCount++
console.log('Published data event:', event.detail)
},
publishAlert() {
const alertTypes = ['System Update', 'New Feature Available', 'Maintenance Notice', 'Security Alert']
const alertType = alertTypes[Math.floor(Math.random() * alertTypes.length)]
const event = new CustomEvent('app:alert', {
detail: {
message: alertType,
priority: 'high',
timestamp: new Date().toLocaleTimeString(),
source: 'publisher'
}
})
document.dispatchEvent(event)
this.publishedCount++
},
publishCustom() {
const message = this.customMessage.trim()
if (!message) return
const event = new CustomEvent('app:custom', {
detail: {
message: message,
type: this.eventType,
timestamp: new Date().toLocaleTimeString(),
source: 'publisher'
}
})
document.dispatchEvent(event)
this.publishedCount++
this.customMessage = ''
console.log('Published custom event:', event.detail)
}
})
// Event Subscriber Component
wildflower.component('event-subscriber', {
state: {
receivedEvents: []
},
computed: {
eventCount() {
return this.receivedEvents.length
},
messageCount() {
return this.receivedEvents.filter(e => e.type === 'Message').length
},
dataCount() {
return this.receivedEvents.filter(e => e.type === 'Data').length
},
customCount() {
return this.receivedEvents.filter(e => e.type === 'Custom' || e.type === 'Alert').length
},
isEmpty() {
return this.receivedEvents.length === 0
},
lastEventTime() {
return this.receivedEvents.length > 0
? this.receivedEvents[0].timestamp
: 'None'
},
// Item-level computed properties for template
typeBadgeClass() {
const classes = {
'Message': 'bg-primary',
'Data': 'bg-success',
'Alert': 'bg-warning',
'Custom': 'bg-info'
}
return `badge ${classes[this.type] || 'bg-secondary'}`
},
dataPreview() {
if (!this.data) return ''
const str = JSON.stringify(this.data)
return str.length > 50 ? str.substring(0, 50) + '...' : str
}
},
init() {
// Bind event handlers to maintain 'this' context
this.handleMessage = this.handleMessage.bind(this)
this.handleData = this.handleData.bind(this)
this.handleAlert = this.handleAlert.bind(this)
this.handleCustom = this.handleCustom.bind(this)
// Listen for custom events
document.addEventListener('app:message', this.handleMessage)
document.addEventListener('app:data', this.handleData)
document.addEventListener('app:alert', this.handleAlert)
document.addEventListener('app:custom', this.handleCustom)
},
destroy() {
// Clean up event listeners to prevent memory leaks
document.removeEventListener('app:message', this.handleMessage)
document.removeEventListener('app:data', this.handleData)
document.removeEventListener('app:alert', this.handleAlert)
document.removeEventListener('app:custom', this.handleCustom)
console.log('Event subscriber destroyed and listeners removed')
},
handleMessage(event) {
this.addEvent('Message', event.detail.message, null)
console.log('Received message event:', event.detail)
},
handleData(event) {
this.addEvent('Data', `Data received (${Object.keys(event.detail.data).length} properties)`, event.detail.data)
console.log('Received data event:', event.detail)
},
handleAlert(event) {
this.addEvent('Alert', event.detail.message, null)
console.log('Received alert event:', event.detail)
},
handleCustom(event) {
this.addEvent('Custom', `${event.detail.type}: ${event.detail.message}`, null)
console.log('Received custom event:', event.detail)
},
addEvent(type, message, data = null) {
this.receivedEvents.unshift({
type: type,
message: message,
data: data,
timestamp: new Date().toLocaleTimeString()
})
// Keep only last 20 events for performance
if (this.receivedEvents.length > 20) {
this.receivedEvents = this.receivedEvents.slice(0, 20)
}
},
clearEvents() {
this.receivedEvents = []
console.log('Event log cleared')
}
})
Form Event Handling
Handle form submission, validation, and input events:
<div data-component="form-handler">
<p class="text-muted">Real-time validation with comprehensive event handling.</p>
<form data-action="handleSubmit" class="needs-validation" novalidate>
<div class="row">
<div class="col-md-6">
<h5>Personal Information</h5>
<div class="mb-3">
<label class="form-label">Full Name: <span class="text-danger">*</span></label>
<input type="text"
data-model="form.name"
data-action="blur:validateName input:handleNameInput"
class="form-control"
placeholder="Enter your full name"
required>
<div class="text-danger small mt-1" data-bind="errors.name"></div>
<small class="form-text text-muted">
Length: <span data-bind="nameLength"></span> characters
</small>
</div>
<div class="mb-3">
<label class="form-label">Email Address: <span class="text-danger">*</span></label>
<input type="email"
data-model="form.email"
data-action="input:validateEmail focus:handleEmailFocus"
class="form-control"
placeholder="Enter your email"
required>
<small class="form-text" data-bind-class="emailHintClass">
<span data-bind="emailHint"></span>
</small>
</div>
<div class="mb-3">
<label class="form-label">Age: <span class="text-danger">*</span></label>
<input type="text" inputmode="numeric"
data-model="form.age"
data-action="change:validateAge input:handleAgeInput"
class="form-control"
placeholder="Enter your age"
required>
<div class="text-danger small mt-1" data-bind="errors.age"></div>
<small class="form-text text-muted">
Age range: 18-100 years
</small>
</div>
<div class="mb-3">
<label class="form-label">Country:</label>
<select data-model="form.country"
data-action="change:handleCountryChange"
class="form-select">
<option value="">Select your country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="uk">United Kingdom</option>
<option value="au">Australia</option>
<option value="de">Germany</option>
<option value="fr">France</option>
<option value="other">Other</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox"
data-model="form.newsletter"
data-action="change:handleNewsletterChange"
class="form-check-input"
id="newsletter">
<label class="form-check-label" for="newsletter">
Subscribe to newsletter
</label>
</div>
</div>
</div>
<div class="col-md-6">
<h5>Form Status & Actions</h5>
<div class="p-3 border rounded mb-3" style="background-color: var(--card-bg);">
<h6>Validation Status</h6>
<div class="mb-2">
<span class="badge" data-bind-class="nameValidationClass">
Name: <span data-bind="nameValidationText"></span>
</span>
</div>
<div class="mb-2">
<span class="badge" data-bind-class="emailValidationClass">
Email: <span data-bind="emailValidationText"></span>
</span>
</div>
<div class="mb-2">
<span class="badge" data-bind-class="ageValidationClass">
Age: <span data-bind="ageValidationText"></span>
</span>
</div>
<hr>
<p><strong>Form Valid:</strong>
<span data-bind="isFormValid" class="badge" data-bind-class="formValidClass"></span>
</p>
</div>
<div class="mb-3">
<button type="submit"
data-bind-class="submitButtonClass"
data-bind-attr="{ disabled: isSubmitDisabled }">
<span data-bind="submitButtonText"></span>
</button>
<button type="button"
data-action="resetForm"
class="btn btn-secondary ms-2">
Reset Form
</button>
<button type="button"
data-action="fillSampleData"
class="btn btn-info ms-2">
Fill Sample Data
</button>
</div>
<div class="mb-3">
<h6>Event Log (<span data-bind="eventCount"></span> events):</h6>
<div class="border rounded p-2"
style="height: 150px; overflow-y: auto; font-family: monospace; font-size: 0.8em; background-color: var(--card-bg);">
<div data-list="eventLog">
<template>
<div class="d-flex justify-content-between py-1 border-bottom">
<span><span data-bind="timestamp"></span> - <span data-bind="event"></span></span>
<small class="badge bg-secondary"><span data-bind="field"></span></small>
</div>
</template>
</div>
<div data-show="noEvents" class="text-center text-muted py-3">
No events logged yet. Start interacting with the form.
</div>
</div>
</div>
</div>
</div>
</form>
<div class="mt-4" data-show="submissionResult">
<div class="alert alert-success">
<h6>✅ Form Submitted Successfully!</h6>
<p>The form data has been processed. Here's what was submitted:</p>
<pre class="mb-0" style="font-size: 0.85em;"><code data-bind="submissionResult"></code></pre>
</div>
</div>
</div>
wildflower.component('form-handler', {
state: {
form: {
name: '',
email: '',
age: '',
country: '',
newsletter: false
},
errors: {
name: '',
email: '',
age: ''
},
submissionResult: '',
eventLog: [],
isSubmitting: false
},
computed: {
// Form validation status
isFormValid() {
return this.form.name.trim() &&
this.form.email.trim() &&
this.form.age &&
!this.errors.name &&
!this.errors.email &&
!this.errors.age
},
// Field-specific computed properties
nameLength() {
return this.form.name.length
},
nameValidationClass() {
if (!this.form.name) return 'bg-secondary'
return this.errors.name ? 'bg-danger' : 'bg-success'
},
nameValidationText() {
if (!this.form.name) return 'Empty'
return this.errors.name ? 'Invalid' : 'Valid'
},
emailValidationClass() {
if (!this.form.email) return 'bg-secondary'
return this.errors.email ? 'bg-danger' : 'bg-success'
},
emailValidationText() {
if (!this.form.email) return 'Empty'
return this.errors.email ? 'Invalid' : 'Valid'
},
emailHint() {
if (!this.form.email) return 'Enter a valid email address'
if (this.errors.email) return this.errors.email
return 'Email format looks good'
},
emailHintClass() {
if (!this.form.email) return 'text-muted'
return this.errors.email ? 'text-danger' : 'text-success'
},
ageValidationClass() {
if (!this.form.age) return 'bg-secondary'
return this.errors.age ? 'bg-danger' : 'bg-success'
},
ageValidationText() {
if (!this.form.age) return 'Empty'
return this.errors.age ? 'Invalid' : 'Valid'
},
formValidClass() {
return this.isFormValid ? 'bg-success' : 'bg-danger'
},
// Submit button logic
submitButtonClass() {
const baseClass = 'btn'
if (this.isSubmitting) return `${baseClass} btn-secondary`
return this.isFormValid ? `${baseClass} btn-primary` : `${baseClass} btn-primary disabled`
},
submitButtonText() {
if (this.isSubmitting) return 'Submitting...'
return 'Submit Form'
},
isSubmitDisabled() {
return !this.isFormValid || this.isSubmitting
},
// Event log
eventCount() {
return this.eventLog.length
},
noEvents() {
return this.eventLog.length === 0
}
},
// Form submission
async handleSubmit(event, element) {
event.preventDefault()
this.logEvent('Form submission attempted', 'form')
// Validate all fields
this.validateName()
this.validateEmail()
this.validateAge()
if (this.isFormValid) {
this.isSubmitting = true
this.logEvent('Form validation passed, submitting...', 'form')
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500))
// Success - show result
this.submissionResult = JSON.stringify({
...this.form,
submittedAt: new Date().toISOString(),
validationStatus: 'passed'
}, null, 2)
this.logEvent('Form submitted successfully', 'form')
// Auto-reset after showing result
setTimeout(() => {
this.resetForm()
}, 5000)
} catch (error) {
this.logEvent('Form submission failed', 'form')
console.error('Form submission error:', error)
} finally {
this.isSubmitting = false
}
} else {
this.logEvent('Form validation failed', 'form')
}
},
// Field validation methods
validateName(event, element) {
const name = this.form.name.trim()
if (!name) {
this.errors.name = 'Name is required'
} else if (name.length < 2) {
this.errors.name = 'Name must be at least 2 characters'
} else if (name.length > 50) {
this.errors.name = 'Name must be less than 50 characters'
} else {
this.errors.name = ''
}
if (event) this.logEvent(`Name validated: ${this.errors.name || 'valid'}`, 'name')
},
validateEmail(event, element) {
const email = this.form.email.trim()
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!email) {
this.errors.email = 'Email is required'
} else if (!emailRegex.test(email)) {
this.errors.email = 'Please enter a valid email address'
} else {
this.errors.email = ''
}
if (event) this.logEvent(`Email validated: ${this.errors.email || 'valid'}`, 'email')
},
validateAge(event, element) {
const age = parseInt(this.form.age)
if (!this.form.age) {
this.errors.age = 'Age is required'
} else if (isNaN(age)) {
this.errors.age = 'Please enter a valid number'
} else if (age < 18) {
this.errors.age = 'Must be at least 18 years old'
} else if (age > 100) {
this.errors.age = 'Must be 100 years or younger'
} else {
this.errors.age = ''
}
if (event) this.logEvent(`Age validated: ${this.errors.age || 'valid'}`, 'age')
},
// Input event handlers
handleNameInput(event, element) {
this.logEvent(`Name input: "${event.target.value}"`, 'name')
},
handleEmailFocus(event, element) {
this.logEvent('Email field focused', 'email')
},
handleAgeInput(event, element) {
this.logEvent(`Age input: ${event.target.value}`, 'age')
},
handleCountryChange(event, element) {
this.logEvent(`Country selected: ${event.target.value || 'none'}`, 'country')
},
handleNewsletterChange(event, element) {
this.logEvent(`Newsletter: ${event.target.checked ? 'subscribed' : 'unsubscribed'}`, 'newsletter')
},
// Utility methods
resetForm() {
this.form = {
name: '',
email: '',
age: '',
country: '',
newsletter: false
}
this.errors = {
name: '',
email: '',
age: ''
}
this.submissionResult = ''
this.eventLog = []
this.logEvent('Form reset', 'form')
},
fillSampleData() {
this.form = {
name: 'John Doe',
email: 'john.doe@example.com',
age: '28',
country: 'us',
newsletter: true
}
// Validate the sample data
this.validateName()
this.validateEmail()
this.validateAge()
this.logEvent('Sample data filled', 'form')
},
logEvent(description, field) {
this.eventLog.unshift({
timestamp: new Date().toLocaleTimeString(),
event: description,
field: field
})
// Keep only last 20 events
if (this.eventLog.length > 20) {
this.eventLog = this.eventLog.slice(0, 20)
}
}
})