From 85c1a3525cc6a94f24ac4361e30c30286f07075a Mon Sep 17 00:00:00 2001 From: Kevin Chappell Date: Mon, 28 Sep 2026 20:35:07 +0100 Subject: [PATCH 1/9] docs(controls): document controls.panels.displayType Refs #151 --- docs/options/controls/README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/options/controls/README.md b/docs/options/controls/README.md index f6f01b10..60dadc24 100644 --- a/docs/options/controls/README.md +++ b/docs/options/controls/README.md @@ -11,6 +11,7 @@ Control options can be used to disable, extend and modify the Formeo's control p | [groups](#groups) | Array | define custom [control groups](../../controls/#control-groups) beyond the default 3 | [see below](#groups) | `[]` | | [groupOrder](#grouporder) | Array | set order of [control groups](../../controls/#control-groups) | `['html', 'layout']` | `['common', 'html', 'layout']` | | [ghostPreview](#ghostpreview) | Boolean | use a live preview of the control when dragging | `true` | `false` | +| [panels](#panels) | Object | how the control groups are shown: `'slider'` (arrows), `'tabbed'` or `'auto'` | `{ displayType: 'tabbed' }` | `{ displayType: 'slider' }` | ## sortable @@ -153,6 +154,32 @@ const formeoOptions = { const formeo = new FormeoEditor(formeoOptions) ``` +## panels + +Control how the control groups (layout, common, html, ...) are shown in the control panel. + +```javascript +const controlOptions = { + panels: { displayType: 'tabbed' }, +} + +const formeoOptions = { + controls: controlOptions, +} + +const formeo = new FormeoEditor(formeoOptions) +``` + +`displayType` accepts: + +- `'slider'` (default) - one group is shown at a time, with next/previous arrows to page through the rest. +- `'tabbed'` - every group's label is shown as a tab, and clicking a tab switches to it. +- `'auto'` - switches between `'tabbed'` and `'slider'` based on the control panel's width, using a `390px` breakpoint (`'tabbed'` above it, `'slider'` at or below it). It's re-evaluated whenever the panel is resized. + +Any other value is treated as `'slider'`. + +This only affects the control panel's groups. Field edit panels (the per-field property editor) always use `'auto'` and aren't configurable. + ## ghostPreview Show a live preview of the field instead of the control button while a control is being dragged onto the stage. From cf38c13f69f7362472863ab3356c858252abab39 Mon Sep 17 00:00:00 2001 From: Kevin Chappell Date: Mon, 28 Sep 2026 20:35:12 +0100 Subject: [PATCH 2/9] docs(renderer): show ESM, CommonJS and UMD imports Refs #222 --- README.md | 2 ++ docs/renderer/renderer.md | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/README.md b/README.md index 22c205ab..d8637ee2 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ For quick prototyping or simple projects, you can use a CDN: ``` +This exposes the `window.FormeoEditor` and `window.FormeoRenderer` globals. For production, pin a version (e.g. `formeo@5.13.0`) instead of `@latest` and add an `integrity` hash with `crossorigin="anonymous"` — see [Installation](docs/renderer/renderer.md#installation) in the renderer docs. + ## Usage ### Basic Setup diff --git a/docs/renderer/renderer.md b/docs/renderer/renderer.md index 6c2c9d96..54daa303 100644 --- a/docs/renderer/renderer.md +++ b/docs/renderer/renderer.md @@ -17,10 +17,35 @@ The `FormeoRenderer` class is responsible for rendering Formeo form data into in ## Installation +Formeo ships ESM, CommonJS and UMD builds. Pick the one that matches how you load it. + +### ESM + ```javascript import { FormeoRenderer } from 'formeo' +import 'formeo/dist/formeo.min.css' +``` + +### CommonJS + +```javascript +const { FormeoRenderer } = require('formeo') ``` +This works in Node from the release that ships the `.cjs` build (5.9.1+). Formeo renders in a browser, so in Node this is for bundlers and SSR imports, not for calling `render()` server-side. + +### UMD (CDN, no build step) + +```html + +``` + +Pin a version instead of `@latest` so the hash stays valid, and add the matching `integrity` hash (shown on unpkg's file listing for that version) with `crossorigin="anonymous"`. This exposes `window.FormeoRenderer` (and `window.FormeoEditor`) globally. + ## Basic Usage ```javascript From 1d735c7441aa0a139826958ca5be2b780ab13414 Mon Sep 17 00:00:00 2001 From: Kevin Chappell Date: Mon, 28 Sep 2026 20:40:19 +0100 Subject: [PATCH 3/9] docs(angular): replace the design note with a working standalone component docs/angular-integration.md was an internal design note ("For your use case, I recommend...") with no component code, dead Angular UMD CDN URLs (Angular dropped UMD in v13), and an unverified StackBlitz link. Replace it with a copy-pasteable Angular 17+ standalone component, verified against 5.13.0: install/styles setup, a component built with afterNextRender that creates FormeoEditor/FormeoRenderer only in the browser, a Preview button that renders editor.formData, cleanup via editor.destroy()/renderer.destroy(), reading submitted answers via events.onSubmit, an SSR note, and a note that formeo now ships its own .d.ts/.d.cts (no declare module needed). docs/angular-integration-example/ held only a README pointing at demo internals with no runnable project; delete it and fix the stale references to it in the demo file and README. Refs #184 --- README.md | 2 +- docs/angular-integration-example/README.md | 11 - docs/angular-integration.md | 226 ++++++++++----------- src/demo/js/frameworks/angular.js | 7 +- 4 files changed, 116 insertions(+), 130 deletions(-) delete mode 100644 docs/angular-integration-example/README.md diff --git a/README.md b/README.md index d8637ee2..7eaaa754 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ editor.whenReady().then(() => { Formeo can be integrated with popular frontend frameworks: - **[React Integration Guide](https://github.com/Draggable/formeo/blob/main/docs/react-integration.md)** - Custom hooks, functional components, and Context API patterns -- **[Angular Integration Guide](https://github.com/Draggable/formeo/blob/main/docs/angular-integration.md)** - Services, components, and standalone patterns +- **[Angular Integration Guide](https://github.com/Draggable/formeo/blob/main/docs/angular-integration.md)** - A standalone component, SSR-safe setup, and reading submitted answers ## Theming diff --git a/docs/angular-integration-example/README.md b/docs/angular-integration-example/README.md deleted file mode 100644 index 23fe94aa..00000000 --- a/docs/angular-integration-example/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Angular + Formeo Integration Example - -This directory holds only this README — there is no runnable Angular project here. - -The code samples referenced below (a standalone component, an injectable service, and module/config -wiring) live inline in `src/demo/js/frameworks/angular.js`, which the demo's "Angular" tab displays and -syntax-highlights. That file also explains why the demo simulates Angular patterns instead of loading -the real framework. - -For the full picture — the reasoning behind the simulation, and how to build a real Angular project -against Formeo — see [`docs/angular-integration.md`](../angular-integration.md). diff --git a/docs/angular-integration.md b/docs/angular-integration.md index 5357f898..541703d3 100644 --- a/docs/angular-integration.md +++ b/docs/angular-integration.md @@ -1,145 +1,143 @@ -# Angular + Formeo Integration Guide +# Angular + Formeo Integration -## Why the Demo Doesn't Load Real Angular v20 +A standalone Angular component that builds a form with `FormeoEditor`, previews it with `FormeoRenderer`, and +cleans up both on destroy. Verified against `formeo` 5.13.0; Angular 17+ syntax (standalone components, +`afterNextRender`). -The current Angular demo in `src/demo/js/frameworks/angular.js` simulates Angular patterns rather than loading the actual Angular framework for several technical reasons: - -### Technical Limitations - -1. **Dynamic ES Module Loading Complexity** - - Angular v20 requires complex module resolution - - Dependencies between @angular/core, @angular/common, @angular/platform-browser - - Browser ES module loading limitations - -2. **Bundle Size Impact** - - Angular v20 with dependencies is ~200KB+ minified - - Would significantly slow down demo loading - - Not suitable for quick demonstrations - -3. **Build Tool Requirements** - - Angular typically requires compilation (TypeScript → JavaScript) - - Template compilation and bundling - - Zone.js and other runtime dependencies - -4. **Runtime Environment Mismatch** - - Demo runs in vanilla browser environment - - Angular expects Node.js tooling ecosystem - -## Recommended Alternatives - -### Option 1: Complete Angular Project Example (Recommended) - -Create a separate Angular project that demonstrates real integration: +## Install ```bash -# Create new Angular project -ng new formeo-angular-example --routing --style=scss -cd formeo-angular-example - -# Install Formeo npm install formeo - -# Run development server -ng serve ``` -**Benefits:** -- Real Angular v20 environment -- Proper TypeScript support -- Full Angular CLI tooling -- Production-ready patterns - -### Option 2: Improved CDN Loading (Experimental) - -Update the demo to use Angular's standalone CDN bundles: - -```javascript -// Load Angular via CDN (Angular v20 standalone) -async function loadAngularFromCDN() { - const scripts = [ - 'https://unpkg.com/@angular/core@20/bundles/core.umd.js', - 'https://unpkg.com/@angular/common@20/bundles/common.umd.js', - 'https://unpkg.com/@angular/platform-browser@20/bundles/platform-browser.umd.js' - ]; - - for (const src of scripts) { - await loadScript(src); +Add the stylesheet in `angular.json`: + +```json +{ + "projects": { + "my-app": { + "architect": { + "build": { + "options": { + "styles": ["node_modules/formeo/dist/formeo.min.css", "src/styles.scss"] + } + } + } + } } } ``` -**Limitations:** -- Still complex dependency management -- Large download size -- Limited TypeScript support -- Not recommended for production +(Or `@import 'formeo/dist/formeo.min.css';` from a global stylesheet instead, if you'd rather not touch +`angular.json`.) -### Option 3: Web Components Approach - -Use Angular Elements to create a web component: +## The component ```typescript -// Create Angular Element -import { createCustomElement } from '@angular/elements'; -import { FormBuilderComponent } from './form-builder.component'; +import { Component, ElementRef, OnDestroy, ViewChild, afterNextRender } from '@angular/core' +import { FormeoEditor, FormeoRenderer } from 'formeo' + +@Component({ + selector: 'app-form-builder', + standalone: true, + template: ` +
+ +
+ `, +}) +export class FormBuilderComponent implements OnDestroy { + @ViewChild('editorRef', { static: true }) editorRef!: ElementRef + @ViewChild('previewRef', { static: true }) previewRef!: ElementRef + + private editor?: FormeoEditor + private renderer?: FormeoRenderer + + constructor() { + // afterNextRender only runs in the browser (see "Server-side rendering" below), after the view + // holding editorRef/previewRef has rendered. + afterNextRender(() => { + this.editor = new FormeoEditor({ editorContainer: this.editorRef.nativeElement }) + this.renderer = new FormeoRenderer({ + renderContainer: this.previewRef.nativeElement, + events: { + onSubmit: ({ event, userData }) => { + event.preventDefault() // formeo doesn't call this for you + console.log('submitted:', userData) + }, + }, + }) + }) + } + + preview(): void { + this.renderer?.render(this.editor?.formData) + } -const FormBuilderElement = createCustomElement(FormBuilderComponent, { injector }); -customElements.define('formeo-angular-builder', FormBuilderElement); + ngOnDestroy(): void { + this.editor?.destroy() + this.renderer?.destroy() + } +} ``` -**Benefits:** -- Can be loaded in any environment -- Encapsulated Angular functionality -- Framework-agnostic usage +Notes: -### Option 4: Stackblitz/CodeSandbox Integration +- `editorContainer`/`renderContainer` take an `Element` directly, so the `@ViewChild` refs' `.nativeElement` + work without a selector string. +- `editor.formData` is a getter — it always returns the editor's current form definition, so `preview()` just + reads it and hands it to `renderer.render()`. +- `editor.destroy()` and `renderer.destroy()` both exist and are safe to call more than once; call them in + `ngOnDestroy` so a routed-away component doesn't leak Sortable instances or DOM listeners. -Embed live Angular examples using online IDEs: +## Reading submitted answers -```html - -``` +The renderer's `onSubmit` event fires on the rendered `
`'s native `submit` event and hands you +`{ event, form, userData }`, where `userData` is the form's values keyed by field name (repeated names — checkbox +groups, multi-selects — become arrays). Formeo does not call `event.preventDefault()` itself, so call it yourself +if you're not doing a normal form POST: -## Current Demo Value +```typescript +events: { + onSubmit: ({ event, userData }) => { + event.preventDefault() + this.http.post('/api/submissions', userData).subscribe() + }, +} +``` -The current simulated demo is still valuable because it: +## Server-side rendering (SSR) -1. **Shows Integration Patterns**: Demonstrates proper Angular component structure -2. **Provides Copy-Paste Code**: Complete, working examples for real projects -3. **Educational Value**: Teaches Angular + Formeo integration concepts -4. **Performance**: Loads quickly without framework overhead +`FormeoEditor` and `FormeoRenderer` both touch `document`, so only construct them in the browser. `afterNextRender` +(used above) already guarantees this — Angular's server renderer never invokes its callback, so no extra guard is +needed. -## Implementation Recommendation +If you're not using `afterNextRender` (e.g. an older Angular version, or constructing Formeo somewhere other than +a component's injection context), guard the same code with `isPlatformBrowser`: -For your use case, I recommend: +```typescript +import { Component, PLATFORM_ID, inject } from '@angular/core' +import { isPlatformBrowser } from '@angular/common' -1. **Keep the current demo** for quick pattern demonstration -2. **Add a complete Angular project** in `/docs/angular-integration-example/` -3. **Update the demo documentation** to clearly explain the simulation -4. **Add links to the full project** for developers who want real integration +export class FormBuilderComponent { + private platformId = inject(PLATFORM_ID) -This approach provides both quick learning (demo) and complete implementation (separate project). + ngAfterViewInit(): void { + if (isPlatformBrowser(this.platformId)) { + this.editor = new FormeoEditor({ editorContainer: this.editorRef.nativeElement }) + } + } +} +``` -## Migration Path +## TypeScript -If you want to enhance the current demo: +Formeo ships its own type definitions (`dist/formeo.d.ts`, `dist/formeo.d.cts`) — nothing extra to install or +declare. See [TypeScript](typescript.md) for what's typed, including `FormeoRendererEvents['onSubmit']` and the +`UserData` type used above. -```javascript -// Enhanced demo with better Angular simulation -export async function loadAngularDemo(container) { - // 1. Show comprehensive setup instructions - // 2. Provide modern Angular v20 code examples - // 3. Demonstrate advanced patterns (signals, standalone components) - // 4. Include service-based architecture examples - // 5. Add TypeScript type definitions -} -``` +## The demo's Angular tab -The updated demo now includes: -- Modern Angular v20 patterns -- Standalone components -- Angular signals -- Service-based architecture -- Better TypeScript examples -- Comprehensive integration guide +The demo (`npm start`, then the Angular tab) shows the same construction pattern hand-rolled against plain DOM, +without loading Angular itself — see `src/demo/js/frameworks/angular.js`. It's there to demonstrate the pattern +quickly in the browser, not as a real Angular app; use the component above for that. diff --git a/src/demo/js/frameworks/angular.js b/src/demo/js/frameworks/angular.js index fccff281..8d643fb7 100644 --- a/src/demo/js/frameworks/angular.js +++ b/src/demo/js/frameworks/angular.js @@ -6,8 +6,7 @@ import { editorOptions, renderOptions } from '../options/index.js' * * Note: This demo simulates Angular patterns rather than loading the full framework * due to browser compatibility and performance considerations. For a real Angular - * integration, see the complete Angular project example in /docs/angular-integration-example/ - * Full integration to come in codesandbox.io/angular example in future. + * integration, see docs/angular-integration.md. */ export async function loadAngularDemo(container) { // Show loading state while demo initializes @@ -38,8 +37,8 @@ export async function loadAngularDemo(container) {

This demo shows how to integrate Formeo with Angular applications using modern patterns.

- Note: This is a pattern demonstration. For a complete Angular v20 project with Formeo, - see /docs/angular-integration-example/ in the repository. + Note: This is a pattern demonstration. For a complete Angular integration guide, + see docs/angular-integration.md in the repository.
From 4b8ff8fdc50ed3e45a48aaeb79884611fe06f00e Mon Sep 17 00:00:00 2001 From: Kevin Chappell Date: Mon, 28 Sep 2026 20:40:31 +0100 Subject: [PATCH 4/9] fix(demo): call editor.load(), not editor.render(), in the Angular service snippet editor.render() takes no arguments; loading a saved formData into the editor is editor.load(formData) (src/lib/js/editor.js). The display-only TypeScript snippet in the Angular demo's FormeoService.loadForm() was passing formData to render(), which silently ignores it. Refs #184 --- src/demo/js/frameworks/angular.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/demo/js/frameworks/angular.js b/src/demo/js/frameworks/angular.js index 8d643fb7..b7652a52 100644 --- a/src/demo/js/frameworks/angular.js +++ b/src/demo/js/frameworks/angular.js @@ -714,7 +714,7 @@ export class FormeoService { loadForm(containerId: string, formData: any): void { const editor = this.getEditor(containerId); if (editor && formData) { - editor.render(formData); + editor.load(formData); this.updateFormData(formData); } } From 9282ac0552c7d3e248d5712df34ff3d9ca551a96 Mon Sep 17 00:00:00 2001 From: Kevin Chappell Date: Mon, 28 Sep 2026 20:49:40 +0100 Subject: [PATCH 5/9] docs(controls): add a rich text editor (CKEditor 5) recipe Refs #340 --- docs/controls/README.md | 3 +- docs/controls/custom-controls.md | 1 + docs/controls/rich-text-editors.md | 119 +++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 docs/controls/rich-text-editors.md diff --git a/docs/controls/README.md b/docs/controls/README.md index 429dbef2..7690f41e 100644 --- a/docs/controls/README.md +++ b/docs/controls/README.md @@ -26,7 +26,8 @@ const inputElement = { ## [Control Options](https://github.com/Draggable/formeo/tree/main/docs/options/controls) -See [Custom controls](custom-controls.md) for a full editor + renderer example. +See [Custom controls](custom-controls.md) for a full editor + renderer example, or +[Rich Text Editors](rich-text-editors.md) for a worked example wiring in CKEditor 5. ## Control Groups diff --git a/docs/controls/custom-controls.md b/docs/controls/custom-controls.md index 13c2c02c..e466acb9 100644 --- a/docs/controls/custom-controls.md +++ b/docs/controls/custom-controls.md @@ -208,3 +208,4 @@ would throw on. - [Custom Attribute Types](custom-attribute-types.md) - Attribute input types for a control's `attrs` - [Renderer: Custom Elements](../renderer/renderer.md#advanced-topics) - The renderer's `elements` option - [Control sets](#control-sets) - Add several fields at once +- [Rich Text Editors](rich-text-editors.md) - A full worked example: a CKEditor 5 control diff --git a/docs/controls/rich-text-editors.md b/docs/controls/rich-text-editors.md new file mode 100644 index 00000000..8209d1dd --- /dev/null +++ b/docs/controls/rich-text-editors.md @@ -0,0 +1,119 @@ +# Rich Text Editors + +A recipe for wiring a modern WYSIWYG editor into Formeo as a custom control, using +[CKEditor 5](https://ckeditor.com/ckeditor-5/) as the example. It builds on +[Custom Controls](custom-controls.md) — read that first for the full picture (defining a control, registering it, +rendering it, and getting its value into `userData`). This page only covers what's specific to a CDN-loaded rich +text editor: waiting for the script to load before initializing, and syncing the editor's HTML back to the +`