@petrarca/sonnet-forms 0.5.0 → 0.7.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 +232 -6
- package/dist/index.js +755 -126
- package/dist/index.js.map +1 -1
- package/dist/widget-options.js +72 -1
- package/dist/widget-options.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 and the
|
|
95
|
+
change-tracking model.
|
|
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).
|