@petrarca/sonnet-forms 0.6.0 → 0.8.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 +69 -244
- package/dist/index.d.ts +165 -6
- package/dist/index.js +481 -64
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,276 +1,101 @@
|
|
|
1
1
|
# @petrarca/sonnet-forms
|
|
2
2
|
|
|
3
|
-
Schema-driven
|
|
4
|
-
|
|
3
|
+
Schema-driven forms for the Petrarca Sonnet component library. One component,
|
|
4
|
+
`JsonSchemaFormRenderer`, takes a JSON Schema and a data object and produces
|
|
5
|
+
labelled, validated controls.
|
|
5
6
|
|
|
6
|
-
## What
|
|
7
|
+
## What it provides
|
|
7
8
|
|
|
8
|
-
**
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
**The renderer.** `$ref` resolution, `if`/`then`/`else` and `allOf`/`anyOf`
|
|
10
|
+
flattening against the current values, schema defaults, nested objects and
|
|
11
|
+
arrays, buffered or live editing, and a change set on save.
|
|
11
12
|
|
|
12
|
-
**
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
**A widget registry.** Sixteen built-in controls covering text, numbers,
|
|
14
|
+
quantities, enums, checkboxes, tags, arrays, objects, entity references and
|
|
15
|
+
embedded JSON, plus your own controls keyed by `x-ui-widget`. A parallel
|
|
16
|
+
template registry makes the structure around a field -- the array-item row, the
|
|
17
|
+
object container, the group chrome -- replaceable without touching the widgets.
|
|
15
18
|
|
|
16
|
-
**
|
|
17
|
-
|
|
18
|
-
tags, quantity, arrays, objects, entity references, and JSON editing.
|
|
19
|
+
**Layout.** Rows, tabs, steps and sections, declared as `x-ui-*` hints on the
|
|
20
|
+
schema or in a UI schema passed beside it.
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
**Standalone field components.** `FormInput`, `FormTextarea`, `FormSelect`,
|
|
23
|
+
`FormMultiSelect`, `FormCheckbox`, `FormNumberInput`, `FormQuantityInput` and
|
|
24
|
+
`FormTagsInput`, for labelled inputs outside a schema-driven form.
|
|
25
|
+
|
|
26
|
+
**Change tracking.** `onUpdate` reports what changed, including inside arrays,
|
|
27
|
+
so a caller can send a targeted update rather than the whole object.
|
|
21
28
|
|
|
22
29
|
## Install
|
|
23
30
|
|
|
24
31
|
```bash
|
|
25
|
-
pnpm add @petrarca/sonnet-forms
|
|
32
|
+
pnpm add @petrarca/sonnet-forms
|
|
26
33
|
```
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
`@petrarca/sonnet-core` and `@petrarca/sonnet-ui` are regular dependencies and
|
|
36
|
+
arrive with it.
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
| Peer | Range | Needed for |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `react`, `react-dom` | `>=19` | Everything |
|
|
41
|
+
| `tailwindcss` | `>=3` | The classes every component emits |
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
The output assumes `sonnet-ui`'s stylesheet and Tailwind preset are already
|
|
44
|
+
wired up. Do that first -- see
|
|
45
|
+
[sonnet-ui: getting started](../sonnet-ui/docs/usage/getting-started.md).
|
|
33
46
|
|
|
34
|
-
|
|
47
|
+
## Usage
|
|
35
48
|
|
|
36
49
|
```tsx
|
|
37
50
|
import { JsonSchemaFormRenderer } from "@petrarca/sonnet-forms";
|
|
51
|
+
import type { JsonSchema } from "@petrarca/sonnet-core/schema";
|
|
38
52
|
|
|
39
|
-
const
|
|
53
|
+
const WIDGET_SCHEMA: JsonSchema = {
|
|
54
|
+
title: "Widget",
|
|
40
55
|
type: "object",
|
|
56
|
+
required: ["name"],
|
|
41
57
|
properties: {
|
|
42
|
-
name:
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
name: { type: "string", title: "Name" },
|
|
59
|
+
status: {
|
|
60
|
+
type: "string",
|
|
61
|
+
title: "Status",
|
|
62
|
+
enum: ["draft", "active", "retired"],
|
|
63
|
+
default: "draft",
|
|
64
|
+
},
|
|
65
|
+
notes: { type: "string", title: "Notes", "x-ui-widget": "textarea" },
|
|
45
66
|
},
|
|
46
|
-
required: ["name", "email"],
|
|
47
67
|
};
|
|
48
68
|
|
|
49
69
|
<JsonSchemaFormRenderer
|
|
50
|
-
schema={
|
|
51
|
-
data={
|
|
52
|
-
onUpdate={(data, changedFields) => save(data)}
|
|
53
|
-
onCancel={handleClose}
|
|
54
|
-
showActions
|
|
55
|
-
saveLabel="Save"
|
|
56
|
-
/>
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Key props
|
|
60
|
-
|
|
61
|
-
| Prop | Type | Default | Purpose |
|
|
62
|
-
|---|---|---|---|
|
|
63
|
-
| `schema` | `JsonSchema` | required | JSON Schema describing the form |
|
|
64
|
-
| `data` | `Record<string, unknown>` | required | Current form values |
|
|
65
|
-
| `onUpdate` | `(data, changedFields, diff) => void` | — | Called on save |
|
|
66
|
-
| `onCancel` | `() => void` | — | Called on cancel |
|
|
67
|
-
| `onChange` | `(data) => void` | — | Called on every field change (use instead of `onUpdate` when managing your own buttons) |
|
|
68
|
-
| `showActions` | `boolean` | `false` | Render built-in Save / Cancel buttons inside the form |
|
|
69
|
-
| `showCancel` | `boolean` | `true` | Show the Cancel button (requires `showActions`) |
|
|
70
|
-
| `saveLabel` | `string` | `"Save"` | Save button label |
|
|
71
|
-
| `cancelLabel` | `string` | `"Cancel"` | Cancel button label |
|
|
72
|
-
| `showExtraProperties` | `boolean` | `false` | Preserve form keys not in the schema. Set `true` for open-ended data (e.g. graph node properties) |
|
|
73
|
-
| `uiSchema` | `Record<string, UISchema>` | — | Per-field UI overrides |
|
|
74
|
-
| `widgets` | `WidgetRegistry` | `DEFAULT_WIDGETS` | Custom widget map |
|
|
75
|
-
| `deferStateUpdates` | `boolean` | `false` | Update state on blur instead of on change |
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## Schema construction
|
|
80
|
-
|
|
81
|
-
The recommended pattern is Zod + `toJsonSchema()` with `.meta()` for UI hints:
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
import { z } from "zod";
|
|
85
|
-
import { toJsonSchema } from "@petrarca/sonnet-core/schema";
|
|
86
|
-
|
|
87
|
-
const UserSchema = z.object({
|
|
88
|
-
name: z.string().meta({
|
|
89
|
-
"x-ui-title": "Full name",
|
|
90
|
-
"x-ui-description": "As it appears on official documents.",
|
|
91
|
-
}),
|
|
92
|
-
role: z.enum(["admin", "user"]).meta({
|
|
93
|
-
"x-ui-title": "Role",
|
|
94
|
-
"x-ui-options": { enumNames: ["Administrator", "Standard user"] },
|
|
95
|
-
}),
|
|
96
|
-
bio: z.string().optional().meta({
|
|
97
|
-
"x-ui-title": "Bio",
|
|
98
|
-
"x-ui-widget": "textarea",
|
|
99
|
-
}),
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Convert once, store as a module-level constant:
|
|
103
|
-
const USER_FORM_SCHEMA = toJsonSchema(UserSchema);
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Or write the JSON Schema directly for dynamic / server-driven forms.
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## Schema UI annotations
|
|
111
|
-
|
|
112
|
-
Annotate individual fields with `x-ui-*` to control rendering:
|
|
113
|
-
|
|
114
|
-
| Annotation | Purpose |
|
|
115
|
-
|---|---|
|
|
116
|
-
| `"x-ui-title"` | Override the field label (`false` to hide) |
|
|
117
|
-
| `"x-ui-description"` | Override the field description |
|
|
118
|
-
| `"x-ui-widget"` | Force a specific widget: `"textarea"`, `"tags"`, `"entity-select"`, or a custom key |
|
|
119
|
-
| `"x-ui-options"` | Widget-specific options (see widget docs) |
|
|
120
|
-
| `"x-ui-order"` | Array of field names to control rendering order in an object |
|
|
121
|
-
| `"x-ui-layout"` | Array layout config: `{ direction: "horizontal", columns, gap, compact }` |
|
|
122
|
-
|
|
123
|
-
---
|
|
124
|
-
|
|
125
|
-
## onChange mode (external buttons)
|
|
126
|
-
|
|
127
|
-
When you want to drive your own Submit button outside the form, omit `showActions`
|
|
128
|
-
and use `onChange` instead of `onUpdate`:
|
|
129
|
-
|
|
130
|
-
```tsx
|
|
131
|
-
const [formData, setFormData] = useState({});
|
|
132
|
-
|
|
133
|
-
<JsonSchemaFormRenderer
|
|
134
|
-
schema={schema}
|
|
135
|
-
data={formData}
|
|
136
|
-
onChange={setFormData}
|
|
137
|
-
// no showActions — caller owns the buttons
|
|
138
|
-
/>
|
|
139
|
-
|
|
140
|
-
<Button onClick={() => submit(formData)}>Submit</Button>
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
## UISchema
|
|
146
|
-
|
|
147
|
-
Override rendering per-field without modifying the schema:
|
|
148
|
-
|
|
149
|
-
```tsx
|
|
150
|
-
<JsonSchemaFormRenderer
|
|
151
|
-
schema={schema}
|
|
152
|
-
data={data}
|
|
153
|
-
uiSchema={{
|
|
154
|
-
name: { "x-ui-title": "Display name" }, // override label
|
|
155
|
-
internal_id: { "x-ui-title": false }, // hide label entirely
|
|
156
|
-
notes: { "x-ui-widget": "textarea" }, // force widget
|
|
157
|
-
}}
|
|
158
|
-
onUpdate={onUpdate}
|
|
70
|
+
schema={WIDGET_SCHEMA}
|
|
71
|
+
data={widget}
|
|
159
72
|
showActions
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
## Custom widgets
|
|
166
|
-
|
|
167
|
-
Extend `DEFAULT_WIDGETS` with your own widget components:
|
|
168
|
-
|
|
169
|
-
```ts
|
|
170
|
-
import { DEFAULT_WIDGETS, type WidgetRegistry, type WidgetProps } from "@petrarca/sonnet-forms";
|
|
171
|
-
|
|
172
|
-
function MyColorWidget({ value, onChange, schema }: WidgetProps) {
|
|
173
|
-
return (
|
|
174
|
-
<input
|
|
175
|
-
type="color"
|
|
176
|
-
value={String(value ?? "#000000")}
|
|
177
|
-
onChange={(e) => onChange(e.target.value)}
|
|
178
|
-
/>
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export const MY_WIDGETS: WidgetRegistry = {
|
|
183
|
-
...DEFAULT_WIDGETS,
|
|
184
|
-
"color-picker": MyColorWidget, // key matches "x-ui-widget": "color-picker" in schema
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
// Usage:
|
|
188
|
-
<JsonSchemaFormRenderer schema={schema} data={data} widgets={MY_WIDGETS} onUpdate={onUpdate} showActions />
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
`WidgetProps` includes `formData` (the full current form state) — use this to
|
|
192
|
-
build dependent widgets that react to sibling field values.
|
|
193
|
-
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
## Dynamic schema (edit vs. create)
|
|
197
|
-
|
|
198
|
-
To hide fields that are immutable after creation, delete them from a cloned
|
|
199
|
-
schema. Also exclude them from `data` — if a key exists in `data` but not
|
|
200
|
-
the schema, the form engine treats it as a dirty field immediately:
|
|
201
|
-
|
|
202
|
-
```ts
|
|
203
|
-
const schema = useMemo(() => {
|
|
204
|
-
if (!isEditing) return BASE_SCHEMA;
|
|
205
|
-
const s = structuredClone(BASE_SCHEMA);
|
|
206
|
-
delete s.properties?.app_key; // read-only after creation
|
|
207
|
-
return s;
|
|
208
|
-
}, [isEditing]);
|
|
209
|
-
|
|
210
|
-
const data = isEditing
|
|
211
|
-
? { name: entity.name } // omit app_key from data too
|
|
212
|
-
: {};
|
|
73
|
+
saveLabel="Save changes"
|
|
74
|
+
onUpdate={(data, changedFields) => save(changedFields)}
|
|
75
|
+
onCancel={close}
|
|
76
|
+
showExtraProperties={false}
|
|
77
|
+
/>;
|
|
213
78
|
```
|
|
214
79
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
error={nameError}
|
|
234
|
-
/>
|
|
235
|
-
|
|
236
|
-
<FormSelect
|
|
237
|
-
label="Role"
|
|
238
|
-
options={[{ value: "admin", label: "Admin" }, { value: "user", label: "User" }]}
|
|
239
|
-
value={role}
|
|
240
|
-
onChange={setRole}
|
|
241
|
-
clearable={false}
|
|
242
|
-
/>
|
|
243
|
-
|
|
244
|
-
<FormCheckbox
|
|
245
|
-
label="Active"
|
|
246
|
-
checked={active}
|
|
247
|
-
onChange={(checked) => setActive(checked === true)}
|
|
248
|
-
/>
|
|
249
|
-
|
|
250
|
-
<FormTextarea
|
|
251
|
-
label="Notes"
|
|
252
|
-
value={notes}
|
|
253
|
-
onChange={(e) => setNotes(e.currentTarget.value)}
|
|
254
|
-
autosize
|
|
255
|
-
minRows={3}
|
|
256
|
-
rightSection={<ClearButton />}
|
|
257
|
-
rightSectionWidth={40}
|
|
258
|
-
/>
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**Common props on all field components:**
|
|
262
|
-
|
|
263
|
-
| Prop | Purpose |
|
|
264
|
-
|---|---|
|
|
265
|
-
| `label` | Field label. Pass `false` to suppress. |
|
|
266
|
-
| `description` | Help text below the field |
|
|
267
|
-
| `error` | Error message (also sets error styling) |
|
|
268
|
-
| `compact` | Tighter vertical spacing |
|
|
269
|
-
| `disabled` | Disables the field |
|
|
270
|
-
| `wrapperClassName` | Class on the outer wrapper div |
|
|
271
|
-
|
|
272
|
-
---
|
|
80
|
+
Hold the schema as a module-level constant; rebuilding it on every render
|
|
81
|
+
re-resolves references and conditionals. `showExtraProperties` defaults to
|
|
82
|
+
`true` and shows a card listing data keys absent from the schema -- most forms
|
|
83
|
+
want it off. The twenty-four props, the three edit modes and what validation
|
|
84
|
+
actually enforces are in [getting started](docs/usage/getting-started.md).
|
|
85
|
+
|
|
86
|
+
## Documentation
|
|
87
|
+
|
|
88
|
+
- [Usage guides](docs/usage/index.md) -- getting started, the
|
|
89
|
+
[`x-ui-*` vocabulary](docs/usage/ui-schema.md),
|
|
90
|
+
[layout](docs/usage/layout.md), [widgets](docs/usage/widgets.md),
|
|
91
|
+
[templates](docs/usage/templates.md),
|
|
92
|
+
[field components](docs/usage/field-components.md) and
|
|
93
|
+
[change tracking](docs/usage/change-tracking.md).
|
|
94
|
+
- [Design docs](docs/design/index.md) -- form layout, nested fields, the
|
|
95
|
+
change-tracking model, and custom validation.
|
|
96
|
+
- [Workspace documentation](../../docs/README.md) -- how the packages layer and
|
|
97
|
+
the shared schema extension contract.
|
|
273
98
|
|
|
274
99
|
## License
|
|
275
100
|
|
|
276
|
-
See [LICENSE.md](../../LICENSE.md).
|
|
101
|
+
Apache 2.0. See [LICENSE.md](../../LICENSE.md).
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,10 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import * as React$1 from 'react';
|
|
3
3
|
import React__default, { ComponentType, ReactNode } from 'react';
|
|
4
4
|
import { Input, Textarea, badgeVariants } from '@petrarca/sonnet-ui';
|
|
5
|
-
import { JsonSchemaProperty, UILayoutConfig, JsonSchema, UISchema, UISchemaOptions, FormDiff, RefResolver
|
|
5
|
+
import { JsonSchemaProperty, UILayoutConfig, JsonSchema, UISchema, UISchemaOptions, FieldError, FormDiff, RefResolver } from '@petrarca/sonnet-core/schema';
|
|
6
6
|
export { UISchema, UISchemaOptions } from '@petrarca/sonnet-core/schema';
|
|
7
7
|
import { VariantProps } from 'class-variance-authority';
|
|
8
|
+
import { SaveWhenInvalid, ValidationDisplay } from '@petrarca/sonnet-core/config';
|
|
8
9
|
|
|
9
10
|
interface FormFieldWrapperProps {
|
|
10
11
|
/** Field id -- wired to the label's htmlFor */
|
|
@@ -663,16 +664,27 @@ interface FormActionsProps {
|
|
|
663
664
|
disabled?: boolean;
|
|
664
665
|
hasChanges: boolean;
|
|
665
666
|
isValid: boolean;
|
|
667
|
+
/**
|
|
668
|
+
* Whether Save is available while invalid. `reveal` (default) keeps it
|
|
669
|
+
* enabled -- an invalid click reveals errors and does not submit; `disable`
|
|
670
|
+
* greys it out while invalid.
|
|
671
|
+
*/
|
|
672
|
+
saveWhenInvalid?: SaveWhenInvalid;
|
|
666
673
|
saveLabel?: string;
|
|
667
674
|
cancelLabel?: string;
|
|
668
675
|
showCancel?: boolean;
|
|
676
|
+
/**
|
|
677
|
+
* True while an async validator is in flight. Save is disabled until it
|
|
678
|
+
* settles (validity is not yet known); Cancel stays enabled.
|
|
679
|
+
*/
|
|
680
|
+
isValidating?: boolean;
|
|
669
681
|
/**
|
|
670
682
|
* Optional content rendered left-aligned in the action row (e.g. a secondary
|
|
671
683
|
* action like "Test connection"). Cancel/Save stay grouped on the right.
|
|
672
684
|
*/
|
|
673
685
|
leadingActions?: ReactNode;
|
|
674
686
|
}
|
|
675
|
-
declare function FormActions({ onSave, onCancel, disabled, hasChanges, isValid, saveLabel, cancelLabel, showCancel, leadingActions, }: FormActionsProps): react_jsx_runtime.JSX.Element;
|
|
687
|
+
declare function FormActions({ onSave, onCancel, disabled, hasChanges, isValid, saveWhenInvalid, saveLabel, cancelLabel, showCancel, isValidating, leadingActions, }: FormActionsProps): react_jsx_runtime.JSX.Element;
|
|
676
688
|
|
|
677
689
|
type Props = {
|
|
678
690
|
properties: Record<string, any>;
|
|
@@ -845,7 +857,7 @@ declare function EntitySelectWidget({ id, name, value, onChange, onBlur, label,
|
|
|
845
857
|
* compact / label suppression propagates to all nested widgets.
|
|
846
858
|
*/
|
|
847
859
|
|
|
848
|
-
declare function ObjectWidget({ propertySchema, value, onChange, registry, uiSchema, disabled, readonly, label, description, options, }: WidgetProps): React.ReactElement | null;
|
|
860
|
+
declare function ObjectWidget({ id, propertySchema, value, onChange, onBlur, registry, uiSchema, disabled, readonly, label, description, options, }: WidgetProps): React.ReactElement | null;
|
|
849
861
|
|
|
850
862
|
/**
|
|
851
863
|
* ArrayWidget -- renders a repeatable list of sub-forms for
|
|
@@ -1019,6 +1031,71 @@ declare function getDefaultWidgetForType(type: string): string;
|
|
|
1019
1031
|
*/
|
|
1020
1032
|
declare const DEFAULT_WIDGETS: WidgetRegistry;
|
|
1021
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* Custom validation registry -- named field and form validators referenced from
|
|
1036
|
+
* the schema via `x-ui-validate` and implemented here.
|
|
1037
|
+
*
|
|
1038
|
+
* See the custom-validation design in docs/design/custom-validation.md. `field`
|
|
1039
|
+
* and `form` are synchronous, invoked by `runValidators` every validation pass;
|
|
1040
|
+
* `asyncField` and `asyncForm` return a promise and are invoked only by
|
|
1041
|
+
* `useAsyncValidators`, keyed on their inputs so a remote check fires on
|
|
1042
|
+
* change, never per render.
|
|
1043
|
+
*/
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Context handed to every validator so a rule can reach beyond its immediate
|
|
1047
|
+
* value without the caller wiring it. `formData` is the ROOT form data (carried
|
|
1048
|
+
* like x-ui-rule's rootData), so a validator on a nested field can still compare
|
|
1049
|
+
* against a top-level field.
|
|
1050
|
+
*/
|
|
1051
|
+
interface ValidatorContext {
|
|
1052
|
+
/** The whole current form data (root), for cross-field checks. */
|
|
1053
|
+
formData: Record<string, unknown>;
|
|
1054
|
+
/** RFC 6901 JSON Pointer of the field being validated (field validators). */
|
|
1055
|
+
path: string;
|
|
1056
|
+
/** Per-reference config from the `x-ui-validate` object form (`{ name, args }`). */
|
|
1057
|
+
args?: Record<string, unknown>;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* What a SYNC validator returns. A field validator may use the `string | null`
|
|
1061
|
+
* shorthand (message attached to its own field, or null when valid); a form
|
|
1062
|
+
* validator returns `FieldError[]` because it decides which field each error
|
|
1063
|
+
* lands on (pointer "" = form-level).
|
|
1064
|
+
*/
|
|
1065
|
+
type ValidatorResult = string | null | FieldError[];
|
|
1066
|
+
/** A per-value sync validator: judges one field's value. */
|
|
1067
|
+
type FieldValidator = (value: unknown, ctx: ValidatorContext) => string | null | FieldError[];
|
|
1068
|
+
/** A whole-data sync validator: cross-field / form-level rules. */
|
|
1069
|
+
type FormValidator = (data: Record<string, unknown>, ctx: ValidatorContext) => FieldError[];
|
|
1070
|
+
/**
|
|
1071
|
+
* An async per-value validator -- returns a promise. Declared in its own
|
|
1072
|
+
* namespace (`asyncField`) so the engine knows it is async without calling it,
|
|
1073
|
+
* and invokes it only when its field value changes (never per render). A failed
|
|
1074
|
+
* promise means "could not check", not "invalid".
|
|
1075
|
+
*/
|
|
1076
|
+
type AsyncFieldValidator = (value: unknown, ctx: ValidatorContext) => Promise<string | null | FieldError[]>;
|
|
1077
|
+
/** An async whole-data validator -- returns a promise. See `dependsOn` on the
|
|
1078
|
+
* `x-ui-validate` reference to control when it re-fires. */
|
|
1079
|
+
type AsyncFormValidator = (data: Record<string, unknown>, ctx: ValidatorContext) => Promise<FieldError[]>;
|
|
1080
|
+
/**
|
|
1081
|
+
* The registry: sync and async namespaces, keeping the signatures -- and the
|
|
1082
|
+
* invocation strategy -- apart. Sync validators run on every validation pass;
|
|
1083
|
+
* async validators are invoked only when their inputs change (the engine can
|
|
1084
|
+
* tell them apart because they are declared separately, so a remote check is
|
|
1085
|
+
* never fired speculatively per render). A plain object, like the widget and
|
|
1086
|
+
* template registries -- no factory, no lifecycle.
|
|
1087
|
+
*/
|
|
1088
|
+
interface ValidatorRegistry {
|
|
1089
|
+
/** Sync per-field validators, referenced by `x-ui-validate` on a property. */
|
|
1090
|
+
field?: Record<string, FieldValidator>;
|
|
1091
|
+
/** Sync whole-data validators, referenced by `x-ui-validate` on the root. */
|
|
1092
|
+
form?: Record<string, FormValidator>;
|
|
1093
|
+
/** Async per-field validators (promise), referenced by `x-ui-validate`. */
|
|
1094
|
+
asyncField?: Record<string, AsyncFieldValidator>;
|
|
1095
|
+
/** Async whole-data validators (promise), referenced by `x-ui-validate`. */
|
|
1096
|
+
asyncForm?: Record<string, AsyncFormValidator>;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1022
1099
|
/**
|
|
1023
1100
|
* JsonSchemaFormRenderer - Generic form renderer based on JSON Schema
|
|
1024
1101
|
*
|
|
@@ -1091,6 +1168,17 @@ type JsonSchemaFormRendererProps = {
|
|
|
1091
1168
|
* caller pre-loads referenced schemas into a map and passes a lookup.
|
|
1092
1169
|
*/
|
|
1093
1170
|
refResolver?: RefResolver;
|
|
1171
|
+
/**
|
|
1172
|
+
* When to reveal field validation errors. Overrides the app-wide default set
|
|
1173
|
+
* in SonnetConfigProvider globals. Defaults to `onSubmit`.
|
|
1174
|
+
*/
|
|
1175
|
+
validationDisplay?: ValidationDisplay;
|
|
1176
|
+
/**
|
|
1177
|
+
* Whether Save is available while the form is invalid. Overrides the app-wide
|
|
1178
|
+
* default set in SonnetConfigProvider globals. Defaults to `reveal` (Save
|
|
1179
|
+
* stays enabled; an invalid click reveals errors and does not submit).
|
|
1180
|
+
*/
|
|
1181
|
+
saveWhenInvalid?: SaveWhenInvalid;
|
|
1094
1182
|
/** Label for Save button (default: "Save") */
|
|
1095
1183
|
saveLabel?: string;
|
|
1096
1184
|
/** Label for Cancel button (default: "Cancel") */
|
|
@@ -1114,6 +1202,22 @@ type JsonSchemaFormRendererProps = {
|
|
|
1114
1202
|
* a partial registry -- unspecified keys fall back to DEFAULT_TEMPLATES.
|
|
1115
1203
|
*/
|
|
1116
1204
|
templates?: TemplateRegistry;
|
|
1205
|
+
/**
|
|
1206
|
+
* Custom validator registry -- named field and form validators referenced
|
|
1207
|
+
* from the schema via `x-ui-validate`, run alongside the built-in schema
|
|
1208
|
+
* validation. `field`/`form` are synchronous; `asyncField`/`asyncForm`
|
|
1209
|
+
* return a promise and gate Save while in flight. See
|
|
1210
|
+
* docs/design/custom-validation.md.
|
|
1211
|
+
*/
|
|
1212
|
+
validators?: ValidatorRegistry;
|
|
1213
|
+
/**
|
|
1214
|
+
* Externally-supplied errors (e.g. a server 422) shown as persistent field
|
|
1215
|
+
* errors, keyed by JSON Pointer. Always displayed regardless of
|
|
1216
|
+
* `validationDisplay`, and block Save. Should be stable/memoized. A
|
|
1217
|
+
* higher-precedence error (built-in or a custom validator) on the same field
|
|
1218
|
+
* wins over an extraError. See the custom-validation design.
|
|
1219
|
+
*/
|
|
1220
|
+
extraErrors?: FieldError[];
|
|
1117
1221
|
/**
|
|
1118
1222
|
* When true, renders with `display: contents` instead of a SimpleStack.
|
|
1119
1223
|
*
|
|
@@ -1177,8 +1281,10 @@ interface FormFieldRendererProps {
|
|
|
1177
1281
|
* to keep ids unique.
|
|
1178
1282
|
*/
|
|
1179
1283
|
idPrefix?: string;
|
|
1284
|
+
/** Validation messages for this field, rendered inline by the widget. */
|
|
1285
|
+
rawErrors?: string[];
|
|
1180
1286
|
}
|
|
1181
|
-
declare function FormFieldRenderer({ fieldName, property, value, onChange, onBlur, required, disabled, readOnly, schema, uiSchema, registry, globalOptions, formData, idPrefix, }: FormFieldRendererProps): React__default.ReactElement | null;
|
|
1287
|
+
declare function FormFieldRenderer({ fieldName, property, value, onChange, onBlur, required, disabled, readOnly, schema, uiSchema, registry, globalOptions, formData, idPrefix, rawErrors, }: FormFieldRendererProps): React__default.ReactElement | null;
|
|
1182
1288
|
|
|
1183
1289
|
/**
|
|
1184
1290
|
* useFormEngine - Core form state, schema resolution, and update-mode logic
|
|
@@ -1228,6 +1334,29 @@ interface UseFormEngineOptions {
|
|
|
1228
1334
|
* synchronous -- pre-load referenced schemas into a map and pass a lookup.
|
|
1229
1335
|
*/
|
|
1230
1336
|
refResolver?: RefResolver;
|
|
1337
|
+
/**
|
|
1338
|
+
* Externally-supplied errors (e.g. a server 422), keyed by JSON Pointer.
|
|
1339
|
+
* Merged into `fieldErrors` (so they affect `isValid` and block Save) with
|
|
1340
|
+
* the lowest precedence -- a built-in error on the same field wins. The
|
|
1341
|
+
* renderer shows them regardless of `validationDisplay`; see the
|
|
1342
|
+
* custom-validation design.
|
|
1343
|
+
*/
|
|
1344
|
+
extraErrors?: FieldError[];
|
|
1345
|
+
/**
|
|
1346
|
+
* Custom validator registry. Field validators (named by `x-ui-validate` on a
|
|
1347
|
+
* property) and, at the root, form/cross-field validators (named on the root
|
|
1348
|
+
* object) run here and merge into `fieldErrors` between built-in errors and
|
|
1349
|
+
* extraErrors. The sync `field`/`form` namespaces run every pass via
|
|
1350
|
+
* `runValidators`; the async `asyncField`/`asyncForm` namespaces are invoked
|
|
1351
|
+
* by `useAsyncValidators` and gate Save (`isValidating`) while in flight.
|
|
1352
|
+
*/
|
|
1353
|
+
validators?: ValidatorRegistry;
|
|
1354
|
+
/**
|
|
1355
|
+
* Whether this engine is the ROOT form (not a nested sub-form). Root-only
|
|
1356
|
+
* concerns -- currently the whole-form/cross-field validators -- run only when
|
|
1357
|
+
* true, so a nested sub-form does not re-run them against its slice.
|
|
1358
|
+
*/
|
|
1359
|
+
isRoot?: boolean;
|
|
1231
1360
|
}
|
|
1232
1361
|
interface UseFormEngineResult {
|
|
1233
1362
|
/** The current working form data (for controlled field values) */
|
|
@@ -1243,17 +1372,23 @@ interface UseFormEngineResult {
|
|
|
1243
1372
|
orderedProperties: [string, JsonSchemaProperty][];
|
|
1244
1373
|
hasChanges: boolean;
|
|
1245
1374
|
isValid: boolean;
|
|
1375
|
+
/** True while any async validator's promise is in flight. */
|
|
1376
|
+
isValidating: boolean;
|
|
1246
1377
|
/**
|
|
1247
1378
|
* Current validation errors, each attributed to its field by JSON Pointer.
|
|
1248
1379
|
* Lets the renderer surface problems for fields that are not on screen.
|
|
1249
1380
|
*/
|
|
1250
1381
|
fieldErrors: FieldError[];
|
|
1382
|
+
/** Fields the user has touched (focused then blurred). */
|
|
1383
|
+
touched: Record<string, boolean>;
|
|
1384
|
+
/** Whether a save has been attempted (reveals all errors under any policy). */
|
|
1385
|
+
submitAttempted: boolean;
|
|
1251
1386
|
handleFieldChange: (fieldName: string, value: any) => void;
|
|
1252
1387
|
handleFieldBlur: (fieldName: string) => void;
|
|
1253
1388
|
handleSave: () => void;
|
|
1254
1389
|
handleCancel: () => void;
|
|
1255
1390
|
}
|
|
1256
|
-
declare function useFormEngine({ schema, data, uiSchema, onChange, onBlur, onValidationChange, onUpdate, onCancel, readOnly, showActions, deferChanges, deferStateUpdates, allowPristineSave, refResolver, }: UseFormEngineOptions): UseFormEngineResult;
|
|
1391
|
+
declare function useFormEngine({ schema, data, uiSchema, onChange, onBlur, onValidationChange, onUpdate, onCancel, readOnly, showActions, deferChanges, deferStateUpdates, allowPristineSave, refResolver, extraErrors, validators, isRoot, }: UseFormEngineOptions): UseFormEngineResult;
|
|
1257
1392
|
|
|
1258
1393
|
/**
|
|
1259
1394
|
* useResolvedSchema - Schema resolution and property ordering
|
|
@@ -1299,6 +1434,30 @@ type NestedRenderer = (props: Omit<JsonSchemaFormRendererProps, "onUpdate" | "on
|
|
|
1299
1434
|
interface NestedFormContextValue {
|
|
1300
1435
|
renderer: NestedRenderer;
|
|
1301
1436
|
templates: TemplateRegistry;
|
|
1437
|
+
/**
|
|
1438
|
+
* The whole form's data, carried down so a nested renderer can evaluate an
|
|
1439
|
+
* `x-ui-rule` scope against the form root rather than its own slice.
|
|
1440
|
+
*
|
|
1441
|
+
* Every other JSON Pointer in the vocabulary (`editablePaths`, validation
|
|
1442
|
+
* error pointers) is root-anchored; without this, a rule on a nested field
|
|
1443
|
+
* could only ever see its immediate object, so a rule referencing a
|
|
1444
|
+
* top-level field silently never fired.
|
|
1445
|
+
*
|
|
1446
|
+
* The outermost renderer sets this to its own data; nested renderers inherit
|
|
1447
|
+
* it unchanged.
|
|
1448
|
+
*/
|
|
1449
|
+
rootData: Record<string, unknown>;
|
|
1450
|
+
/**
|
|
1451
|
+
* Whether the root form's save has been attempted. A nested sub-form is a
|
|
1452
|
+
* separate form-engine instance with its own submit state, which the root's
|
|
1453
|
+
* Save never flips -- so without this a required field inside a nested object
|
|
1454
|
+
* would never reveal its error on submit (only on touch). Carried down so a
|
|
1455
|
+
* nested renderer ORs it into its own error-visibility.
|
|
1456
|
+
*
|
|
1457
|
+
* The outermost renderer sets this to its own submitAttempted; nested
|
|
1458
|
+
* renderers inherit it unchanged.
|
|
1459
|
+
*/
|
|
1460
|
+
submitAttempted: boolean;
|
|
1302
1461
|
}
|
|
1303
1462
|
|
|
1304
1463
|
/**
|
|
@@ -1397,4 +1556,4 @@ declare const DEFAULT_TEMPLATES: TemplateRegistry;
|
|
|
1397
1556
|
*/
|
|
1398
1557
|
declare function groupTemplateKey(variant: string | undefined): string;
|
|
1399
1558
|
|
|
1400
|
-
export { type ArrayHeaderTemplate, type ArrayHeaderTemplateProps, type ArrayItemTemplate, type ArrayItemTemplateProps, ArrayWidget, CheckboxWidget, DEFAULT_TEMPLATES, DEFAULT_WIDGETS, DEFAULT_WIDGET_MAP, DateTimeWidget, DateWidget, DisplayWidget, EntitySelectWidget, ExtraPropertiesCard, type FieldGroup, FormActions, FormCheckbox, FormFieldRenderer, FormFieldWrapper, type FormFieldWrapperProps, FormInput, FormMultiSelect, FormNumberInput, FormQuantityInput, FormSelect, FormTagsInput, FormTextarea, GroupErrorBadge, type GroupLayoutTemplate, type GroupLayoutTemplateProps, JsonEditorWidget, JsonSchemaFormRenderer, type JsonSchemaFormRendererProps, type NestedFormContextValue, type NestedRenderer, NumberWidget, type ObjectContainerTemplate, type ObjectContainerTemplateProps, ObjectWidget, OneOfWidget, type QuantityValue, QuantityWidget, SectionsGroupTemplate, type SelectOption, SelectWidget, StepperGroupTemplate, TabsGroupTemplate, TagsInputWidget, type TemplateRegistry, TextInputWidget, TextareaWidget, TimeWidget, type UnitOption, type UnitPosition, type Widget, type WidgetDescriptor, type WidgetMap, type WidgetProps, type WidgetRegistry, firstError, getAriaDescribedBy, getDefaultWidgetForType, groupTemplateKey, resolveWidget, useFormEngine, useNestedFormContext, useResolvedSchema };
|
|
1559
|
+
export { type ArrayHeaderTemplate, type ArrayHeaderTemplateProps, type ArrayItemTemplate, type ArrayItemTemplateProps, ArrayWidget, type AsyncFieldValidator, type AsyncFormValidator, CheckboxWidget, DEFAULT_TEMPLATES, DEFAULT_WIDGETS, DEFAULT_WIDGET_MAP, DateTimeWidget, DateWidget, DisplayWidget, EntitySelectWidget, ExtraPropertiesCard, type FieldGroup, type FieldValidator, FormActions, FormCheckbox, FormFieldRenderer, FormFieldWrapper, type FormFieldWrapperProps, FormInput, FormMultiSelect, FormNumberInput, FormQuantityInput, FormSelect, FormTagsInput, FormTextarea, type FormValidator, GroupErrorBadge, type GroupLayoutTemplate, type GroupLayoutTemplateProps, JsonEditorWidget, JsonSchemaFormRenderer, type JsonSchemaFormRendererProps, type NestedFormContextValue, type NestedRenderer, NumberWidget, type ObjectContainerTemplate, type ObjectContainerTemplateProps, ObjectWidget, OneOfWidget, type QuantityValue, QuantityWidget, SectionsGroupTemplate, type SelectOption, SelectWidget, StepperGroupTemplate, TabsGroupTemplate, TagsInputWidget, type TemplateRegistry, TextInputWidget, TextareaWidget, TimeWidget, type UnitOption, type UnitPosition, type ValidatorContext, type ValidatorRegistry, type ValidatorResult, type Widget, type WidgetDescriptor, type WidgetMap, type WidgetProps, type WidgetRegistry, firstError, getAriaDescribedBy, getDefaultWidgetForType, groupTemplateKey, resolveWidget, useFormEngine, useNestedFormContext, useResolvedSchema };
|