-
Notifications
You must be signed in to change notification settings - Fork 206
fix: events system #411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
fix: events system #411
Changes from 5 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
966822d
fix: update events
kevinchappell 0a5000b
chore: event system tests
kevinchappell 6368e14
chore: update CLAUDE.md
kevinchappell 11b3e96
chore: remove commented uot code
kevinchappell 48531fc
feat: onRemove and onAdd events
kevinchappell e52e6cb
chore: remove test file
kevinchappell 8ccfd77
Update docs/options/events/README.md
kevinchappell b6f5fc6
Update src/lib/js/common/events.test.js
kevinchappell 8e4b6ef
docs: add missing callback options to events documentation
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| ## Project Overview | ||
|
|
||
| Formeo is a zero-dependency, highly configurable drag-and-drop form builder library. It provides two main classes: | ||
| - **FormeoEditor**: Visual form building interface with drag-and-drop capabilities | ||
| - **FormeoRenderer**: Renders form definitions into functional HTML forms | ||
|
|
||
| The library is framework-agnostic and provides TypeScript definitions. It's distributed as UMD, ES, and CommonJS modules. | ||
|
|
||
| ## Development Commands | ||
|
|
||
| ### Core Development | ||
| ```bash | ||
| npm start # Build icons and start dev server at http://localhost:5173 | ||
| npm run dev # Start dev server only | ||
| npm run build # Build library and demo (runs build:lib first) | ||
| npm run build:lib # Build library for distribution (both minified and unminified) | ||
| ``` | ||
|
|
||
| ### Testing | ||
| ```bash | ||
| npm test # Run unit tests with Node.js native test runner | ||
| npm run test:watch # Run tests in watch mode | ||
| npm run test:updateSnapshots # Update test snapshots | ||
| npm run playwright:test # Run e2e tests | ||
| npm run playwright:test:ui # Run e2e tests with browser UI | ||
| npm run playwright:test:report # View Playwright test report | ||
| ``` | ||
|
|
||
| ### Code Quality | ||
| ```bash | ||
| npm run lint # Lint with Biome | ||
| npm run lint:fix # Lint and auto-fix issues | ||
| npm run format # Format code with Biome | ||
| ``` | ||
|
|
||
| ### Other | ||
| ```bash | ||
| npm run preview # Preview production build locally | ||
| npm run build:icons # Generate SVG sprite from icons | ||
| ``` | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### Component Hierarchy | ||
|
|
||
| Formeo uses a hierarchical component system with four core levels: | ||
| 1. **Stage** → top-level container (equivalent to a form/page) | ||
| 2. **Row** → horizontal sections within a stage | ||
| 3. **Column** → vertical divisions within a row (enables multi-column layouts) | ||
| 4. **Field** → individual form controls (input, select, textarea, etc.) | ||
|
|
||
| Each component type is managed by dedicated data classes in `/src/lib/js/components/`: | ||
| - `stages/index.js` - StagesData | ||
| - `rows/index.js` - RowsData | ||
| - `columns/index.js` - ColumnsData | ||
| - `fields/index.js` - FieldsData | ||
|
|
||
| All components extend the base `Component` class (`/src/lib/js/components/component.js`), which provides: | ||
| - Event system with `addEventListener`/`removeEventListener` | ||
| - Data management via `get`/`set` methods | ||
| - DOM rendering with `render()` method | ||
| - Address-based component lookup (e.g., `stages.abc-123.rows`) | ||
|
|
||
| ### Data Flow | ||
|
|
||
| **FormData Structure**: The canonical form definition is a flat structure with components keyed by ID: | ||
| ```javascript | ||
| { | ||
| id: "form-id", | ||
| stages: { "stage-id": { id: "stage-id", attrs: {}, children: ["row-id-1"] } }, | ||
| rows: { "row-id-1": { id: "row-id-1", children: ["col-id-1"] } }, | ||
| columns: { "col-id-1": { id: "col-id-1", children: ["field-id-1"] } }, | ||
| fields: { "field-id-1": { id: "field-id-1", tag: "input", attrs: { type: "text" } } } | ||
| } | ||
| ``` | ||
|
|
||
| **Address System**: Components are accessed via dot-notation addresses: `stages.{id}.rows` or `fields.{id}.attrs.label`. See `splitAddress()` in `/src/lib/js/common/utils/string.mjs`. | ||
|
|
||
| **Components Singleton**: The `/src/lib/js/components/index.js` exports a singleton `Components` instance that acts as the central data store. Access it via: | ||
| - `Components.get(type, id)` - Get component instance | ||
| - `Components.getAddress('stages.abc-123')` - Get component by address | ||
| - `Components.formData` - Get complete form definition | ||
|
|
||
| ### Key Modules | ||
|
|
||
| **DOM Utilities** (`/src/lib/js/common/dom.js`): | ||
| - `dom.render(data, tag)` - Render data to DOM elements | ||
| - `dom.create({tag, content, attributes})` - Create DOM elements | ||
| - `dom.empty(element)` - Remove all children | ||
| - Extensive DOM manipulation helpers | ||
|
|
||
| **Events System** (`/src/lib/js/common/events.js`): | ||
| - Global event bus for component communication | ||
| - Component-level events via `Component.addEventListener()` | ||
| - Supports bubbling and custom event data | ||
|
|
||
| **Actions System** (`/src/lib/js/common/actions.js`): | ||
| - Handles user actions (add, remove, clone, edit components) | ||
| - Integrates with undo/redo if enabled | ||
|
|
||
| **Controls** (`/src/lib/js/components/controls/index.js`): | ||
| - Extensible control registry for form field types | ||
| - Each control type (input, select, textarea, etc.) is defined in `/src/lib/js/components/controls/form/` | ||
| - Custom controls can be registered via the `controls` config option | ||
|
|
||
| **Edit Panel** (`/src/lib/js/components/edit-panel/edit-panel.js`): | ||
| - Sidebar for editing component properties | ||
| - Auto-generates edit forms based on control metadata | ||
|
|
||
| ### Renderer Architecture | ||
|
|
||
| The `FormeoRenderer` class (`/src/lib/js/renderer/index.js`) converts formData into rendered HTML forms: | ||
| - Processes conditional logic (show/hide fields based on conditions) | ||
| - Handles field attributes, validation, and user data | ||
| - Exposes `userData` getter/setter for form values | ||
| - Completely separate from editor code (no shared DOM dependencies) | ||
|
|
||
| ## Code Conventions | ||
|
|
||
| - **Formatting**: Use Biome (line width 120, single quotes, 2 spaces) | ||
| - **Module System**: ES modules (`.js` and `.mjs` files). Use `.mjs` for pure utility modules without DOM dependencies | ||
| - **Imports**: Relative paths required, import order enforced by Biome | ||
| - **Naming**: camelCase for variables/functions, PascalCase for classes | ||
| - **Constants**: UPPER_SNAKE_CASE in `/src/lib/js/constants.js` | ||
| - **Testing**: Use Node.js native test runner with snapshot support | ||
| - **Commits**: Conventional commits enforced (types: feat, fix, chore, docs, style, refactor, test, build, ci, perf, revert) | ||
|
|
||
| ## Important Implementation Notes | ||
|
|
||
| - **Circular Dependencies**: Be aware that `Components` has circular imports with individual component types. The `/src/lib/js/components/index.js` carefully manages this with lazy loading. | ||
| - **Event System**: Components can define events in their config that fire during lifecycle (onCreate, onRender, onRemove). The Events module in `/src/lib/js/common/events.js` coordinates these. | ||
| - **Internationalization**: Uses `@draggable/i18n` package. Language files loaded from CDN by default. See `/src/lib/js/config.js` for i18n config. | ||
| - **Icons**: SVG sprite generated at build time from `/src/lib/icons/`. Icon references use the pattern `#formeo-sprite-{icon-name}`. | ||
| - **SessionStorage**: Editor can persist formData to sessionStorage. Controlled by `sessionStorage` option. | ||
| - **TypeScript**: No TypeScript source files, but type definitions should be maintained for consumers. | ||
|
|
||
| ## Git Hooks | ||
|
|
||
| Lefthook is configured with: | ||
| - **pre-commit**: Runs Biome linting/formatting on staged files | ||
| - **pre-push**: Runs full test suite | ||
| - **commit-msg**: Validates conventional commit format | ||
|
|
||
| ## Build Output | ||
|
|
||
| The build process produces: | ||
| - `dist/formeo.es.js` - ES module | ||
| - `dist/formeo.cjs.js` - CommonJS module | ||
| - `dist/formeo.umd.js` - UMD bundle (unpkg default) | ||
| - `dist/formeo.min.*.js` - Minified versions | ||
| - `dist/formeo.min.css` - Compiled styles | ||
| - `dist/formeo-sprite.svg` - Icon sprite | ||
| - `dist/formData_schema.json` - JSON Schema for form data validation | ||
|
|
||
| ## CI/CD | ||
|
|
||
| - **Semantic Release**: Automated versioning and publishing to npm on main branch | ||
| - **GitHub Actions**: Playwright tests run on PRs, deploy to GitHub Pages on publish | ||
| - **Conventional Commits**: Required for proper semantic versioning | ||
|
|
||
| ## Repository Structure | ||
|
|
||
| ``` | ||
| src/ | ||
| ├── lib/ # Core library source | ||
| │ ├── js/ | ||
| │ │ ├── editor.js # FormeoEditor main class | ||
| │ │ ├── renderer/ # FormeoRenderer implementation | ||
| │ │ ├── components/ # Component hierarchy (stages/rows/columns/fields) | ||
| │ │ ├── common/ # Shared utilities (dom, events, actions) | ||
| │ │ ├── config.js # Default configuration | ||
| │ │ └── constants.js # Global constants | ||
| │ ├── sass/ # Styles | ||
| │ └── icons/ # SVG icons for sprite generation | ||
| └── demo/ # Demo site source | ||
|
|
||
| tools/ # Build and development utilities | ||
| dist/ # Build output (gitignored) | ||
| docs/ # Documentation | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,203 @@ | ||
| # Events | ||
|
|
||
| Events are emitted by interacting with the form. While [actions](../actions/) let you override certain functionality, events simply allow you to react to an event (typically after an action completes). | ||
| Below are a list of built-in events. | ||
|
|
||
| | Option | Type | Description | | ||
| | -------------------- | -------- | --------------------------------- | | ||
| | [formeoLoaded](#) | Function | Fires when Formeo loads | | ||
| | [onAdd](#) | Function | Fires when element is added | | ||
| | [onSave](#) | Function | Fires when form is saved | | ||
| | [onUpdate](#) | Function | Fires when form is updated | | ||
| | [onRender](#) | Function | Fires when an element is rendered | | ||
| | [confirmClearAll](#) | Function | Fires when form is cleared | | ||
|
|
||
| Formeo supports **two ways** to work with events: | ||
|
|
||
| 1. **Configuration Callbacks** - Pass event handler functions in the `events` configuration option | ||
| 2. **DOM Event Listeners** - Listen for custom events dispatched on the document | ||
|
|
||
| ## Configuration Callbacks | ||
|
|
||
| Pass callback functions when initializing Formeo: | ||
|
|
||
| ```javascript | ||
| const editor = new FormeoEditor({ | ||
| events: { | ||
| onChange: (eventData) => { | ||
| console.log('Form changed:', eventData) | ||
| }, | ||
| onUpdate: (eventData) => { | ||
| console.log('Form updated:', eventData) | ||
| }, | ||
| onSave: (eventData) => { | ||
| console.log('Form saved:', eventData.formData) | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| ### Available Callback Options | ||
|
|
||
| | Option | Type | Description | | ||
| | -------------------- | -------- | ---------------------------------------- | | ||
| | `formeoLoaded` | Function | Fires when Formeo loads | | ||
| | `onAdd` | Function | Fires when element is added | | ||
| | `onChange` | Function | Fires when form data changes | | ||
| | `onUpdate` | Function | Fires when form is updated (all changes) | | ||
| | `onUpdateStage` | Function | Fires when stage is updated | | ||
| | `onUpdateRow` | Function | Fires when row is updated | | ||
| | `onUpdateColumn` | Function | Fires when column is updated | | ||
| | `onUpdateField` | Function | Fires when field is updated | | ||
| | `onSave` | Function | Fires when form is saved | | ||
| | `onRender` | Function | Fires when an element is rendered | | ||
| | `confirmClearAll` | Function | Fires when form is cleared | | ||
|
|
||
| ## DOM Event Listeners | ||
|
|
||
| Listen for custom events dispatched on the document: | ||
|
|
||
| ```javascript | ||
| // Listen for any form update | ||
| document.addEventListener('formeoUpdated', (event) => { | ||
| console.log('Form updated:', event.detail) | ||
| }) | ||
|
|
||
| // Listen for changes (alias for formeoUpdated) | ||
| document.addEventListener('formeoChanged', (event) => { | ||
| console.log('Form changed:', event.detail) | ||
| }) | ||
|
|
||
| // Listen for specific component updates | ||
| document.addEventListener('formeoUpdatedField', (event) => { | ||
| console.log('Field updated:', event.detail) | ||
| }) | ||
| ``` | ||
|
|
||
| ### Available DOM Events | ||
|
|
||
| | Event Name | Description | | ||
| | ------------------------ | ---------------------------------------- | | ||
| | `formeoLoaded` | Formeo has finished loading | | ||
| | `formeoSaved` | Form has been saved | | ||
| | `formeoUpdated` | Form data has been updated | | ||
| | `formeoChanged` | Form data has changed (alias for updated)| | ||
| | `formeoUpdatedStage` | Stage component was updated | | ||
| | `formeoUpdatedRow` | Row component was updated | | ||
| | `formeoUpdatedColumn` | Column component was updated | | ||
| | `formeoUpdatedField` | Field component was updated | | ||
kevinchappell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| | `formeoCleared` | Form has been cleared | | ||
| | `formeoOnRender` | Component has been rendered | | ||
| | `formeoConditionUpdated` | Conditional logic has been updated | | ||
|
|
||
| ## Event Data Structure | ||
|
|
||
| Events include detailed information about what changed: | ||
|
|
||
| ```javascript | ||
| { | ||
| timeStamp: 1699123456789, // When the event occurred | ||
| type: 'formeoUpdated', // Event type | ||
| detail: { // Event-specific details | ||
| entity: Component, // Component that changed | ||
| dataPath: 'fields.abc123', // Path to the component | ||
| changePath: 'fields.abc123.attrs.label', // Specific property | ||
| value: 'New Label', // New value | ||
| previousValue: 'Old Label', // Previous value (if applicable) | ||
| changeType: 'changed', // 'added', 'removed', or 'changed' | ||
| data: {...}, // Full component data | ||
| src: HTMLElement // DOM element (if applicable) | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## onChange vs onUpdate | ||
|
|
||
| Both callbacks receive the same event data, but serve different purposes: | ||
|
|
||
| - **`onChange`**: General notification that something in the form changed | ||
| - **`onUpdate`**: More specific notification about updates, can be used with component-specific variants | ||
|
|
||
| You can use either or both depending on your needs: | ||
|
|
||
| ```javascript | ||
| const editor = new FormeoEditor({ | ||
| events: { | ||
| // Handle all changes generically | ||
| onChange: (evt) => { | ||
| saveToLocalStorage(evt.detail) | ||
| }, | ||
|
|
||
| // Handle updates with more granularity | ||
| onUpdate: (evt) => { | ||
| logChange(evt) | ||
| }, | ||
|
|
||
| // Handle specific component updates | ||
| onUpdateField: (evt) => { | ||
| validateField(evt.detail) | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| ## Mixing Both Approaches | ||
|
|
||
| You can use both configuration callbacks AND DOM event listeners together: | ||
|
|
||
| ```javascript | ||
| // Configuration callback | ||
| const editor = new FormeoEditor({ | ||
| events: { | ||
| onChange: (evt) => { | ||
| console.log('Config callback:', evt) | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| // DOM event listener | ||
| document.addEventListener('formeoChanged', (evt) => { | ||
| console.log('DOM listener:', evt.detail) | ||
| }) | ||
| ``` | ||
|
|
||
| Both will be called when the form changes. | ||
|
|
||
| ## Best Practices | ||
|
|
||
| 1. **Use configuration callbacks** when you control the Formeo initialization | ||
| 2. **Use DOM event listeners** when you need to react to Formeo events from external code | ||
| 3. **Component-specific events** (like `onUpdateField`) are useful for targeted reactions | ||
| 4. **Throttling**: The generic `formeoUpdated` event is automatically throttled to prevent excessive callbacks | ||
| 5. **Event bubbling**: Set `bubbles: true` in configuration to enable event bubbling (useful for debugging) | ||
|
|
||
| ## Example: Auto-save with Debounce | ||
|
|
||
| ```javascript | ||
| import { debounce } from 'lodash' | ||
|
|
||
| const autoSave = debounce((formData) => { | ||
| fetch('/api/forms/save', { | ||
| method: 'POST', | ||
| body: JSON.stringify(formData), | ||
| headers: { 'Content-Type': 'application/json' } | ||
| }) | ||
| }, 1000) | ||
|
|
||
| const editor = new FormeoEditor({ | ||
| events: { | ||
| onChange: (evt) => { | ||
| autoSave(evt.detail.data) | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| ## Example: Field Validation | ||
|
|
||
| ```javascript | ||
| const editor = new FormeoEditor({ | ||
| events: { | ||
| onUpdateField: (evt) => { | ||
| const { changePath, value } = evt.detail | ||
|
|
||
| // Only validate when label changes | ||
| if (changePath.endsWith('.attrs.label')) { | ||
| if (!value || value.trim().length === 0) { | ||
| console.warn('Field label cannot be empty') | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.