form-page-builder 0.1.0 → 2.0.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/LICENSE +0 -0
- package/README.md +183 -9
- package/dist/index.cjs +1086 -816
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -32
- package/dist/index.d.ts +129 -32
- package/dist/index.js +1083 -814
- package/dist/index.js.map +1 -1
- package/package.json +12 -4
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -8,7 +8,22 @@
|
|
|
8
8
|
|
|
9
9
|
Embeddable, bilingual (EN/JA by default, extensible), drag-and-drop form builder widget for React. Ships a single `<FormBuilder />` component with a Build mode (drag/drop canvas, field inspector) and a Preview mode (responsive, validating runtime form), plus a JSON export of the resulting document.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Examples
|
|
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.
|
|
14
|
+
|
|
15
|
+
| Example | What it shows |
|
|
16
|
+
|---|---|
|
|
17
|
+
| [Full-featured](https://mainsolutioncoltd.github.io/form-page-builder/#full-featured) | No props beyond `features={{ design: true }}` — the default, everything on. |
|
|
18
|
+
| [Minimal (forms-only embed)](https://mainsolutioncoltd.github.io/form-page-builder/#minimal) | Every optional surface off, a 3-type field allowlist — stripped down for embedding inside a larger app's own chrome. |
|
|
19
|
+
| [Branded, theme locked](https://mainsolutioncoltd.github.io/form-page-builder/#branded) | A fixed `theme` plus `features.design`/`blockStyling` off — colors are locked to the brand, no styling UI exposed. |
|
|
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
|
+
| [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
|
+
| [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
|
+
|
|
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
|
+
|
|
26
|
+
**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.
|
|
12
27
|
|
|
13
28
|
## Install
|
|
14
29
|
|
|
@@ -32,21 +47,95 @@ export default function BuilderPage() {
|
|
|
32
47
|
|
|
33
48
|
In Next.js App Router, the package's entry already carries a `"use client"` directive, so it can be imported directly from a Server Component tree without you adding the directive yourself.
|
|
34
49
|
|
|
50
|
+
### Sizing
|
|
51
|
+
|
|
52
|
+
The widget caps itself at the viewport height (`100vh`, upgrading to `100dvh` on browsers that support it) and scrolls its own Palette/Canvas/Inspector panels internally past that — it never grows taller than the space available. Give its wrapping container an explicit height (e.g. `height: "100dvh"` for a full-height layout on mobile, or any fixed/`%` height) if you want it to fill that space; without one, it just sizes to its content and the page scrolls normally, either way with a single scrollbar.
|
|
53
|
+
|
|
54
|
+
If you *do* size a wrapper to exactly `100vh`/`100dvh` and still see the page itself scroll by a few extra pixels, that's almost always the browser's default `<body>` margin (commonly `8px`) adding to that full-viewport height — reset it yourself (`body { margin: 0; }`), same as you would for any other full-height layout; this package doesn't touch your page's global styles.
|
|
55
|
+
|
|
56
|
+
### Responsive layout
|
|
57
|
+
|
|
58
|
+
Above ~720px wide, Build mode is the usual three-column Palette/Canvas/Inspector layout. Below that, Canvas becomes the full-width primary view, and Palette/Inspector become full-bleed drawers that slide over it instead of permanently eating vertical space — a small "Blocks"/"Properties" bar (reusing the same chrome labels as the desktop tabs, no separate i18n) toggles between them. Tapping a block type in the drawer adds it and returns straight to Canvas; tapping a field in Canvas opens the Properties drawer on it automatically. Nothing gets clipped by the widget's own `overflow: hidden`, and there's no horizontal scrolling. This is CSS-only (a media query in the stylesheet the widget injects) with a small bit of state driving which drawer is open, so it responds to the container's width, not a JS-measured breakpoint — useful if you're embedding it in a narrow sidebar on an otherwise-wide page. Modals size themselves as `width: 100%` up to a `max-width` (with padding around the overlay) instead of a fixed pixel width, so they never overflow a narrow viewport either.
|
|
59
|
+
|
|
35
60
|
### Props
|
|
36
61
|
|
|
37
62
|
| Prop | Type | Description |
|
|
38
63
|
|---|---|---|
|
|
39
|
-
| `theme` | `Partial<Theme>` | Override default colors/layout spacing. |
|
|
40
|
-
| `
|
|
64
|
+
| `theme` | `Partial<Theme>` | Override default colors/layout spacing — see "Features vs. theming" below. |
|
|
65
|
+
| `features` | `FormBuilderFeatures` | Independently toggle UI surfaces on/off — full-featured by default. See "Features" below. |
|
|
41
66
|
| `language` | `string` | Initial builder language (defaults to the first entry in `languages`). |
|
|
42
67
|
| `languages` | `{ code: string; label: string }[]` | Language switcher options (default: EN/JA). |
|
|
43
68
|
| `strings` | partial override of runtime/validation strings, keyed by language code | |
|
|
44
69
|
| `chrome` | partial override of builder-UI strings, keyed by language code | |
|
|
45
|
-
| `storage` | `StorageAdapter` | Pluggable persistence backend for the builder's own draft/library — see below. |
|
|
70
|
+
| `storage` | `StorageAdapter` | Pluggable persistence backend for the builder's own draft/Templates library — see below. |
|
|
71
|
+
| `onSubmit` | `(payload: SubmitPayload) => void` | Called when Preview mode's Submit button is clicked and validation passes — see below. |
|
|
72
|
+
| `initialDocument` | `FormDocument` | Seeds the builder with this document on mount instead of the autosaved draft — see "Programmatic integration" below. |
|
|
73
|
+
|
|
74
|
+
A `ref` on `<FormBuilder />` gives you a `FormBuilderHandle` (`getDocument()` / `loadDocument()` / `exportJson()`) — see "Programmatic integration" below.
|
|
75
|
+
|
|
76
|
+
### Features: `features`
|
|
77
|
+
|
|
78
|
+
Every optional UI surface can be switched on or off independently through one `features` prop, kept deliberately separate from `theme`/`ThemeOverrides` (which control *how* things look, not *whether* they appear). Everything defaults to `true` (`design` defaults to `false`, matching the pre-`features` default), so the default `<FormBuilder />` is unchanged and fully featured; pass only the keys you want to restrict.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// A forms-only embed: no title editing, no templates/JSON/preview chrome,
|
|
82
|
+
// no theming UI, and only two field types available to add.
|
|
83
|
+
<FormBuilder
|
|
84
|
+
features={{
|
|
85
|
+
naming: false,
|
|
86
|
+
templates: false,
|
|
87
|
+
newForm: false,
|
|
88
|
+
autosave: false,
|
|
89
|
+
jsonView: false,
|
|
90
|
+
previewMode: false,
|
|
91
|
+
languageSwitcher: false,
|
|
92
|
+
design: false,
|
|
93
|
+
blockStyling: false,
|
|
94
|
+
contentBlocks: false,
|
|
95
|
+
fieldTypes: ["input", "select"],
|
|
96
|
+
sections: false,
|
|
97
|
+
dragReorder: false,
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| Key | Type | Default | Controls |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| `naming` | `boolean` | `true` | The editable form-title input in the toolbar. |
|
|
105
|
+
| `templates` | `boolean` | `true` | The Templates library (browse/open/delete) and the "Save" button. |
|
|
106
|
+
| `newForm` | `boolean` | `true` | The "New Form" reset button. |
|
|
107
|
+
| `autosave` | `boolean` | `true` | Autosaving the draft to `storage`. The initial draft *load* always happens regardless — this only gates the write path. |
|
|
108
|
+
| `jsonView` | `boolean` | `true` | The "View JSON" button and modal. |
|
|
109
|
+
| `previewMode` | `boolean` | `true` | The Build/Preview tabs. When `false`, the builder stays in Build mode and the tabs are hidden. |
|
|
110
|
+
| `languageSwitcher` | `boolean` | `true` | The language-switcher pill in the toolbar. |
|
|
111
|
+
| `design` | `boolean` | `false` | The global "Design" tab (colors/spacing), i.e. the old `themeEditable` prop. |
|
|
112
|
+
| `blockStyling` | `boolean` | `true` | Per-field styling controls: paragraph heading/font/color, button color — independent of `design`. |
|
|
113
|
+
| `contentBlocks` | `boolean \| ("paragraph" \| "image" \| "button")[]` | `true` | Which content blocks can be *added* from the palette. `true` = all, `false` = none, or an allowlist. |
|
|
114
|
+
| `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. |
|
|
115
|
+
| `sections` | `boolean` | `true` | Add/duplicate/move/delete-section controls and the section background picker. |
|
|
116
|
+
| `dragReorder` | `boolean` | `true` | Drag-to-reorder fields within a section. |
|
|
117
|
+
|
|
118
|
+
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.
|
|
119
|
+
|
|
120
|
+
### Features vs. theming
|
|
121
|
+
|
|
122
|
+
`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.
|
|
123
|
+
|
|
124
|
+
### Dark theme
|
|
125
|
+
|
|
126
|
+
Every color in the widget — including modals, toggles, and badges, not just the canvas/toolbar — is driven by the `theme` prop via CSS custom properties, so dark mode is just a different set of color values, no separate "dark mode" flag needed:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { FormBuilder, DARK_THEME } from "form-page-builder";
|
|
130
|
+
|
|
131
|
+
<FormBuilder theme={DARK_THEME} />;
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`DARK_THEME` (and `DEFAULT_THEME`, the light palette used when `theme` is omitted) are both exported as plain `Theme` objects, so you can spread and tweak either one (`{ ...DARK_THEME, primary: "#22c55e" }`) or swap between them at runtime for a user-facing light/dark toggle — see the "Localized (French)"-adjacent light/Dark switch in the [live demo](https://mainsolutioncoltd.github.io/form-page-builder/) for a working example.
|
|
46
135
|
|
|
47
136
|
### Persistence: `StorageAdapter`
|
|
48
137
|
|
|
49
|
-
By default the component autosaves a draft
|
|
138
|
+
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.
|
|
50
139
|
|
|
51
140
|
```ts
|
|
52
141
|
interface StorageAdapter {
|
|
@@ -79,11 +168,93 @@ const apiStorage: StorageAdapter = {
|
|
|
79
168
|
<FormBuilder storage={apiStorage} />;
|
|
80
169
|
```
|
|
81
170
|
|
|
82
|
-
This is separate from — and unrelated to — however you choose to handle real end-user form submissions in your own app; this package doesn't send or receive those.
|
|
171
|
+
This is separate from — and unrelated to — however you choose to handle real end-user form submissions in your own app; this package doesn't send or receive those on its own (see `onSubmit` below if you want Preview mode's Submit button to hand you the entered values).
|
|
83
172
|
|
|
84
|
-
|
|
173
|
+
### Handling submissions: `onSubmit`
|
|
85
174
|
|
|
86
|
-
|
|
175
|
+
A submit action lives on a **Button field** — drag one into a section from the palette (next to Paragraph/Image) and set its "When clicked" mode to Submit, with a scope of either "This section" or "Whole form". There's no document-level submit setting anymore; you can place as many buttons as you like (e.g. a per-section "Next" alongside a final "Submit", or a plain "Open link" CTA button that doesn't submit at all).
|
|
176
|
+
|
|
177
|
+
Clicking a submit-action button validates the fields in its scope and, once they pass, calls `onSubmit` with the entered values — pass this if you want to do something with them (send to your backend, log them, etc.) instead of just seeing the built-in "here's what would be sent" confirmation:
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
import { FormBuilder, type SubmitPayload } from "form-page-builder";
|
|
181
|
+
|
|
182
|
+
function handleSubmit(payload: SubmitPayload) {
|
|
183
|
+
// payload.buttonId: id of the button field that triggered this — use it to
|
|
184
|
+
// branch when a form has more than one submit button (e.g. "save draft" vs "submit")
|
|
185
|
+
// payload.scope: "form" | "section", matching that button's own scope setting
|
|
186
|
+
// payload.all: every field's raw value across the whole form, keyed by field id
|
|
187
|
+
// payload.sections: the same values, broken down section by section
|
|
188
|
+
// payload.values: just whatever was submitted (the whole form, or one section if scope is "section")
|
|
189
|
+
console.log(payload);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
<FormBuilder onSubmit={handleSubmit} />;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Programmatic integration
|
|
196
|
+
|
|
197
|
+
Copying and pasting the "View JSON" output is fine for development, but a production integration usually wants to load and save forms through its own backend in the background instead. Two props/APIs cover that:
|
|
198
|
+
|
|
199
|
+
- `initialDocument` seeds the builder with a document (e.g. one your backend just fetched) instead of the autosaved draft.
|
|
200
|
+
- A ref exposes `getDocument()`, `loadDocument(doc)`, and `exportJson()` so you can pull the current document out (to save it yourself, on whatever schedule/event you choose) or push a new one in, independent of the `storage` autosave path:
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { useEffect, useRef, useState } from "react";
|
|
204
|
+
import { FormBuilder, type FormBuilderHandle, type FormDocument } from "form-page-builder";
|
|
205
|
+
|
|
206
|
+
function BuilderPage() {
|
|
207
|
+
const ref = useRef<FormBuilderHandle>(null);
|
|
208
|
+
const [initialDocument, setInitialDocument] = useState<FormDocument>();
|
|
209
|
+
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
fetch("/api/forms/123").then((r) => r.json()).then(setInitialDocument);
|
|
212
|
+
}, []);
|
|
213
|
+
|
|
214
|
+
async function saveNow() {
|
|
215
|
+
if (!ref.current) return;
|
|
216
|
+
await fetch("/api/forms/123", { method: "PUT", body: ref.current.exportJson() });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!initialDocument) return null;
|
|
220
|
+
return <FormBuilder ref={ref} initialDocument={initialDocument} />;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Using in a plain HTML page (no bundler)
|
|
225
|
+
|
|
226
|
+
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/):
|
|
227
|
+
|
|
228
|
+
```html
|
|
229
|
+
<!DOCTYPE html>
|
|
230
|
+
<html>
|
|
231
|
+
<head>
|
|
232
|
+
<script type="importmap">
|
|
233
|
+
{
|
|
234
|
+
"imports": {
|
|
235
|
+
"react": "https://esm.sh/react@18",
|
|
236
|
+
"react-dom/client": "https://esm.sh/react-dom@18/client",
|
|
237
|
+
"form-page-builder": "https://esm.sh/form-page-builder?external=react,react-dom"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
</script>
|
|
241
|
+
</head>
|
|
242
|
+
<body>
|
|
243
|
+
<div id="root"></div>
|
|
244
|
+
<script type="module">
|
|
245
|
+
import React from "react";
|
|
246
|
+
import { createRoot } from "react-dom/client";
|
|
247
|
+
import { FormBuilder } from "form-page-builder";
|
|
248
|
+
|
|
249
|
+
createRoot(document.getElementById("root")).render(
|
|
250
|
+
React.createElement(FormBuilder, {})
|
|
251
|
+
);
|
|
252
|
+
</script>
|
|
253
|
+
</body>
|
|
254
|
+
</html>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The `?external=react,react-dom` query on the `form-page-builder` import tells esm.sh to reuse the same `react`/`react-dom` module the page already imports, instead of bundling its own copy — so there's no duplicate-React problem, and no build step (Vite/Webpack/etc.) required. Since there's no JSX here, components are created with `React.createElement(...)` instead of `<FormBuilder />`; everything else (props, `storage` adapter, etc.) works the same as in the React/Next.js example above.
|
|
87
258
|
|
|
88
259
|
## Local development
|
|
89
260
|
|
|
@@ -91,10 +262,13 @@ There's no build target for raw PHP output — a React component tree still need
|
|
|
91
262
|
npm install
|
|
92
263
|
npm run dev # Vite dev harness at http://localhost:5173, imports FormBuilder from src/
|
|
93
264
|
npm run typecheck
|
|
265
|
+
npm run test # vitest run -- see tests/
|
|
94
266
|
npm run build # tsup -> dist/ (ESM + CJS + .d.ts), the published package
|
|
95
267
|
npm run build:demo # vite build -> pages-dist/, the GitHub Pages demo site
|
|
96
268
|
```
|
|
97
269
|
|
|
270
|
+
Tests use [Vitest](https://vitest.dev) + [Testing Library](https://testing-library.com/react) against jsdom, covering rendering, the `features` prop's gating of each UI surface, the `FormBuilderHandle` ref API, autosave/`initialDocument`, and the Preview-mode validation + `onSubmit` flow. `npm run test:watch` re-runs on change.
|
|
271
|
+
|
|
98
272
|
## Releasing
|
|
99
273
|
|
|
100
274
|
Versioning and the changelog are managed by [Changesets](https://github.com/changesets/changesets); publishing to npm is a separate, deliberate step.
|
|
@@ -102,6 +276,6 @@ Versioning and the changelog are managed by [Changesets](https://github.com/chan
|
|
|
102
276
|
1. On your PR, describe the change for consumers: `npx changeset` (pick patch/minor/major, write a summary, commit the generated file in `.changeset/`).
|
|
103
277
|
2. Once merged to `main`, a bot opens/updates a **"Version Packages"** PR that bumps `package.json` and writes `CHANGELOG.md` from the accumulated changesets ([.github/workflows/version.yml](.github/workflows/version.yml)).
|
|
104
278
|
3. Merge that PR when you're ready to ship.
|
|
105
|
-
4. Cut a [GitHub Release](https://github.com/MainSolutionCoLtd/form-page-builder/releases/new) tagged `vX.Y.Z` matching the new version — this triggers [.github/workflows/release.yml](.github/workflows/release.yml), which builds and runs `npm publish` (
|
|
279
|
+
4. Cut a [GitHub Release](https://github.com/MainSolutionCoLtd/form-page-builder/releases/new) tagged `vX.Y.Z` matching the new version — this triggers [.github/workflows/release.yml](.github/workflows/release.yml), which builds and runs `npm publish --provenance` via npm's Trusted Publishing (OIDC), so no `NPM_TOKEN` secret is needed.
|
|
106
280
|
|
|
107
281
|
Pushes to `main` also rebuild and redeploy the [live demo](https://mainsolutioncoltd.github.io/form-page-builder/) via [.github/workflows/pages.yml](.github/workflows/pages.yml).
|