Components with Events
Events enable components to respond to user interactions and communicate with other parts of your application. This guide covers creating components with custom events, handling user interactions, and integrating with FormEngine's action system.
Understanding Events in FormEngine
An event in FormEngine components occurs when a component property annotated with event is called. Event handlers, or actions, can be attached to an event. Actions are simply functions (synchronous or asynchronous) that are executed sequentially, one after another.
See also the section on event handling.
Event Flow
User Interaction → Component Event → FormEngine → Actions
Example Flow:
- User clicks a button
- Button's
onClickevent fires - FormEngine captures the event
- Configured actions execute (validation, API calls, etc.)
- UI updates based on action results
Basic Event Implementation
Example: Button with Custom Event
import {boolean, define, event, string} from '@react-form-builder/core'
export const ActionButton = ({children, onClick, disabled, loading}: any) => {
const handleClick = (event) => {
if (!disabled && !loading) {
onClick?.(event)
}
}
return (
<button
className="action-button"
onClick={handleClick}
disabled={disabled || loading}
>
{loading && <span className="action-button__spinner">⟳</span>}
<span className="action-button__text">{children}</span>
</button>
)
}
export const actionButton = define(ActionButton, 'ActionButton')
.props({
children: string.default('Click Me'),
// Define an event prop
onClick: event,
disabled: boolean.default(false),
loading: boolean.default(false)
})
.build()
Usage in JSON:
{
"key": "submitButton",
"type": "ActionButton",
"props": {
"children": {
"value": "Submit Form"
}
},
"events": {
"onClick": [
{
"name": "validate",
"type": "common"
},
{
"name": "log",
"type": "common"
}
]
}
}
In this example, clicking the button will sequentially call two built-in event handlers. First, the validate action will be called, which
will validate the form, then the log action will be called, which will write the event data to the console.
Live Example
function App() { const ActionButton = ({children, onClick, disabled, loading}) => { const handleClick = (event) => { if (!disabled && !loading) { onClick?.(event) } } return ( <button className="action-button" onClick={handleClick} disabled={disabled || loading} > {loading && <span className="action-button__spinner">⟳</span>} <span className="action-button__text">{children}</span> </button> ) } const actionButton = define(ActionButton, 'ActionButton') .props({ children: string.default('Click Me'), onClick: event, disabled: boolean.default(false), loading: boolean.default(false) }) .build() const view = createView([actionButton.model]) const formJson = { "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "submitButton", "type": "ActionButton", "props": { "children": { "value": "Submit Form" } }, "events": { "onClick": [ { "name": "validate", "type": "common" }, { "name": "log", "type": "common" } ] } } ] } } return ( <FormViewer view={view} getForm={() => JSON.stringify(formJson)} onFormDataChange={({data}) => { console.log(JSON.stringify(data)) }} /> ) }
Event Types
onChange Event
The most common event for input components:
import {array, define, event, string} from '@react-form-builder/core'
const CustomSelect = ({value, onChange, options, placeholder}: any) => (
<select
value={value}
onChange={e => onChange(e.target.value)}
>
<option value="">{placeholder}</option>
{options.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
)
export const customSelect = define(CustomSelect, 'CustomSelect')
.props({
value: string.valued.uncontrolledValue(''),
onChange: event,
options: array.default([]),
placeholder: string.default('Select an option...')
})
.build()
Live Example
function App() { const CustomSelect = ({value, onChange, options, placeholder}) => ( <select value={value} onChange={e => onChange(e.target.value)} > <option value="">{placeholder}</option> {options.map(opt => ( <option key={opt.value} value={opt.value}> {opt.label} </option> ))} </select> ) const customSelect = define(CustomSelect, 'CustomSelect') .props({ value: string.valued.uncontrolledValue(''), onChange: event, options: array.default([]), placeholder: string.default('Select an option...') }) .build() const view = createView([customSelect.model]) const formJson = { "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "customSelect", "type": "CustomSelect", "props": { "options": { "value": [ { "label": "Mercedes", "value": "m" }, { "label": "Toyota", "value": "t" }, { "label": "Kia", "value": "k" } ] } } } ] } } return ( <FormViewer view={view} getForm={() => JSON.stringify(formJson)} onFormDataChange={({data}) => { console.log(JSON.stringify(data)) }} /> ) }
onBlur and onFocus Events
For handling focus changes:
import {define, event, string} from '@react-form-builder/core'
import {useState} from 'react'
const ValidatedInput = ({value, onChange, onBlur, onFocus, label}: any) => {
const [isFocused, setIsFocused] = useState(false)
const handleFocus = (e) => {
setIsFocused(true)
onFocus?.(e)
}
const handleBlur = (e) => {
setIsFocused(false)
onBlur?.(e)
}
return (
<div className={`validated-input ${isFocused ? 'is-focused' : ''}`}>
<label>{label} <input
value={value}
onChange={e => onChange(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
/>
</label>
</div>
)
}
export const validatedInput = define(ValidatedInput, 'ValidatedInput')
.props({
value: string.valued.uncontrolledValue(''),
label: string,
onChange: event,
onBlur: event,
onFocus: event,
})
.build()
Live Example
function App() { const ValidatedInput = ({value, onChange, onBlur, onFocus, label}) => { const [isFocused, setIsFocused] = useState(false) const handleFocus = (e) => { setIsFocused(true) onFocus?.(e) } const handleBlur = (e) => { setIsFocused(false) onBlur?.(e) } return ( <div className={`validated-input ${isFocused ? 'is-focused' : ''}`}> <label>{label} <input value={value} onChange={e => onChange(e.target.value)} onFocus={handleFocus} onBlur={handleBlur} /> </label> </div> ) } const validatedInput = define(ValidatedInput, 'ValidatedInput') .props({ value: string.valued.uncontrolledValue(''), label: string, onChange: event, onBlur: event, onFocus: event, }) .build() const view = createView([validatedInput.model]) const formJson = { "actions": { "handleBlur": { "body": "e.store.formData.state.itemFocused = false" }, "handleFocus": { "body": "e.store.formData.state.itemFocused = true" } }, "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "validatedInput", "type": "ValidatedInput", "props": { "label": { "computeType": "function", "fnSource": "return 'MyLabel ' + (form.state.itemFocused ? 'focused' : 'unfocused')" } }, "events": { "onBlur": [ { "name": "handleBlur", "type": "code" } ], "onFocus": [ { "name": "handleFocus", "type": "code" } ] } } ] } } return ( <FormViewer view={view} getForm={() => JSON.stringify(formJson)} onFormDataChange={({data}) => { console.log(JSON.stringify(data)) }} /> ) }
In this example, saving the itemFocused variable to the form state
has been added to the event handlers. The label for the input is a computed property that depends on the form state.
For more information on saving user state, see this section of the documentation.
Testing Event Handling
import {cleanup, fireEvent, render} from '@testing-library/react'
import {afterEach, describe, expect, test, vi} from 'vitest'
import {ActionButton} from './ActionButton'
import '@testing-library/jest-dom/vitest'
describe('ActionButton', () => {
afterEach(() => {
cleanup()
})
test('calls onClick when clicked', () => {
const onClick = vi.fn()
const {getByText} = render(
<ActionButton onClick={onClick}>Click Me</ActionButton>
)
fireEvent.click(getByText('Click Me'))
expect(onClick).toHaveBeenCalledTimes(1)
})
test('passes event data correctly', () => {
const onClick = vi.fn()
const {getByText} = render(
<ActionButton onClick={onClick}>Click Me</ActionButton>
)
fireEvent.click(getByText('Click Me'))
expect(onClick).toHaveBeenCalledWith(expect.any(Object))
})
test('respects disabled state', () => {
const onClick = vi.fn()
const {getByText} = render(
<ActionButton onClick={onClick} disabled={true}>Click Me</ActionButton>
)
fireEvent.click(getByText('Click Me'))
expect(onClick).not.toHaveBeenCalled()
})
test('shows loading state', async () => {
const onClick = vi.fn(() => new Promise(resolve => setTimeout(resolve, 100)))
const {getByText} = render(
<ActionButton onClick={onClick} loading={true}>Click Me</ActionButton>
)
expect(getByText('⟳')).toBeInTheDocument()
})
})
Summary
Events are crucial for interactive components. Key points:
- Use the
eventtype builder to define events - Events receive the full form context automatically
- Can trigger common actions or custom logic
- Support async operations
Next Steps:
- Explore Advanced Patterns for complex component scenarios
Happy event handling! 🎉