Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
run: npm install
- name: Test
run: npm test
- name: Type-check public types
run: npm run test:types
- name: Build library
run: npm run build:lib
- name: Test built package
Expand Down
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ npm run build:lib # Build library for distribution (both minified and
### Testing
```bash
npm test # Run unit tests with Node.js native test runner
npm run test:types # Type-check src/types/formeo.d.ts against its fixtures
npm run test:watch # Run tests in watch mode
npm run test:updateSnapshots # Update test snapshots
npm run playwright:test # Run e2e tests
Expand Down Expand Up @@ -136,7 +137,7 @@ The `FormeoRenderer` class (`/src/lib/js/renderer/index.js`) converts formData i
- **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.
- **TypeScript**: No TypeScript sources. Public types are hand-written in src/types/formeo.d.ts and copied to dist/formeo.d.ts + dist/formeo.d.cts by tools/build-types.mjs after build:lib. Any change to the public API (options, events, before hooks, actions, DOM events, methods, formData shape) must update that file and src/types/*.test-d.ts, and npm run test:types must pass. Keep FormeoFormData in sync with tools/formdata-schema.mjs.

## Git Hooks

Expand All @@ -153,6 +154,7 @@ The build process produces:
- `dist/formeo.umd.js` - UMD bundle (unpkg default)
- `dist/formeo.min.{es.js,cjs,umd.js}` - Minified versions
- `dist/formeo.min.css` - Compiled styles
- `dist/formeo.d.ts`, `dist/formeo.d.cts` - TypeScript definitions (ESM/UMD and CommonJS)
- `dist/formeo-sprite.svg` - Icon sprite
- `dist/formData_schema.json` - JSON Schema for form data validation

Expand Down
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,10 @@ import 'formeo/dist/formeo.min.css'
const editor = new FormeoEditor({
editorContainer: '#formeo-editor',
events: {
onSave: (formData) => {
onSave: ({ formData }) => {
console.log('Form saved:', formData)
// Render the form
renderer.render(formData.formData)
renderer.render(formData)
}
}
})
Expand All @@ -141,20 +141,31 @@ const renderer = new FormeoRenderer({

### TypeScript

Formeo includes TypeScript definitions. Import and use with full type support:
Formeo ships its own type definitions (`dist/formeo.d.ts`, `dist/formeo.d.cts`); TypeScript picks them up when you
import `formeo`. See [TypeScript](https://github.com/Draggable/formeo/blob/main/docs/typescript.md) for what is typed.

```typescript
import { FormeoEditor, FormeoRenderer } from 'formeo'
import type { FormeoOptions, FormData } from 'formeo'
import type { FormeoEditorOptions, FormeoFormData } from 'formeo'

const options: FormeoOptions = {
editorContainer: '#formeo-editor'
const renderer = new FormeoRenderer({ renderContainer: '#formeo-renderer' })

const options: FormeoEditorOptions = {
editorContainer: '#formeo-editor',
events: {
onSave: ({ formData }) => renderer.render(formData),
},
}

const editor = new FormeoEditor(options)
const formData: FormData = editor.formData
editor.whenReady().then(() => {
const formData: FormeoFormData = editor.formData
console.log(Object.keys(formData.fields))
})
```

`FormeoOptions` and `FormData` still work as aliases of `FormeoEditorOptions` and `FormeoFormData`.

## Framework Integration

Formeo can be integrated with popular frontend frameworks:
Expand Down Expand Up @@ -202,6 +213,7 @@ Comprehensive documentation is available in the [docs](https://github.com/Dragga
- **[Events](https://github.com/Draggable/formeo/blob/main/docs/options/events/README.md)** - Available events and callbacks
- **[Actions](https://github.com/Draggable/formeo/blob/main/docs/options/actions/README.md)** - Action handlers
- **[Editor API](https://github.com/Draggable/formeo/blob/main/docs/editor/README.md)** - Editor methods and properties
- **[TypeScript](https://github.com/Draggable/formeo/blob/main/docs/typescript.md)** - Shipped type definitions and typed events
- **[Build Tools](https://github.com/Draggable/formeo/blob/main/docs/tools/README.md)** - Development and build utilities

## Development
Expand Down
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,8 @@ Remove the rendered form from the page and stop its pagination (the page navigat
- [Component Events](editor/component-events.md) - Component lifecycle event system
- [Clear Method](editor/editor-clear-method.md) - Resetting the editor to initial state

## [TypeScript](typescript.md)

Type definitions ship with the package; this page lists what is typed and how to narrow the before-hook details.

## [Build Tools](tools/)
2 changes: 1 addition & 1 deletion docs/editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const editor = new FormeoEditor(options, formData)

| Argument | Type | Description |
| ---------- | ---------------- | -------------------------------------------------------------------------------------------- |
| `options` | Object | Required. See [Options](../options/README.md). Pass `{}` for defaults. |
| `options` | Object | Optional. See [Options](../options/README.md). |
| `formData` | Object \| String | Optional form definition (or its JSON string) to load. Takes priority over `options.formData`. |

Set `editorContainer` to an element or selector, otherwise the editor is built but not attached to the page.
Expand Down
4 changes: 2 additions & 2 deletions docs/options/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
| `remove.conditions` | Function | Called before a condition is removed with its × button, and by "Clear All" in the conditions panel (`evt.isClearAll: true`); call `evt.removeAction()` to remove |
| `remove.component` | Function | Called when a row, column or field's remove (×) button is clicked; call `evt.removeAction()` to remove it. Default: removes at once. See [Confirm before deleting](#confirm-before-deleting) |
| `remove.page` | Function | Called before a page is removed from its tab; call `evt.removeAction()` to remove it. Default: removes an empty page, asks first for a page with content. See [Page Tabs](../../editor/pages.md#actions) |
| `click.button` | Function | Called when clicking a form action button |
| `save` | Function | Called when saving |
| `click.btn` | Function | Called when the Save button is clicked, just before `save.form`; `evt` is `{ action, coords, message, button }` |
| `save.form` | Function | Called with the formData when the form is saved |

With Actions you can modify or completely replace some editor functions. By default, adding an attribute opens a small dialog in the editor. Define `add.attr` to use your own UI or extra validation.

Expand Down
18 changes: 10 additions & 8 deletions docs/options/events/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ const editor = new FormeoEditor({
| `onBeforeSave` | Function | Runs before the Save button saves; can cancel or hold it. See [Before hooks](#before-hooks) |
| `onSave` | Function | Fires when form is saved |
| `onRender` | Function | Fires when an element is rendered |
| `onEditOpen` | Function | Fires when a row, column or field's edit panel opens. See [Edit panel events](#edit-panel-events) |
| `onEditClose` | Function | Fires when a row, column or field's edit panel closes. See [Edit panel events](#edit-panel-events) |
| `onEditOpen` | Function | Fires when a stage, row, column or field's edit panel opens. See [Edit panel events](#edit-panel-events) |
| `onEditClose` | Function | Fires when a stage, row, column or field's edit panel closes. See [Edit panel events](#edit-panel-events) |
| `onPageChange` | Function | Fires when the active page tab switches (with the `pages` option). See [Page Tabs](../../editor/pages.md#events) |
| `confirmClearAll` | Function | Fires when form is cleared |

Expand Down Expand Up @@ -106,8 +106,8 @@ document.addEventListener('formeoUpdatedField', (event) => {
| `formeoBeforeSave` | Before the Save button saves; cancelable. See [Before hooks](#before-hooks) |
| `formeoCleared` | Form has been cleared |
| `formeoOnRender` | Component has been rendered |
| `formeoEditOpened` | A row, column or field's edit panel opened |
| `formeoEditClosed` | A row, column or field's edit panel closed |
| `formeoEditOpened` | A stage, row, column or field's edit panel opened |
| `formeoEditClosed` | A stage, row, column or field's edit panel closed |
| `formeoConditionUpdated` | Conditional logic has been updated |

Like the callbacks, `formeoAddedRow`, `formeoAddedColumn` and `formeoAddedField` fire for components added after load, not for components loaded from `formData`.
Expand Down Expand Up @@ -186,10 +186,12 @@ don't run them.

## Edit panel events

`onEditOpen` and `onEditClose` fire when a row, column or field's edit panel opens or closes, with
`detail: { component, componentType, componentId }`. The `formeoEditOpened` and `formeoEditClosed` DOM events carry
the same `detail`; they're dispatched on the component's element and bubble to `document`. They only fire when the
panel actually opens or closes, can't be cancelled, and don't fire when a component is removed with its panel open.
`onEditOpen` and `onEditClose` fire when a stage, row, column or field's edit panel opens or closes, with
`detail: { component, componentType, componentId }`; `componentType` is `'stage'`, `'row'`, `'column'` or `'field'`
(with the `pages` option, a stage's panel is a page's settings). The `formeoEditOpened` and `formeoEditClosed` DOM
events carry the same `detail`; they're dispatched on the component's element and bubble to `document`. They only
fire when the panel actually opens or closes, can't be cancelled, and don't fire when a component is removed with
its panel open.
formBuilder's `onOpenFieldEdit`/`onCloseFieldEdit` map to these, filtered to `componentType === 'field'`.

```javascript
Expand Down
122 changes: 122 additions & 0 deletions docs/typescript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# TypeScript

Formeo ships hand-written type definitions: `dist/formeo.d.ts` for the ES module and UMD builds, and
`dist/formeo.d.cts` for the CommonJS build. `package.json` points `types` and every `exports` condition at them, so
`import { FormeoEditor } from 'formeo'` is typed with `moduleResolution` `bundler` or `node16`/`nodenext` (from ES
modules or CommonJS). There is nothing extra to install. Projects still on `moduleResolution: node` (`node10`) get the
types through the top-level `types` field on TypeScript 6 or older; TypeScript 7 removed that mode, so switch to
`bundler` or `nodenext` there.

Requires TypeScript 4.7+ (the `exports` conditions need `moduleResolution: node16`/`nodenext`, which 4.7 added;
`moduleResolution: bundler` needs 5.0+). Your `lib` needs `"dom"` — without it, most of this file's types don't
resolve (`HTMLElement`, `CustomEvent`, and the rest), and with the common `skipLibCheck: true` those errors are
silently skipped because they'd be reported inside a `.d.ts` file; set `skipLibCheck: false` at least once to check.

## What is typed

- `FormeoEditor` and `FormeoRenderer`: constructors, properties and methods, including `destroy()`, `editor.pages`
and the renderer's `page` and `pageCount`. See [Editor](editor/README.md) and [Renderer](renderer/renderer.md).
- Every editor option ([Options](options/README.md)), including `config`, `controls`, `i18n`, `events` and `actions`.
- Every renderer option, including `pagination` and `events`.
- The form definition, `FormeoFormData`, with its conditions. It matches `dist/formData_schema.json`.
- Formeo's DOM events on `document`, so `evt.detail` is typed in `document.addEventListener('formeoBeforeSave', …)`.

## Before hooks narrow on `componentType`

The `detail` of `onBeforeAdd`, `onBeforeRemove` and the other [before hooks](options/events/README.md#before-hooks)
depends on what is being added or removed. Check `componentType` before reading keys only some of them have:

```typescript
import { FormeoEditor } from 'formeo'

new FormeoEditor({
editorContainer: '#formeo-editor',
events: {
onBeforeAdd: ({ detail }) => {
if (detail.componentType === 'stage') {
return detail.index < 5 // at most five pages
}
return detail.controlId !== 'hidden-input'
},
onBeforeRemove: async ({ detail }) => {
if (detail.componentType === 'stage') {
return detail.isEmpty || window.confirm(`Remove "${detail.title}"?`)
}
return true
},
},
})
```

A hook cancels by returning `false`, or a Promise that resolves to `false`. Returning anything else, or nothing,
lets the change happen.

## DOM events

```typescript
document.addEventListener('formeoBeforeSave', evt => {
if (Object.keys(evt.detail.formData.fields).length === 0) {
evt.preventDefault()
}
})
```

## Control sets

`ControlDefinition` also covers [control sets](controls/custom-controls.md#control-sets). A set has no `tag` of its
own, and `onBeforeAdd` reports it as `componentType: 'controlSet'` with `data: { layout, row, fields }`:

```typescript
import type { ControlDefinition } from 'formeo'

const addressSet: ControlDefinition = {
meta: { group: 'common', id: 'address-set', icon: 'rows' },
config: { label: 'Address' },
controlSet: {
layout: 'stacked',
fields: [
{ control: 'text-input', attrs: { name: 'street' }, config: { label: 'Street' } },
{ control: 'text-input', attrs: { name: 'city' }, config: { label: 'City' } },
],
},
}
```

## Older names

`FormeoOptions` and `FormData` are deprecated aliases of `FormeoEditorOptions` and `FormeoFormData`. Importing
`FormData` from `formeo` hides the browser's `FormData` in that file, so prefer `FormeoFormData`.

## Script tags

With the UMD build, `window.FormeoEditor` and `window.FormeoRenderer` are typed once the definitions are part of your
program, for example with `/// <reference types="formeo" />` in one file.

## What isn't typed

- Formeo's internals: the component stores behind `editor.Components`, `editor.events` and `editor.actions`. The
stages, rows, columns and fields that events and actions hand you are typed as `FormeoComponent`, which only lists
their public part (`id`, `name`, `data`, `dom`, `parent`, `children`, `get`, `set`, `remove`, `addChild`).
- The `Control` base class isn't published (see
[Or subclass `Control`](controls/custom-controls.md#or-subclass-control)), so `ControlClass` only helps controls
built from a source checkout.

## Replacing your own declarations

If your project has its own `declare module 'formeo'` block, delete it. What happens if you don't depends on where
it lives and on `skipLibCheck`:

- In a **module file** (one with its own `import`/`export`) with the common `skipLibCheck: true`, it merges with the
shipped types silently — the shipped declaration wins, and any error shows up where your code reads a member only
your shim declared, not where the shim is. With `skipLibCheck: false` it's a hard clash (TS2300 duplicate
identifier) at both declarations.
- In a **global file** (no `import`/`export`) as a bodyless shorthand — `declare module 'formeo'` with no `{ }` —
it replaces the shipped types with `any`, regardless of `skipLibCheck`.

If you declared `Window.FormeoEditor` or `FormeoRenderer` yourself, delete it: it now conflicts with formeo's own
declaration. Depending on where it lives and your `skipLibCheck` setting, the conflict shows up as TS2717, as errors
elsewhere in your code, or by silently replacing formeo's type.

Since formeo's public surface used to be untyped (`any`) everywhere, removing a shim like this — or just upgrading
past a version that lacked these types — can surface type errors in code that compiled before, now that TypeScript
is actually checking it.
2 changes: 2 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pre-push:
commands:
run-tests:
run: npm test
type-check:
run: npm run test:types

pre-commit:
parallel: true
Expand Down
Loading
Loading