|
| 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. |
0 commit comments