Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions CLAUDE.md
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
```
210 changes: 200 additions & 10 deletions docs/options/events/README.md
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 |
| `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')
}
}
}
}
})
```
Loading
Loading