Skip to content

Commit ca2973d

Browse files
committed
feat: component events
ability to pass event handlers through config for common events such as onAdd
1 parent 29ec90e commit ca2973d

File tree

8 files changed

+779
-22
lines changed

8 files changed

+779
-22
lines changed

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,6 @@ Render the form, or update the rendered form to use the given `formData` object.
7575

7676
## [Options](options/)
7777

78+
## [Component Events](component-events.md)
79+
7880
## [Build Tools](tools/)

docs/component-events.md

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
# Component Event System
2+
3+
## Overview
4+
5+
This document describes the enhanced Component event system that allows you to configure event handlers for component lifecycle events. The system provides both backwards compatibility and enhanced functionality similar to the `action` config pattern in dom.js.
6+
7+
## What Changed
8+
9+
### 1. Enhanced Component Constructor
10+
- Added `eventListeners` Map to store event handlers
11+
- Added `initEventHandlers()` call to process config.events
12+
13+
### 2. New Event Methods
14+
- `addEventListener(eventName, handler)` - Add event listeners programmatically
15+
- `removeEventListener(eventName, handler)` - Remove specific event listeners
16+
- `dispatchComponentEvent(eventName, eventData)` - Dispatch events to all registered listeners
17+
18+
### 3. Enhanced Existing Methods
19+
All major component lifecycle methods now dispatch events:
20+
21+
- **`onAdd`** - Dispatches when component is added to form
22+
- **`remove`** - Dispatches `onRemove` before removal
23+
- **`addChild`** - Dispatches enhanced `onAddChild` event
24+
- **`onRender`** - Dispatches `onRender` event
25+
- **`clone`** - Dispatches `onClone` event
26+
- **`set`** - Dispatches `onUpdate` when data changes
27+
28+
## Available Events
29+
30+
| Event Name | When Triggered | Event Data |
31+
|------------|----------------|------------|
32+
| `onAdd` | Component added to form (via drag-drop OR addChild) | `{addedVia, ...}` + context-specific data |
33+
| `onRemove` | Before component removal | `{path, parent, children}` |
34+
| `onAddChild` | Child added to component (parent perspective) | `{parent, child, index, childData}` |
35+
| `onRender` | Component rendered to DOM | `{dom}` |
36+
| `onClone` | Component cloned | `{original, clone, parent}` |
37+
| `onUpdate` | Component data changed | `{path, oldValue, newValue}` |
38+
39+
### Event Context Data
40+
41+
**onAdd Event Data:**
42+
- **Via Drag-Drop:** `{from, to, item, newIndex, fromType, toType, addedComponent, addedVia: 'dragDrop'}`
43+
- **Via addChild:** `{parent, child, index, childData, addedVia: 'addChild'}`
44+
45+
### When to Use Each Event
46+
47+
**Use `onAdd`** when you want to react to a component being added to the form, regardless of how it was added:
48+
```javascript
49+
onAdd: (eventData) => {
50+
console.log('Component added:', eventData.component.id)
51+
52+
// Different handling based on how it was added
53+
if (eventData.addedVia === 'dragDrop') {
54+
// Handle drag-drop specific logic
55+
} else if (eventData.addedVia === 'addChild') {
56+
// Handle programmatic/click addition logic
57+
}
58+
}
59+
```
60+
61+
**Use `onAddChild`** when you want to react to this component receiving a new child:
62+
```javascript
63+
onAddChild: (eventData) => {
64+
console.log('I received a new child:', eventData.child.id)
65+
// This fires on the parent component
66+
}
67+
```
68+
69+
**Note:** When `addChild()` is called, both events fire:
70+
1. `onAddChild` fires on the parent component
71+
2. `onAdd` fires on the child component being added
72+
73+
All events include common data:
74+
- `component` - The component instance
75+
- `type` - Event name
76+
- `timestamp` - When event occurred
77+
78+
## Configuration Usage
79+
80+
### Basic Configuration
81+
```javascript
82+
const config = {
83+
rows: {
84+
all: {
85+
events: {
86+
onAdd: (eventData) => {
87+
console.log('Row added:', eventData.component.id)
88+
},
89+
onRemove: (eventData) => {
90+
console.log('Row removed:', eventData.component.id)
91+
}
92+
}
93+
}
94+
}
95+
}
96+
```
97+
98+
### Targeting Specific Components
99+
```javascript
100+
const config = {
101+
fields: {
102+
// All text fields
103+
text: {
104+
events: {
105+
onAdd: (eventData) => {
106+
console.log('Text field added!')
107+
}
108+
}
109+
},
110+
111+
// Specific field by ID
112+
'field-id-12345': {
113+
events: {
114+
onUpdate: (eventData) => {
115+
console.log('Specific field updated')
116+
}
117+
}
118+
}
119+
}
120+
}
121+
```
122+
123+
### Runtime Event Listeners
124+
```javascript
125+
// Add listeners programmatically
126+
component.addEventListener('onUpdate', (eventData) => {
127+
console.log('Component updated:', eventData.path)
128+
})
129+
130+
// Remove listeners
131+
component.removeEventListener('onUpdate', handlerFunction)
132+
```
133+
134+
## Backwards Compatibility
135+
136+
The refactoring maintains full backwards compatibility:
137+
138+
1. **Existing `config.events.onRender`** - Still works exactly as before
139+
2. **Existing `config.events.onAddChild`** - Still called in addition to new event system
140+
3. **Global formeo events** - Continue to be dispatched as before
141+
142+
## Migration Guide
143+
144+
### Before (Limited Event Support)
145+
```javascript
146+
const config = {
147+
rows: {
148+
all: {
149+
events: {
150+
onAdd: (element) => {
151+
console.log('Row added:', element.id)
152+
}
153+
}
154+
}
155+
}
156+
}
157+
```
158+
159+
### After (Enhanced Event Support)
160+
```javascript
161+
const config = {
162+
rows: {
163+
all: {
164+
events: {
165+
// Same onAdd works, but with more data
166+
onAdd: (eventData) => {
167+
console.log('Row added:', eventData.component.id)
168+
console.log('Added from:', eventData.fromType)
169+
console.log('Added to:', eventData.toType)
170+
},
171+
172+
// New events available
173+
onRemove: (eventData) => {
174+
console.log('Row being removed')
175+
},
176+
177+
onUpdate: (eventData) => {
178+
if (eventData.path === 'attrs.className') {
179+
console.log('Row class changed')
180+
}
181+
}
182+
}
183+
}
184+
}
185+
}
186+
```
187+
188+
## Benefits
189+
190+
### 1. Consistent Event Interface
191+
All components now support the same set of events with consistent data structures.
192+
193+
### 2. Enhanced Debugging
194+
Event data includes detailed context about what changed, when, and where.
195+
196+
### 3. Granular Control
197+
Listen to specific property changes, component types, or individual instances.
198+
199+
### 4. Error Handling
200+
Event handlers are wrapped in try-catch to prevent one handler from breaking others.
201+
202+
### 5. Runtime Flexibility
203+
Add and remove event listeners dynamically without reconfiguring.
204+
205+
## Implementation Details
206+
207+
### Event Data Structure
208+
```javascript
209+
{
210+
component: ComponentInstance, // The component that triggered the event
211+
type: 'onAdd', // Event name
212+
timestamp: 1699123456789, // When event occurred
213+
// Event-specific data varies by event type
214+
}
215+
```
216+
217+
### Error Handling
218+
If an event handler throws an error, it's logged to console but doesn't prevent other handlers from executing.
219+
220+
### Performance
221+
- Event listeners are stored in efficient Map structures
222+
- Events are only dispatched if listeners exist
223+
- Minimal overhead when no listeners are configured
224+
225+
## Example Use Cases
226+
227+
### Auto-save on Changes
228+
```javascript
229+
stages: {
230+
all: {
231+
events: {
232+
onUpdate: (eventData) => {
233+
localStorage.setItem('formeo-autosave', JSON.stringify(eventData.component.data))
234+
}
235+
}
236+
}
237+
}
238+
```
239+
240+
### Validation on Field Updates
241+
```javascript
242+
fields: {
243+
all: {
244+
events: {
245+
onUpdate: (eventData) => {
246+
if (eventData.path === 'attrs.required' && eventData.newValue) {
247+
// Add visual required indicator
248+
eventData.component.dom.classList.add('field-required')
249+
}
250+
}
251+
}
252+
}
253+
}
254+
```
255+
256+
### Custom Analytics
257+
```javascript
258+
rows: {
259+
all: {
260+
events: {
261+
onAdd: (eventData) => {
262+
analytics.track('component_added', {
263+
type: 'row',
264+
id: eventData.component.id
265+
})
266+
}
267+
}
268+
}
269+
}
270+
```
271+
272+
This system provides a solid foundation for building more interactive and responsive form building experiences while maintaining the simplicity and backwards compatibility of the existing system.

src/demo/js/options/config.js

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,20 @@ const config = {
22
rows: {
33
all: {
44
events: {
5-
onRender: element => {
6-
console.log(`You just added a new row with the id "${element.id}"`)
5+
onAdd: evt => {
6+
console.log(`You just added a new row with the id "${evt.target.id}"`, evt.index)
7+
if (evt.index === 2) {
8+
const actionBtnWrap = evt.target.dom.querySelector('.action-btn-wrap')
9+
const addColumnBtn = document.createElement('button')
10+
addColumnBtn.type = 'button'
11+
addColumnBtn.className = 'btn btn-sm btn-secondary'
12+
addColumnBtn.textContent = 'Add Column'
13+
addColumnBtn.style = 'border:1px solid #ccc; width: auto;'
14+
addColumnBtn.addEventListener('click', () => {
15+
evt.target.addChild()
16+
})
17+
actionBtnWrap.appendChild(addColumnBtn)
18+
}
719
},
820
},
921
},
@@ -13,25 +25,11 @@ const config = {
1325
actionButtons: {
1426
// buttons: ['edit'], // array of allow action buttons
1527
},
16-
},
17-
'a33bcc32-c54c-46ed-9609-7cdb5b3dc511': {
18-
events: {
19-
onRender: element => {
20-
console.log(element)
21-
const onRenderTimeout = setTimeout(() => {
22-
// formeo.Components.fields.get(element.id).toggleEdit(true)
23-
element.querySelector('.next-group').click()
24-
clearTimeout(onRenderTimeout)
25-
}, 333)
26-
},
27-
},
2828
panels: {
2929
attrs: {
3030
hideDisabled: true,
3131
},
32-
disabled: [
33-
// 'conditions'
34-
],
32+
disabled: ['options'],
3533
},
3634
},
3735
},

0 commit comments

Comments
 (0)