@fogpipe/forma-react 0.12.0 → 0.13.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
@@ -25,22 +25,28 @@ npm install @fogpipe/forma-core @fogpipe/forma-react
25
25
  Components receive `{ field, spec }` props. The `field` object contains all field state and handlers:
26
26
 
27
27
  ```tsx
28
- import type { ComponentMap, TextComponentProps, BooleanComponentProps } from '@fogpipe/forma-react';
28
+ import type {
29
+ ComponentMap,
30
+ TextComponentProps,
31
+ BooleanComponentProps,
32
+ } from "@fogpipe/forma-react";
29
33
 
30
34
  const TextInput = ({ field }: TextComponentProps) => (
31
35
  <div>
32
36
  <input
33
37
  type="text"
34
- value={field.value || ''}
38
+ value={field.value || ""}
35
39
  onChange={(e) => field.onChange(e.target.value)}
36
40
  onBlur={field.onBlur}
37
41
  placeholder={field.placeholder}
38
- aria-invalid={field['aria-invalid']}
39
- aria-describedby={field['aria-describedby']}
40
- aria-required={field['aria-required']}
42
+ aria-invalid={field["aria-invalid"]}
43
+ aria-describedby={field["aria-describedby"]}
44
+ aria-required={field["aria-required"]}
41
45
  />
42
46
  {field.errors.map((e, i) => (
43
- <span key={i} className="error">{e.message}</span>
47
+ <span key={i} className="error">
48
+ {e.message}
49
+ </span>
44
50
  ))}
45
51
  </div>
46
52
  );
@@ -67,21 +73,21 @@ const components: ComponentMap = {
67
73
  ### 2. Render the Form
68
74
 
69
75
  ```tsx
70
- import { FormRenderer } from '@fogpipe/forma-react';
71
- import type { Forma } from '@fogpipe/forma-core';
76
+ import { FormRenderer } from "@fogpipe/forma-react";
77
+ import type { Forma } from "@fogpipe/forma-core";
72
78
 
73
79
  const myForm: Forma = {
74
80
  meta: { title: "Contact Us" },
75
81
  fields: [
76
82
  { id: "name", type: "text", label: "Name", required: true },
77
83
  { id: "email", type: "email", label: "Email", required: true },
78
- { id: "subscribe", type: "boolean", label: "Subscribe to newsletter" }
79
- ]
84
+ { id: "subscribe", type: "boolean", label: "Subscribe to newsletter" },
85
+ ],
80
86
  };
81
87
 
82
88
  function App() {
83
89
  const handleSubmit = (data: Record<string, unknown>) => {
84
- console.log('Submitted:', data);
90
+ console.log("Submitted:", data);
85
91
  };
86
92
 
87
93
  return (
@@ -99,7 +105,7 @@ function App() {
99
105
  For custom rendering, use the `useForma` hook directly:
100
106
 
101
107
  ```tsx
102
- import { useForma } from '@fogpipe/forma-react';
108
+ import { useForma } from "@fogpipe/forma-react";
103
109
 
104
110
  function CustomForm({ spec }: { spec: Forma }) {
105
111
  const {
@@ -114,19 +120,24 @@ function CustomForm({ spec }: { spec: Forma }) {
114
120
  submitForm,
115
121
  } = useForma({
116
122
  spec,
117
- onSubmit: (data) => console.log(data)
123
+ onSubmit: (data) => console.log(data),
118
124
  });
119
125
 
120
126
  return (
121
- <form onSubmit={(e) => { e.preventDefault(); submitForm(); }}>
122
- {spec.fields.map(field => {
127
+ <form
128
+ onSubmit={(e) => {
129
+ e.preventDefault();
130
+ submitForm();
131
+ }}
132
+ >
133
+ {spec.fields.map((field) => {
123
134
  if (!visibility[field.id]) return null;
124
135
 
125
136
  return (
126
137
  <div key={field.id}>
127
138
  <label>{field.label}</label>
128
139
  <input
129
- value={String(data[field.id] || '')}
140
+ value={String(data[field.id] || "")}
130
141
  onChange={(e) => setFieldValue(field.id, e.target.value)}
131
142
  onBlur={() => setFieldTouched(field.id)}
132
143
  />
@@ -154,11 +165,13 @@ function WizardForm({ spec }: { spec: Forma }) {
154
165
  return (
155
166
  <div>
156
167
  {/* Page indicator */}
157
- <div>Page {wizard.currentPageIndex + 1} of {wizard.pages.length}</div>
168
+ <div>
169
+ Page {wizard.currentPageIndex + 1} of {wizard.pages.length}
170
+ </div>
158
171
 
159
172
  {/* Current page fields */}
160
- {wizard.currentPage?.fields.map(fieldId => {
161
- const field = spec.fields.find(f => f.id === fieldId);
173
+ {wizard.currentPage?.fields.map((fieldId) => {
174
+ const field = spec.fields.find((f) => f.id === fieldId);
162
175
  if (!field) return null;
163
176
  // Render field...
164
177
  })}
@@ -184,16 +197,16 @@ function WizardForm({ spec }: { spec: Forma }) {
184
197
 
185
198
  ### FormRenderer Props
186
199
 
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 |
200
+ | Prop | Type | Description |
201
+ | -------------- | ---------------------------------------- | -------------------------------- |
202
+ | `spec` | `Forma` | The Forma specification |
203
+ | `components` | `ComponentMap` | Map of field types to components |
204
+ | `initialData` | `Record<string, unknown>` | Initial form values |
205
+ | `onSubmit` | `(data) => void` | Submit handler |
206
+ | `onChange` | `(data, computed) => void` | Change handler |
207
+ | `layout` | `React.ComponentType<LayoutProps>` | Custom layout |
208
+ | `fieldWrapper` | `React.ComponentType<FieldWrapperProps>` | Custom field wrapper |
209
+ | `validateOn` | `"change" \| "blur" \| "submit"` | Validation timing |
197
210
 
198
211
  ### FormRenderer Ref
199
212
 
@@ -211,55 +224,55 @@ formRef.current?.setValues({ name: "John" });
211
224
 
212
225
  ### useForma Return Value
213
226
 
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 |
227
+ | Property | Type | Description |
228
+ | -------------- | ------------------------- | ---------------------- |
229
+ | `data` | `Record<string, unknown>` | Current form values |
230
+ | `computed` | `Record<string, unknown>` | Computed field values |
231
+ | `visibility` | `Record<string, boolean>` | Field visibility map |
232
+ | `required` | `Record<string, boolean>` | Field required state |
233
+ | `enabled` | `Record<string, boolean>` | Field enabled state |
234
+ | `errors` | `FieldError[]` | Validation errors |
235
+ | `isValid` | `boolean` | Form validity |
236
+ | `isSubmitting` | `boolean` | Submission in progress |
237
+ | `isDirty` | `boolean` | Any field modified |
238
+ | `wizard` | `WizardHelpers \| null` | Wizard navigation |
226
239
 
227
240
  ### useForma Methods
228
241
 
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 |
242
+ | Method | Description |
243
+ | --------------------------------- | ----------------------- |
244
+ | `setFieldValue(path, value)` | Set field value |
245
+ | `setFieldTouched(path, touched?)` | Mark field as touched |
246
+ | `setValues(values)` | Set multiple values |
247
+ | `validateField(path)` | Validate single field |
248
+ | `validateForm()` | Validate entire form |
249
+ | `submitForm()` | Submit the form |
250
+ | `resetForm()` | Reset to initial values |
238
251
 
239
252
  ### useForma Options
240
253
 
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) |
254
+ | Option | Type | Default | Description |
255
+ | ---------------------- | -------------------------------- | -------- | ------------------------- |
256
+ | `spec` | `Forma` | required | The Forma specification |
257
+ | `initialData` | `Record<string, unknown>` | `{}` | Initial form values |
258
+ | `onSubmit` | `(data) => void` | - | Submit handler |
259
+ | `onChange` | `(data, computed) => void` | - | Change handler |
260
+ | `validateOn` | `"change" \| "blur" \| "submit"` | `"blur"` | When to validate |
261
+ | `referenceData` | `Record<string, unknown>` | - | Additional reference data |
262
+ | `validationDebounceMs` | `number` | `0` | Debounce validation (ms) |
250
263
 
251
264
  ## Error Boundary
252
265
 
253
266
  Wrap forms with `FormaErrorBoundary` to catch render errors gracefully:
254
267
 
255
268
  ```tsx
256
- import { FormRenderer, FormaErrorBoundary } from '@fogpipe/forma-react';
269
+ import { FormRenderer, FormaErrorBoundary } from "@fogpipe/forma-react";
257
270
 
258
271
  function App() {
259
272
  return (
260
273
  <FormaErrorBoundary
261
274
  fallback={<div>Something went wrong with the form</div>}
262
- onError={(error) => console.error('Form error:', error)}
275
+ onError={(error) => console.error("Form error:", error)}
263
276
  >
264
277
  <FormRenderer spec={myForm} components={components} />
265
278
  </FormaErrorBoundary>
@@ -268,6 +281,7 @@ function App() {
268
281
  ```
269
282
 
270
283
  The error boundary supports:
284
+
271
285
  - Custom fallback UI (static or function)
272
286
  - `onError` callback for logging
273
287
  - `resetKey` prop to reset error state
package/dist/index.js CHANGED
@@ -72,6 +72,15 @@ function getDefaultBooleanValues(spec) {
72
72
  }
73
73
  return defaults;
74
74
  }
75
+ function getFieldDefaults(spec) {
76
+ const defaults = {};
77
+ for (const [fieldPath, fieldDef] of Object.entries(spec.fields)) {
78
+ if (fieldDef.defaultValue !== void 0) {
79
+ defaults[fieldPath] = fieldDef.defaultValue;
80
+ }
81
+ }
82
+ return defaults;
83
+ }
75
84
  function useForma(options) {
76
85
  const {
77
86
  spec: inputSpec,
@@ -93,8 +102,11 @@ function useForma(options) {
93
102
  };
94
103
  }, [inputSpec, referenceData]);
95
104
  const [state, dispatch] = useReducer(formReducer, {
96
- data: { ...getDefaultBooleanValues(spec), ...initialData },
97
- // Boolean defaults merged UNDER initialData
105
+ data: {
106
+ ...getDefaultBooleanValues(spec),
107
+ ...getFieldDefaults(spec),
108
+ ...initialData
109
+ },
98
110
  touched: {},
99
111
  isSubmitting: false,
100
112
  isSubmitted: false,
@@ -220,8 +232,15 @@ function useForma(options) {
220
232
  }
221
233
  }, [immediateValidation, onSubmit, state.data]);
222
234
  const resetForm = useCallback(() => {
223
- dispatch({ type: "RESET", initialData });
224
- }, [initialData]);
235
+ dispatch({
236
+ type: "RESET",
237
+ initialData: {
238
+ ...getDefaultBooleanValues(spec),
239
+ ...getFieldDefaults(spec),
240
+ ...initialData
241
+ }
242
+ });
243
+ }, [spec, initialData]);
225
244
  const wizard = useMemo(() => {
226
245
  if (!spec.pages || spec.pages.length === 0) return null;
227
246
  const pageVisibility = getPageVisibility(state.data, spec, { computed });
@@ -618,7 +637,9 @@ var FormaContext = createContext(null);
618
637
  function useFormaContext() {
619
638
  const context = useContext(FormaContext);
620
639
  if (!context) {
621
- throw new Error("useFormaContext must be used within a FormaContext.Provider");
640
+ throw new Error(
641
+ "useFormaContext must be used within a FormaContext.Provider"
642
+ );
622
643
  }
623
644
  return context;
624
645
  }
@@ -699,7 +720,9 @@ function getNumberConstraints(schema) {
699
720
  function createDefaultItem(itemFields) {
700
721
  const item = {};
701
722
  for (const [fieldName, fieldDef] of Object.entries(itemFields)) {
702
- if (fieldDef.type === "boolean") {
723
+ if (fieldDef.defaultValue !== void 0) {
724
+ item[fieldName] = fieldDef.defaultValue;
725
+ } else if (fieldDef.type === "boolean") {
703
726
  item[fieldName] = false;
704
727
  } else if (fieldDef.type === "number" || fieldDef.type === "integer") {
705
728
  item[fieldName] = null;
@@ -1006,7 +1029,9 @@ function getNumberConstraints2(schema) {
1006
1029
  function createDefaultItem2(itemFields) {
1007
1030
  const item = {};
1008
1031
  for (const [fieldName, fieldDef] of Object.entries(itemFields)) {
1009
- if (fieldDef.type === "boolean") {
1032
+ if (fieldDef.defaultValue !== void 0) {
1033
+ item[fieldName] = fieldDef.defaultValue;
1034
+ } else if (fieldDef.type === "boolean") {
1010
1035
  item[fieldName] = false;
1011
1036
  } else if (fieldDef.type === "number" || fieldDef.type === "integer") {
1012
1037
  item[fieldName] = null;
@@ -1227,7 +1252,10 @@ function FieldRenderer({
1227
1252
  // src/ErrorBoundary.tsx
1228
1253
  import React3 from "react";
1229
1254
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1230
- function DefaultErrorFallback({ error, onReset }) {
1255
+ function DefaultErrorFallback({
1256
+ error,
1257
+ onReset
1258
+ }) {
1231
1259
  return /* @__PURE__ */ jsxs2("div", { className: "forma-error-boundary", role: "alert", children: [
1232
1260
  /* @__PURE__ */ jsx3("h3", { children: "Something went wrong" }),
1233
1261
  /* @__PURE__ */ jsx3("p", { children: "An error occurred while rendering the form." }),