form-page-builder 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ Embeddable, bilingual (EN/JA by default, extensible), drag-and-drop form builder
10
10
 
11
11
  ## Examples
12
12
 
13
- The [live demo](https://mainsolutioncoltd.github.io/form-page-builder/) is a gallery of six `<FormBuilder />` configurations, each showing a different realistic way to compose `features`/`theme`/`storage`/`initialDocument`/`language` — jump straight to one. Its sticky header (its headings/captions, not the widget's own EN/JA switcher) has an EN/日本語 toggle, a Light/Dark toggle that swaps every example except "Branded" (badged "Fixed theme", since its whole point is a locked theme) between `DEFAULT_THEME` and `DARK_THEME`, and links back to this repo and the npm package.
13
+ The [live demo](https://mainsolutioncoltd.github.io/form-page-builder/) is a gallery of `<FormBuilder />` configurations, each showing a different realistic way to compose `features`/`theme`/`storage`/`initialDocument`/`language` — jump straight to one. Its sticky header (its headings/captions, not the widget's own EN/JA switcher) has an EN/日本語 toggle, a Light/Dark toggle that swaps every example except "Branded" (badged "Fixed theme", since its whole point is a locked theme) between `DEFAULT_THEME` and `DARK_THEME`, and links back to this repo and the npm package.
14
14
 
15
15
  | Example | What it shows |
16
16
  |---|---|
@@ -20,8 +20,9 @@ The [live demo](https://mainsolutioncoltd.github.io/form-page-builder/) is a gal
20
20
  | [Survey builder](https://mainsolutioncoltd.github.io/form-page-builder/#survey) | `fieldTypes`/`contentBlocks` allowlists plus `onSubmit` — a single-shape survey embed that hands you the answers. |
21
21
  | [Locked-structure form](https://mainsolutioncoltd.github.io/form-page-builder/#locked-structure) | `initialDocument` seeds a fixed field set; structural toggles are off so only styling/labels stay editable. |
22
22
  | [Localized (French)](https://mainsolutioncoltd.github.io/form-page-builder/#localized) | `language`/`languages`/`strings`/`chrome` — a language beyond the built-in EN/JA, partially translated. |
23
+ | [Template roles](https://mainsolutioncoltd.github.io/form-page-builder/#template-roles) | Two instances sharing one template library — `templates: { manage: true, max: 3 }` (curate) next to `{ manage: false }` (apply-only), plus copy/paste between them. |
23
24
 
24
- The source for all six lives in [dev/main.tsx](./dev/main.tsx) — `npm run dev` runs the same gallery locally against `src/` directly, and is the fastest way to try a `features`/`theme` combination before wiring it into your app.
25
+ The source for all of them lives in [dev/main.tsx](./dev/main.tsx) — `npm run dev` runs the same gallery locally against `src/` directly, and is the fastest way to try a `features`/`theme` combination before wiring it into your app.
25
26
 
26
27
  **This is a builder + viewer, not a data handler.** It builds and previews a JSON *schema* describing a form's fields, sections, and layout blocks (including plain content blocks like paragraphs and images, not just inputs). Preview mode's "Submit" validates and shows a mock "here's what would be sent to your backend" modal, and — if you pass `onSubmit` — hands you the entered values too; either way, this package never sends or stores them itself. The only thing it persists on its own is the *builder's own* draft/Templates state (via the pluggable `StorageAdapter` below); actually delivering submissions to a backend is up to the host app.
27
28
 
@@ -72,8 +73,10 @@ Above ~720px wide, Build mode is the usual three-column Palette/Canvas/Inspector
72
73
  | `initialDocument` | `FormDocument` | Seeds the builder with this document on mount instead of the autosaved draft — see "Programmatic integration" below. |
73
74
  | `initialMode` | `"build" \| "preview"` | Which mode the widget mounts into (default `"build"`). Pair with `features.previewMode: false` to lock a consumer to just one mode with no tabs — e.g. a fill-only embed that never shows the Build canvas. Or leave `previewMode` on and pick `initialMode` per document (e.g. `"preview"` once a document already has saved data) so the widget opens on the right mode while still letting the built-in tabs switch it. |
74
75
  | `onModeChange` | `(mode: "build" \| "preview") => void` | Fires on mount and on every Build/Preview toggle. Lets a host mirror the current mode (e.g. to show its own Save button only in Build mode) without building a separate tab UI around the widget. |
76
+ | `onTemplateChange` | `(change: TemplateChange) => void` | Fires when a template is created (`source: "new"`), overwritten (`"saved"`), applied as the working document (`"applied"`), or deleted (`"deleted"`). `change` is `{ id: string \| null; title: string; source }`. Useful for syncing a host's own state or backend index. |
77
+ | `templateClipboardKey` | `string` | Overrides the localStorage key behind "Copy template" / "Paste template" (default `"form-page-builder:clipboard"`). Instances sharing a key can copy/paste between each other. |
75
78
 
76
- A `ref` on `<FormBuilder />` gives you a `FormBuilderHandle` (`getDocument()` / `loadDocument()` / `exportJson()`) — see "Programmatic integration" below.
79
+ A `ref` on `<FormBuilder />` gives you a `FormBuilderHandle` (`getDocument()` / `loadDocument()` / `exportJson()` / `getTemplate()` / `loadTemplate()`) — see "Programmatic integration" and "Copy / paste templates" below.
77
80
 
78
81
  ### Features: `features`
79
82
 
@@ -104,23 +107,40 @@ Every optional UI surface can be switched on or off independently through one `f
104
107
  | Key | Type | Default | Controls |
105
108
  |---|---|---|---|
106
109
  | `naming` | `boolean` | `true` | The editable form-title input in the toolbar. |
107
- | `templates` | `boolean` | `true` | The Templates library (browse/open/delete) and the "Save" button. |
110
+ | `formTitle` | `boolean` | `false` | Renders the form title as an `<h2>` heading above the fields in Preview. Off by default — the host app usually shows the form's name in its own chrome, so leave this off to avoid a duplicate. |
111
+ | `templates` | `boolean \| { manage?: boolean; max?: number }` | `true` | The Templates library and the "Save" button. `true` = full library (browse / apply / save / overwrite / delete). `false` = no template UI. `{ manage: false }` = pick-and-apply only: the library lists templates and the user can apply one as a starting point, but can't create, overwrite, or delete them and the "Save" button is hidden. `{ max: n }` caps how many templates can be stored (default 5). |
108
112
  | `newForm` | `boolean` | `true` | The "New Form" reset button. |
109
113
  | `autosave` | `boolean` | `true` | Autosaving the draft to `storage`. The initial draft *load* always happens regardless — this only gates the write path. |
110
114
  | `jsonView` | `boolean` | `true` | The "View JSON" button and modal. |
115
+ | `templateClipboard` | `boolean` | `true` | The "Copy template" / "Paste template" icon buttons next to "View JSON". Paste is enabled only once another builder instance in the same browser has copied one. |
111
116
  | `previewMode` | `boolean` | `true` | The Build/Preview tabs. When `false`, the tabs are hidden and the builder stays in whichever mode it started in (`initialMode`, Build by default). |
112
117
  | `languageSwitcher` | `boolean` | `true` | The language-switcher pill in the toolbar. |
113
118
  | `design` | `boolean` | `false` | The global "Design" tab (colors/spacing), i.e. the old `themeEditable` prop. |
114
119
  | `blockStyling` | `boolean` | `true` | Per-field styling controls: paragraph heading/font/color, button color — independent of `design`. |
115
120
  | `contentBlocks` | `boolean \| ("paragraph" \| "image" \| "button")[]` | `true` | Which content blocks can be *added* from the palette. `true` = all, `false` = none, or an allowlist. |
116
121
  | `fieldTypes` | `boolean \| ("input" \| "textarea" \| "select" \| "radio" \| "checkboxGroup" \| "checkbox" \| "toggle")[]` | `true` | Which form field types can be *added* from the palette. `true` = all, `false` = none, or an allowlist. |
117
- | `sections` | `boolean` | `true` | Add/duplicate/move/delete-section controls and the section background picker. |
122
+ | `sections` | `boolean` | `true` | Add/duplicate/move/delete-section controls and the "Add section" button. |
123
+ | `sectionBackground` | `boolean` | inherits `sections` | Per-section background-color swatches + custom-color picker in the section header. Independent of `sections` — set it `false` to lock section backgrounds while keeping the structural controls, or leave it unset and it follows whatever `sections` is. |
118
124
  | `dragReorder` | `boolean` | `true` | Drag-to-reorder fields within a section. |
119
125
  | `deviceToggle` | `boolean` | `true` | The Laptop/Tablet/Mobile width switcher above the Preview canvas. When `false`, Preview always renders at the Laptop (full) width. |
120
126
  | `maxFields` | `number` | `undefined` (unlimited) | Caps the total number of input-type fields (not content blocks) addable across the whole document. Once reached, the Form Fields palette buttons disable until a field is removed. |
121
127
 
122
128
  Disabling `contentBlocks`/`fieldTypes` for a given type only hides it from the palette going forward — if a document loaded via `initialDocument` (or a saved template) already contains fields of a now-disabled type, they still render and remain editable in Build mode; nothing is stripped or hidden.
123
129
 
130
+ ### Styling hooks
131
+
132
+ The widget styles itself with inline styles and CSS custom properties (see theming below), but the rendered form also carries stable, rule-free class names so a host stylesheet can target it:
133
+
134
+ | Class | On |
135
+ |---|---|
136
+ | `.fb-form` | The Preview form container. |
137
+ | `.fb-section` | Each section wrapper (also `data-section-id`). |
138
+ | `.fb-field` | Every field wrapper, in Preview and on the Build canvas (also `data-field-id`). |
139
+ | `.fb-field--<type>` | Same wrapper, by field type — `fb-field--input`, `fb-field--select`, `fb-field--button`, … |
140
+ | `.fb-field--<id>` | Same wrapper, by field id (`fb-field--field_1`). Field ids are sequential (`field_1`, `field_2`, …) and stable across reloads. |
141
+
142
+ These are additive hooks only — the package ships no rules for them, so anything you write wins without `!important`.
143
+
124
144
  ### Features vs. theming
125
145
 
126
146
  `features` and `theme` are separate, composable concerns: `features.design` decides whether the Design tab's color/spacing *controls* are shown at all, while `theme` (and the Design tab, when shown) decide what those colors/spacing values *are*. You can, for instance, pass a fixed `theme` with `features.design` and `features.blockStyling` both `false` to lock a form to your brand's colors with zero styling UI exposed to the builder's user.
@@ -139,7 +159,7 @@ import { FormBuilder, DARK_THEME } from "form-page-builder";
139
159
 
140
160
  ### Persistence: `StorageAdapter`
141
161
 
142
- By default the component autosaves a draft, plus a "Templates" library (up to 5 saved forms, shown via the toolbar's Templates button), to `window.localStorage`. Being local-storage-only means neither persists across devices or browsers. To persist the builder's state to your own backend (a Next.js API route, a PHP endpoint, etc.) instead — so drafts and templates are shared across devices, and your backend can populate/manage the template list itself — implement and pass a `StorageAdapter`. Its `get`/`set`/`delete` calls *are* the create/update/delete hooks: whatever your implementation does inside them (write to a database, call your API) runs on every template save/update/delete, so no separate event callbacks are needed.
162
+ By default the component autosaves a draft, plus a "Templates" library (up to 5 saved forms, shown via the toolbar's Templates button), to `window.localStorage`. Being local-storage-only means neither persists across devices or browsers. To persist the builder's state to your own backend (a Next.js API route, a PHP endpoint, etc.) instead — so drafts and templates are shared across devices, and your backend can populate/manage the template list itself — implement and pass a `StorageAdapter`. Its `get`/`set`/`delete` calls *are* the create/update/delete hooks: whatever your implementation does inside them (write to a database, call your API) runs on every template save/update/delete. `get`/`set` may be async — the Templates modal shows a spinner while a load is in flight and an inline error if the adapter throws or rejects — and `onTemplateChange` (see the props table) fires alongside so a host can keep its own index in sync.
143
163
 
144
164
  ```ts
145
165
  interface StorageAdapter {
@@ -149,23 +169,33 @@ interface StorageAdapter {
149
169
  }
150
170
  ```
151
171
 
172
+ The adapter is called with three kinds of key, exported so you can route them to your own REST endpoints without hardcoding the strings: `DRAFT_KEY` (the autosaved draft), `INDEX_KEY` (the template list — a JSON `SavedFormMeta[]`), and `formKey(id)` (one saved template). `savedFormId(key)` returns the id for a per-template key, else `null`.
173
+
152
174
  ```tsx
153
- import { FormBuilder, type StorageAdapter } from "form-page-builder";
175
+ import {
176
+ FormBuilder, type StorageAdapter,
177
+ DRAFT_KEY, INDEX_KEY, savedFormId,
178
+ } from "form-page-builder";
154
179
 
155
180
  const apiStorage: StorageAdapter = {
156
181
  async get(key) {
157
- const res = await fetch(`/api/form-page-builder/${encodeURIComponent(key)}`);
158
- if (!res.ok) return null;
159
- return (await res.json()).value ?? null;
182
+ if (key === DRAFT_KEY) return localStorage.getItem(key); // keep the draft local
183
+ if (key === INDEX_KEY) {
184
+ const res = await fetch("/api/form-templates");
185
+ return res.ok ? JSON.stringify(await res.json()) : null;
186
+ }
187
+ const id = savedFormId(key);
188
+ const res = await fetch(`/api/form-templates/${id}`);
189
+ return res.ok ? JSON.stringify(await res.json()) : null;
160
190
  },
161
191
  async set(key, value) {
162
- await fetch(`/api/form-page-builder/${encodeURIComponent(key)}`, {
163
- method: "PUT",
164
- body: value,
165
- });
192
+ if (key === DRAFT_KEY) return void localStorage.setItem(key, value);
193
+ if (key === INDEX_KEY) return; // index is re-derived from the records on read
194
+ await fetch(`/api/form-templates/${savedFormId(key)}`, { method: "PUT", body: value });
166
195
  },
167
196
  async delete(key) {
168
- await fetch(`/api/form-page-builder/${encodeURIComponent(key)}`, { method: "DELETE" });
197
+ const id = savedFormId(key);
198
+ if (id) await fetch(`/api/form-templates/${id}`, { method: "DELETE" });
169
199
  },
170
200
  };
171
201
 
@@ -225,6 +255,24 @@ function BuilderPage() {
225
255
  }
226
256
  ```
227
257
 
258
+ ### Copy / paste templates
259
+
260
+ The toolbar's "Copy template" / "Paste template" icons (next to "View JSON", toggle with `features.templateClipboard`) let a user copy the current form from one builder and paste it into another — across pages, tabs, and reloads in the same browser. Copy writes a portable envelope to a localStorage key (`templateClipboardKey`, default `"form-page-builder:clipboard"`); every mounted builder watches that key, so Paste lights up everywhere as soon as something is copied. Paste asks for confirmation, then replaces the working document.
261
+
262
+ The same portable object is available programmatically — use it to hand a template to your own storage, or to move one between builders without the clipboard:
263
+
264
+ ```tsx
265
+ import { FormBuilder, serializeTemplate, parseTemplate, type FormBuilderHandle } from "form-page-builder";
266
+
267
+ // via the ref handle
268
+ const tpl = ref.current.getTemplate(); // { __fpb: "template", v: 1, document }
269
+ ref.current.loadTemplate(tpl); // accepts the object or its JSON string; returns false if unparseable
270
+
271
+ // or the standalone helpers (e.g. to persist to your backend)
272
+ const json = serializeTemplate(ref.current.getDocument());
273
+ const doc = parseTemplate(json); // also accepts bare "View JSON" output; null if not a document
274
+ ```
275
+
228
276
  ## Using in a plain HTML page (no bundler)
229
277
 
230
278
  There's intentionally no UMD/IIFE global-script build (see below), but you don't need a bundler to use this package — a browser-native ES module setup works too, via an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) and a CDN like [esm.sh](https://esm.sh/):