@fogpipe/forma-react 0.6.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 ADDED
@@ -0,0 +1,277 @@
1
+ # @fogpipe/forma-react
2
+
3
+ Headless React form renderer for Forma specifications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @fogpipe/forma-core @fogpipe/forma-react
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Headless Architecture** - Bring your own UI components
14
+ - **useForma Hook** - Complete form state management
15
+ - **FormRenderer Component** - Declarative form rendering
16
+ - **Multi-Page Support** - Built-in wizard navigation
17
+ - **Type Safety** - Full TypeScript support
18
+ - **Accessibility** - Built-in ARIA attribute support
19
+ - **Error Boundary** - Graceful error handling for form components
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Define Your Components
24
+
25
+ Components receive `{ field, spec }` props. The `field` object contains all field state and handlers:
26
+
27
+ ```tsx
28
+ import type { ComponentMap, TextComponentProps, BooleanComponentProps } from '@fogpipe/forma-react';
29
+
30
+ const TextInput = ({ field }: TextComponentProps) => (
31
+ <div>
32
+ <input
33
+ type="text"
34
+ value={field.value || ''}
35
+ onChange={(e) => field.onChange(e.target.value)}
36
+ onBlur={field.onBlur}
37
+ placeholder={field.placeholder}
38
+ aria-invalid={field['aria-invalid']}
39
+ aria-describedby={field['aria-describedby']}
40
+ aria-required={field['aria-required']}
41
+ />
42
+ {field.errors.map((e, i) => (
43
+ <span key={i} className="error">{e.message}</span>
44
+ ))}
45
+ </div>
46
+ );
47
+
48
+ const Checkbox = ({ field }: BooleanComponentProps) => (
49
+ <label>
50
+ <input
51
+ type="checkbox"
52
+ checked={field.value || false}
53
+ onChange={(e) => field.onChange(e.target.checked)}
54
+ />
55
+ {field.label}
56
+ </label>
57
+ );
58
+
59
+ const components: ComponentMap = {
60
+ text: TextInput,
61
+ email: TextInput,
62
+ boolean: Checkbox,
63
+ // ... more components
64
+ };
65
+ ```
66
+
67
+ ### 2. Render the Form
68
+
69
+ ```tsx
70
+ import { FormRenderer } from '@fogpipe/forma-react';
71
+ import type { Forma } from '@fogpipe/forma-core';
72
+
73
+ const myForm: Forma = {
74
+ meta: { title: "Contact Us" },
75
+ fields: [
76
+ { id: "name", type: "text", label: "Name", required: true },
77
+ { id: "email", type: "email", label: "Email", required: true },
78
+ { id: "subscribe", type: "boolean", label: "Subscribe to newsletter" }
79
+ ]
80
+ };
81
+
82
+ function App() {
83
+ const handleSubmit = (data: Record<string, unknown>) => {
84
+ console.log('Submitted:', data);
85
+ };
86
+
87
+ return (
88
+ <FormRenderer
89
+ spec={myForm}
90
+ components={components}
91
+ onSubmit={handleSubmit}
92
+ />
93
+ );
94
+ }
95
+ ```
96
+
97
+ ## useForma Hook
98
+
99
+ For custom rendering, use the `useForma` hook directly:
100
+
101
+ ```tsx
102
+ import { useForma } from '@fogpipe/forma-react';
103
+
104
+ function CustomForm({ spec }: { spec: Forma }) {
105
+ const {
106
+ data,
107
+ errors,
108
+ visibility,
109
+ required,
110
+ isValid,
111
+ isSubmitting,
112
+ setFieldValue,
113
+ setFieldTouched,
114
+ submitForm,
115
+ } = useForma({
116
+ spec,
117
+ onSubmit: (data) => console.log(data)
118
+ });
119
+
120
+ return (
121
+ <form onSubmit={(e) => { e.preventDefault(); submitForm(); }}>
122
+ {spec.fields.map(field => {
123
+ if (!visibility[field.id]) return null;
124
+
125
+ return (
126
+ <div key={field.id}>
127
+ <label>{field.label}</label>
128
+ <input
129
+ value={String(data[field.id] || '')}
130
+ onChange={(e) => setFieldValue(field.id, e.target.value)}
131
+ onBlur={() => setFieldTouched(field.id)}
132
+ />
133
+ </div>
134
+ );
135
+ })}
136
+
137
+ <button type="submit" disabled={isSubmitting || !isValid}>
138
+ Submit
139
+ </button>
140
+ </form>
141
+ );
142
+ }
143
+ ```
144
+
145
+ ## Multi-Page Forms (Wizard)
146
+
147
+ ```tsx
148
+ function WizardForm({ spec }: { spec: Forma }) {
149
+ const forma = useForma({ spec, onSubmit: handleSubmit });
150
+ const { wizard } = forma;
151
+
152
+ if (!wizard) return <div>Not a wizard form</div>;
153
+
154
+ return (
155
+ <div>
156
+ {/* Page indicator */}
157
+ <div>Page {wizard.currentPageIndex + 1} of {wizard.pages.length}</div>
158
+
159
+ {/* Current page fields */}
160
+ {wizard.currentPage?.fields.map(fieldId => {
161
+ const field = spec.fields.find(f => f.id === fieldId);
162
+ if (!field) return null;
163
+ // Render field...
164
+ })}
165
+
166
+ {/* Navigation */}
167
+ <button onClick={wizard.previousPage} disabled={!wizard.hasPreviousPage}>
168
+ Previous
169
+ </button>
170
+
171
+ {wizard.isLastPage ? (
172
+ <button onClick={forma.submitForm}>Submit</button>
173
+ ) : (
174
+ <button onClick={wizard.nextPage} disabled={!wizard.canProceed}>
175
+ Next
176
+ </button>
177
+ )}
178
+ </div>
179
+ );
180
+ }
181
+ ```
182
+
183
+ ## API Reference
184
+
185
+ ### FormRenderer Props
186
+
187
+ | Prop | Type | Description |
188
+ |------|------|-------------|
189
+ | `spec` | `Forma` | The Forma specification |
190
+ | `components` | `ComponentMap` | Map of field types to components |
191
+ | `initialData` | `Record<string, unknown>` | Initial form values |
192
+ | `onSubmit` | `(data) => void` | Submit handler |
193
+ | `onChange` | `(data, computed) => void` | Change handler |
194
+ | `layout` | `React.ComponentType<LayoutProps>` | Custom layout |
195
+ | `fieldWrapper` | `React.ComponentType<FieldWrapperProps>` | Custom field wrapper |
196
+ | `validateOn` | `"change" \| "blur" \| "submit"` | Validation timing |
197
+
198
+ ### FormRenderer Ref
199
+
200
+ ```tsx
201
+ const formRef = useRef<FormRendererHandle>(null);
202
+
203
+ // Imperative methods
204
+ formRef.current?.submitForm();
205
+ formRef.current?.resetForm();
206
+ formRef.current?.validateForm();
207
+ formRef.current?.focusFirstError();
208
+ formRef.current?.getValues();
209
+ formRef.current?.setValues({ name: "John" });
210
+ ```
211
+
212
+ ### useForma Return Value
213
+
214
+ | Property | Type | Description |
215
+ |----------|------|-------------|
216
+ | `data` | `Record<string, unknown>` | Current form values |
217
+ | `computed` | `Record<string, unknown>` | Computed field values |
218
+ | `visibility` | `Record<string, boolean>` | Field visibility map |
219
+ | `required` | `Record<string, boolean>` | Field required state |
220
+ | `enabled` | `Record<string, boolean>` | Field enabled state |
221
+ | `errors` | `FieldError[]` | Validation errors |
222
+ | `isValid` | `boolean` | Form validity |
223
+ | `isSubmitting` | `boolean` | Submission in progress |
224
+ | `isDirty` | `boolean` | Any field modified |
225
+ | `wizard` | `WizardHelpers \| null` | Wizard navigation |
226
+
227
+ ### useForma Methods
228
+
229
+ | Method | Description |
230
+ |--------|-------------|
231
+ | `setFieldValue(path, value)` | Set field value |
232
+ | `setFieldTouched(path, touched?)` | Mark field as touched |
233
+ | `setValues(values)` | Set multiple values |
234
+ | `validateField(path)` | Validate single field |
235
+ | `validateForm()` | Validate entire form |
236
+ | `submitForm()` | Submit the form |
237
+ | `resetForm()` | Reset to initial values |
238
+
239
+ ### useForma Options
240
+
241
+ | Option | Type | Default | Description |
242
+ |--------|------|---------|-------------|
243
+ | `spec` | `Forma` | required | The Forma specification |
244
+ | `initialData` | `Record<string, unknown>` | `{}` | Initial form values |
245
+ | `onSubmit` | `(data) => void` | - | Submit handler |
246
+ | `onChange` | `(data, computed) => void` | - | Change handler |
247
+ | `validateOn` | `"change" \| "blur" \| "submit"` | `"blur"` | When to validate |
248
+ | `referenceData` | `Record<string, unknown>` | - | Additional reference data |
249
+ | `validationDebounceMs` | `number` | `0` | Debounce validation (ms) |
250
+
251
+ ## Error Boundary
252
+
253
+ Wrap forms with `FormaErrorBoundary` to catch render errors gracefully:
254
+
255
+ ```tsx
256
+ import { FormRenderer, FormaErrorBoundary } from '@fogpipe/forma-react';
257
+
258
+ function App() {
259
+ return (
260
+ <FormaErrorBoundary
261
+ fallback={<div>Something went wrong with the form</div>}
262
+ onError={(error) => console.error('Form error:', error)}
263
+ >
264
+ <FormRenderer spec={myForm} components={components} />
265
+ </FormaErrorBoundary>
266
+ );
267
+ }
268
+ ```
269
+
270
+ The error boundary supports:
271
+ - Custom fallback UI (static or function)
272
+ - `onError` callback for logging
273
+ - `resetKey` prop to reset error state
274
+
275
+ ## License
276
+
277
+ MIT