Event Handling

WildflowerJS binds DOM events to component methods through data-action attributes, providing a clean separation between markup and behavior.

💡 Key Concept: WildflowerJS uses the data-action attribute to bind DOM events to component methods, providing a clean separation between markup and behavior.

Basic Event Handling

Use data-action to bind events to component methods:

<div data-component="interactive-events-demo">
    <div class="row">
        <div class="col-md-4">
            <h5>Click Events</h5>
            <button data-action="handleClick" class="btn btn-primary mb-2">
                Click Me
            </button>
            <p><strong>Clicked:</strong> <span data-bind="clickCount" class="badge bg-primary"></span> times</p>
            <button data-action="dblclick:doubleClick" class="btn btn-success btn-sm">
                Double-Click Me
            </button>
        </div>
        
        <div class="col-md-4">
            <h5>Input Events</h5>
            <input type="text" 
                   data-action="input:handleInputChange keyup:handleKeyup" 
                   data-model="inputValue" 
                   placeholder="Type something..." 
                   class="form-control mb-2">
            <p><strong>Value:</strong> <em data-bind="inputValue"></em></p>
            <p><strong>Length:</strong> <span data-bind="inputLength"></span> chars</p>
            <p><strong>Last Key:</strong> <code data-bind="lastKey"></code></p>
        </div>
        
        <div class="col-md-4">
            <h5>Mouse Events</h5>
            <button data-action="mouseenter:handleHover mouseleave:handleHoverOut"
                    class="btn btn-secondary mb-2">
                Hover Me
            </button>
            <p data-bind="hoverStatus"></p>
            <div data-action="mousedown:handleMouseDown mouseup:handleMouseUp" 
                 class="border rounded p-2 bg-light text-center" 
                 style="height: 60px; cursor: pointer;">
                Mouse Area
                <div class="small"><span data-bind="mouseStatus"></span></div>
            </div>
        </div>
    </div>
    
    <div class="mt-4">
        <h5>Event Log</h5>
        <div class="d-flex justify-content-between mb-2">
            <span>Recent events (<span data-bind="eventCount"></span>):</span>
            <button data-action="clearEventLog" class="btn btn-sm btn-secondary">
                Clear Log
            </button>
        </div>
        <div class="border rounded p-2" style="height: 120px; overflow-y: auto; font-family: monospace; font-size: 0.85em;">
            <div data-list="eventLog">
                <template>
                    <div class="small d-flex justify-content-between" 
                         data-bind-class="type === 'click' ? 'text-primary' : type === 'input' ? 'text-success' : 'text-info'">
                        <span><span data-bind="timestamp"></span> - <span data-bind="description"></span></span>
                        <span class="badge badge-sm" data-bind-class="badgeClass">
                            <span data-bind="type"></span>
                        </span>
                    </div>
                </template>
            </div>
        </div>
    </div>
</div>
wildflower.component('interactive-events-demo', {
    state: {
        clickCount: 0,
        inputValue: '',
        hoverStatus: 'Not hovering',
        mouseStatus: 'Ready',
        lastKey: 'none',
        eventLog: []
    },
    
    computed: {
        inputLength() {
            return this.inputValue.length
        },
        
        eventCount() {
            return this.eventLog.length
        },

        // Item-level computed for event log badges: receives the row's item
        badgeClass(entry) {
            const classes = {
                'click': 'bg-primary',
                'input': 'bg-success',
                'mouse': 'bg-info',
                'keyboard': 'bg-warning'
            }
            return classes[entry.type] || 'bg-secondary'
        }
    },
    
    // Click event handlers
    handleClick(event, element) {
        this.clickCount++
        this.logEvent('Button clicked', 'click')
        console.log('Button clicked!', { event, element })
    },
    
    doubleClick(event, element) {
        this.clickCount += 2
        this.logEvent('Double click performed', 'click')
    },
    
    // Input event handlers
    handleInputChange(event, element) {
        this.logEvent(`Input changed: "${event.target.value}"`, 'input')
        console.log('Input changed:', event.target.value)
    },
    
    handleKeyup(event, element) {
        this.lastKey = event.key
        this.logEvent(`Key pressed: ${event.key}`, 'keyboard')
    },
    
    // Mouse event handlers
    handleHover(event, element) {
        this.hoverStatus = 'Currently hovering! 🙂'
        this.logEvent('Mouse hover started', 'mouse')
    },
    
    handleHoverOut(event, element) {
        this.hoverStatus = 'Not hovering'
        this.logEvent('Mouse hover ended', 'mouse')
    },
    
    handleMouseDown(event, element) {
        this.mouseStatus = 'Mouse Down 🔄'
        this.logEvent('Mouse down in area', 'mouse')
    },
    
    handleMouseUp(event, element) {
        this.mouseStatus = 'Mouse Up ⬆️'
        this.logEvent('Mouse up in area', 'mouse')
        
        // Reset after a short delay
        setTimeout(() => {
            this.mouseStatus = 'Ready'
        }, 1000)
    },
    
    // Utility methods
    logEvent(description, type) {
        this.eventLog.unshift({
            timestamp: new Date().toLocaleTimeString(),
            description: description,
            type: type
        })
        
        // Keep only last 15 events
        if (this.eventLog.length > 15) {
            this.eventLog = this.eventLog.slice(0, 15)
        }
    },
    
    clearEventLog() {
        this.eventLog = []
        this.logEvent('Event log cleared', 'click')
    }
})
Live Preview

Event Types and Syntax

WildflowerJS supports multiple event binding syntaxes:

Syntax Description Example
data-action="method" Default click event <button data-action="save">
data-action="event:method" Specific event type <input data-action="change:validate">
data-action="event1:method1 event2:method2" Multiple events <div data-action="mouseenter:show mouseout:hide">
data-action="method('arg1', 'arg2')" Method with arguments <button data-action="setStatus('active')">
<div data-component="event-types-demo">
    <div class="row">
        <div class="col-md-6">
            <h5>Form Events</h5>
            <form data-action="submit:handleSubmit">
                <div class="mb-2">
                    <input type="text"
                           data-action="focus:handleFocus blur:handleBlur"
                           placeholder="Focus/blur events"
                           class="form-control">
                </div>
                <div class="mb-2">
                    <select data-action="change:handleSelectChange" class="form-select">
                        <option value="">Select an option...</option>
                        <option value="option1">Form Option 1</option>
                        <option value="option2">Form Option 2</option>
                        <option value="option3">Form Option 3</option>
                    </select>
                </div>
                <button type="submit" class="btn btn-primary">Submit Form</button>
            </form>
        </div>
        
        <div class="col-md-6">
            <h5>Mouse Events</h5>
            <div data-action="mousedown:handleMouseDown mouseup:handleMouseUp mousemove:handleMouseMove" 
                 class="border p-3 text-center" 
                 style="height: 120px; cursor: crosshair; background-color: var(--card-bg);">
                <div>Mouse interaction area</div>
                <div class="small mt-2 text-muted">
                    Position: (<span data-bind="mouseX">0</span>, <span data-bind="mouseY">0</span>)
                </div>
                <div class="small text-muted">
                    Status: <span data-bind="mouseStatus">Ready</span>
                </div>
            </div>
        </div>
    </div>
    
    <div class="mt-4">
        <div class="d-flex justify-content-between align-items-center mb-2">
            <h5>Event Log (<span data-bind="eventCount"></span> events):</h5>
            <button data-action="clearEventLog" class="btn btn-sm btn-secondary">
                Clear Log
            </button>
        </div>
        <div class="border rounded p-2"
             style="height: 120px; 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="type"></span></small>
                    </div>
                </template>
            </div>
            <div data-show="isEmpty" class="text-center text-muted py-3">
                No events logged yet. Interact with the form and mouse area above.
            </div>
        </div>
    </div>
</div>
wildflower.component('event-types-demo', {
    state: {
        mouseX: 0,
        mouseY: 0,
        mouseStatus: 'Ready',
        eventLog: []
    },
    
    computed: {
        eventCount() {
            return this.eventLog.length
        },
        
        isEmpty() {
            return this.eventLog.length === 0
        }
    },
    
    // Form event handlers
    handleSubmit(event, element) {
        event.preventDefault()
        this.logEvent('Form submitted', 'form')

        // Get form data
        const formData = new FormData(element)
        const data = Object.fromEntries(formData.entries())
        console.log('Form submitted with data:', data)
    },
    
    handleFocus(event, element) {
        this.logEvent(`Input focused: ${element.placeholder}`, 'focus')
    },
    
    handleBlur(event, element) {
        const value = event.target.value
        this.logEvent(`Input blurred${value ? ': ' + value : ' (empty)'}`, 'blur')
    },
    
    handleSelectChange(event, element) {
        const value = event.target.value
        this.logEvent(`Select changed to: ${value || '(none)'}`, 'change')
    },
    
    // Mouse event handlers
    handleMouseDown(event, element) {
        this.mouseStatus = 'Mouse Down'
        this.logEvent('Mouse button pressed', 'mouse')
    },
    
    handleMouseUp(event, element) {
        this.mouseStatus = 'Mouse Up'
        this.logEvent('Mouse button released', 'mouse')
        
        // Reset status after delay
        setTimeout(() => {
            this.mouseStatus = 'Ready'
        }, 1000)
    },
    
    handleMouseMove(event, element) {
        const rect = element.getBoundingClientRect()
        this.mouseX = Math.round(event.clientX - rect.left)
        this.mouseY = Math.round(event.clientY - rect.top)
        
        // Throttle mouse move events in log
        if (!this._mouseMoveThrottle) {
            this._mouseMoveThrottle = true
            this.logEvent(`Mouse moved to (${this.mouseX}, ${this.mouseY})`, 'mouse')
            setTimeout(() => {
                this._mouseMoveThrottle = false
            }, 500)
        }
    },
    
    // Utility methods
    logEvent(eventDescription, type = 'general') {
        this.eventLog.unshift({
            timestamp: new Date().toLocaleTimeString(),
            event: eventDescription,
            type: type
        })
        
        // Keep only last 20 events
        if (this.eventLog.length > 20) {
            this.eventLog = this.eventLog.slice(0, 20)
        }
    },
    
    clearEventLog() {
        this.eventLog = []
        this.logEvent('Event log cleared', 'system')
    }
})
Live Preview
More Event Features: Event modifiers (data-event-prevent, data-event-debounce, keyboard filters, click-outside detection), custom events for component communication, and form event handling are covered on the Advanced Events page.

Event Handler Arguments

Event handlers receive three arguments:

Argument Type Description
event Event Native DOM event object
element HTMLElement Element that triggered the event
details Object Additional context (list item data, etc.)
<div data-component="arguments-demo">
    <div class="mb-4">
        <h5>Event Analysis</h5>
        <button data-action="analyzeEvent"
                class="btn btn-info me-2"
                data-custom="primary-button">
            Analyze This Click
        </button>
        <button data-action="analyzeEvent"
                class="btn btn-secondary"
                data-custom="secondary-button">
            Analyze This Click
        </button>
    </div>
    
    <div class="mb-4">
        <h5>List Items with Context (<span data-bind="itemCount"></span> items):</h5>
        <div class="mb-2">
            <button data-action="addRandomItem" class="btn btn-sm btn-success me-2">
                Add Random Item
            </button>
            <button data-action="resetItems" class="btn btn-sm btn-secondary">
                Reset Items
            </button>
        </div>
        <div data-list="items">
            <template>
                <div class="card mb-2">
                    <div class="card-body d-flex justify-content-between align-items-center">
                        <div>
                            <strong data-bind="name"></strong>
                            <small class="text-muted d-block">
                                Category: <span data-bind="category"></span> |
                                ID: <span data-bind="id"></span>
                            </small>
                        </div>
                        <div>
                            <button data-action="editItem"
                                    class="btn btn-sm btn-primary me-2"
                                    data-item-action="edit">
                                Edit
                            </button>
                            <button data-action="deleteItem"
                                    class="btn btn-sm btn-danger"
                                    data-item-action="delete">
                                Delete
                            </button>
                        </div>
                    </div>
                </div>
            </template>
        </div>
        <div data-show="isEmpty" class="text-center text-muted py-3">
            No items to display. Add some items to see context handling.
        </div>
    </div>
    
    <div class="mt-4">
        <h5>Last Event Details:</h5>
        <div style="background-color: var(--card-bg);">
            <pre data-bind="lastEventDetails" style="margin: 0; white-space: pre-wrap; font-size: 0.85em;"></pre>
        </div>
    </div>
</div>
wildflower.component('arguments-demo', {
    state: {
        items: [
            { id: 1, name: 'Task Alpha', category: 'work' },
            { id: 2, name: 'Task Beta', category: 'personal' },
            { id: 3, name: 'Task Gamma', category: 'work' }
        ],
        lastEventDetails: 'No events analyzed yet. Click a button or interact with list items to see event details.'
    },
    
    computed: {
        itemCount() {
            return this.items.length
        },
        
        isEmpty() {
            return this.items.length === 0
        }
    },
    
    analyzeEvent(event, element, details) {
        // Comprehensive event analysis
        const eventInfo = {
            // Event object properties
            event: {
                type: event.type,
                timestamp: new Date(event.timeStamp).toISOString(),
                bubbles: event.bubbles,
                cancelable: event.cancelable,
                target: {
                    tagName: event.target.tagName,
                    className: event.target.className,
                    textContent: event.target.textContent.substring(0, 50)
                }
            },
            
            // Element object properties
            element: {
                tagName: element.tagName,
                className: element.className,
                id: element.id || 'no-id',
                dataset: element.dataset,
                textContent: element.textContent.substring(0, 50)
            },
            
            // Details object (context from framework)
            details: details || 'No additional context provided',
            
            // Analysis metadata
            analysis: {
                eventAndElementSame: event.target === element,
                hasCustomData: Object.keys(element.dataset).length > 0,
                componentContext: this.constructor.name || 'unknown'
            }
        }
        
        this.lastEventDetails = JSON.stringify(eventInfo, null, 2)
        console.log('Event Analysis:', eventInfo)
    },
    
    editItem(event, element, details) {
        // Use details.index from the framework's action context
        const itemIndex = details.index
        const item = details.item

        const eventInfo = {
            action: 'Edit item',
            itemData: item,
            listContext: {
                itemIndex: itemIndex,
                totalItems: this.items.length,
                detailsProvided: true
            },
            elementInfo: {
                tag: element.tagName,
                classes: element.className.split(' '),
                customAttributes: element.dataset
            },
            eventDetails: {
                type: event.type,
                button: event.button || 'n/a',
                ctrlKey: event.ctrlKey,
                altKey: event.altKey
            }
        }

        this.lastEventDetails = JSON.stringify(eventInfo, null, 2)
    },

    deleteItem(event, element, details) {
        // Use details.index and details.item from the framework's action context
        const itemIndex = details.index
        const item = details.item

        if (confirm(`Delete "${item.name}"?`)) {
            this.items.splice(itemIndex, 1)

            const eventInfo = {
                action: 'Deleted item',
                deletedItem: item,
                listContext: {
                    itemWasAtIndex: itemIndex,
                    remainingItems: this.items.length,
                    listLength: this.items.length
                },
                eventContext: {
                    type: event.type,
                    elementClicked: element.textContent.trim()
                }
            }

            this.lastEventDetails = JSON.stringify(eventInfo, null, 2)
        }
    },

    addRandomItem() {
        const names = ['Task Delta', 'Task Epsilon', 'Task Zeta', 'Task Theta', 'Task Lambda']
        const categories = ['work', 'personal', 'urgent', 'research']

        const name = names[Math.floor(Math.random() * names.length)]
        const category = categories[Math.floor(Math.random() * categories.length)]
        const id = Date.now() + Math.floor(Math.random() * 1000)

        this.items.push({ id, name, category })

        this.lastEventDetails = `Added new item: ${JSON.stringify({ id, name, category }, null, 2)}`
    },

    resetItems() {
        this.items = [
            { id: 1, name: 'Task Alpha', category: 'work' },
            { id: 2, name: 'Task Beta', category: 'personal' },
            { id: 3, name: 'Task Gamma', category: 'work' }
        ]
        this.lastEventDetails = 'Items reset to original state.'
    }
})
Live Preview

Passing Arguments to Actions

You can pass literal arguments directly in data-action attributes. This eliminates the need for separate methods when the only difference is a parameter value.

Supported Literal Types

Type Examples Notes
String 'hello', "hello" Single or double quoted
Number 42, 3.14, -1, 0 Integer, decimal, negative
Boolean true, false Exact match
Null null Exact match

Syntax

Pattern Description Example
method('arg') Click handler with argument <button data-action="setPriority('high')">
method('a', 'b', 3) Multiple arguments <button data-action="configure('dark', 2, true)">
event:method('arg') Specific event with argument <input data-action="input:search('users')">

Accessing Arguments

Arguments are available in two ways: via details.args (an array), and as extra parameters appended after the standard (event, element, details) signature.

<div data-component="action-args-demo">
    <div class="mb-3">
        <h5>Priority: <span data-bind="priority"></span></h5>
        <button data-action="setPriority('high')" class="btn btn-sm btn-danger me-1">High</button>
        <button data-action="setPriority('medium')" class="btn btn-sm btn-warning me-1">Medium</button>
        <button data-action="setPriority('low')" class="btn btn-sm btn-success">Low</button>
    </div>

    <div class="mb-3">
        <h5>Opacity: <span data-bind="opacity"></span></h5>
        <button data-action="setOpacity(1)" class="btn btn-sm btn-secondary me-1">100%</button>
        <button data-action="setOpacity(0.5)" class="btn btn-sm btn-secondary me-1">50%</button>
        <button data-action="setOpacity(0)" class="btn btn-sm btn-secondary">0%</button>
    </div>

    <div>
        <h5>Last Action:</h5>
        <pre data-bind="lastAction"></pre>
    </div>
</div>
wildflower.component('action-args-demo', {
    state: {
        priority: 'medium',
        opacity: 1,
        lastAction: ''
    },

    setPriority(event, element, details) {
        // details.args = ['high'], ['medium'], or ['low']
        this.priority = details.args[0]
        this.lastAction = 'setPriority called with: ' + details.args[0]
    },

    setOpacity(event, element, details) {
        // details.args = [1], [0.5], or [0]
        this.opacity = details.args[0]
        this.lastAction = 'setOpacity called with: ' + details.args[0]
    }
})
Live Preview

In list contexts, details.args coexists with the standard list context properties (details.item, details.index, etc.):

<div data-list="tasks">
    <template>
        <div>
            <span data-bind="title"></span>
            <button data-action="setStatus('done')">Complete</button>
            <button data-action="setStatus('archived')">Archive</button>
        </div>
    </template>
</div>
setStatus(event, element, details) {
    const task = details.item    // list item for this row
    const status = details.args[0]  // 'done' or 'archived'
    task.status = status
}

Event Best Practices

✅ Do
  • Use descriptive handler method names
  • Prevent default behavior when needed
  • Validate form inputs on appropriate events
  • Clean up custom event listeners in destroy()
  • Use event delegation for dynamic content
❌ Don't
  • Modify state excessively in mouse move handlers
  • Use onclick="..." attributes; use data-action instead
  • Skip event.preventDefault() on form submissions
  • Add addEventListener in init() without removing in destroy()
  • Create separate methods when data-action="method('arg')" suffices