@petrarca/sonnet-forms
v0.4.2
Published
Schema-driven form renderer, field components, and widgets for the Petrarca Sonnet component library
Downloads
633
Readme
@petrarca/sonnet-forms
Schema-driven form renderer and field components for the Petrarca Sonnet component library.
What's included
JsonSchemaFormRenderer — Renders a complete form from a JSON Schema with automatic widget resolution, validation, nested objects, arrays, and custom UI schema overrides.
Form field components — Standalone fields with label, description, error,
and compact mode: FormInput, FormTextarea, FormSelect, FormMultiSelect,
FormCheckbox, FormTagsInput, FormNumberInput, FormQuantityInput.
Widget system — Extensible widget registry mapping schema types/formats to UI controls. Built-in widgets for text, number, select, checkbox, textarea, tags, quantity, arrays, objects, entity references, and JSON editing.
Install
pnpm add @petrarca/sonnet-forms @petrarca/sonnet-ui @petrarca/sonnet-corePeer dependencies: react >=19, react-dom >=19, tailwindcss.
Basic usage
JsonSchemaFormRenderer
import { JsonSchemaFormRenderer } from "@petrarca/sonnet-forms";
const schema = {
type: "object",
properties: {
name: { type: "string", title: "Name" },
email: { type: "string", format: "email", title: "Email" },
age: { type: "integer", title: "Age" },
},
required: ["name", "email"],
};
<JsonSchemaFormRenderer
schema={schema}
data={formData}
onUpdate={(data, changedFields) => save(data)}
onCancel={handleClose}
showActions
saveLabel="Save"
/>Key props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| schema | JsonSchema | required | JSON Schema describing the form |
| data | Record<string, unknown> | required | Current form values |
| onUpdate | (data, changedFields, diff) => void | — | Called on save |
| onCancel | () => void | — | Called on cancel |
| onChange | (data) => void | — | Called on every field change (use instead of onUpdate when managing your own buttons) |
| showActions | boolean | false | Render built-in Save / Cancel buttons inside the form |
| showCancel | boolean | true | Show the Cancel button (requires showActions) |
| saveLabel | string | "Save" | Save button label |
| cancelLabel | string | "Cancel" | Cancel button label |
| showExtraProperties | boolean | false | Preserve form keys not in the schema. Set true for open-ended data (e.g. graph node properties) |
| uiSchema | Record<string, UISchema> | — | Per-field UI overrides |
| widgets | WidgetRegistry | DEFAULT_WIDGETS | Custom widget map |
| deferStateUpdates | boolean | false | Update state on blur instead of on change |
Schema construction
The recommended pattern is Zod + toJsonSchema() with .meta() for UI hints:
import { z } from "zod";
import { toJsonSchema } from "@petrarca/sonnet-core/schema";
const UserSchema = z.object({
name: z.string().meta({
"x-ui-title": "Full name",
"x-ui-description": "As it appears on official documents.",
}),
role: z.enum(["admin", "user"]).meta({
"x-ui-title": "Role",
"x-ui-options": { enumNames: ["Administrator", "Standard user"] },
}),
bio: z.string().optional().meta({
"x-ui-title": "Bio",
"x-ui-widget": "textarea",
}),
});
// Convert once, store as a module-level constant:
const USER_FORM_SCHEMA = toJsonSchema(UserSchema);Or write the JSON Schema directly for dynamic / server-driven forms.
Schema UI annotations
Annotate individual fields with x-ui-* to control rendering:
| Annotation | Purpose |
|---|---|
| "x-ui-title" | Override the field label (false to hide) |
| "x-ui-description" | Override the field description |
| "x-ui-widget" | Force a specific widget: "textarea", "tags", "entity-select", or a custom key |
| "x-ui-options" | Widget-specific options (see widget docs) |
| "x-ui-order" | Array of field names to control rendering order in an object |
| "x-ui-layout" | Array layout config: { direction: "horizontal", columns, gap, compact } |
onChange mode (external buttons)
When you want to drive your own Submit button outside the form, omit showActions
and use onChange instead of onUpdate:
const [formData, setFormData] = useState({});
<JsonSchemaFormRenderer
schema={schema}
data={formData}
onChange={setFormData}
// no showActions — caller owns the buttons
/>
<Button onClick={() => submit(formData)}>Submit</Button>UISchema
Override rendering per-field without modifying the schema:
<JsonSchemaFormRenderer
schema={schema}
data={data}
uiSchema={{
name: { "x-ui-title": "Display name" }, // override label
internal_id: { "x-ui-title": false }, // hide label entirely
notes: { "x-ui-widget": "textarea" }, // force widget
}}
onUpdate={onUpdate}
showActions
/>Custom widgets
Extend DEFAULT_WIDGETS with your own widget components:
import { DEFAULT_WIDGETS, type WidgetRegistry, type WidgetProps } from "@petrarca/sonnet-forms";
function MyColorWidget({ value, onChange, schema }: WidgetProps) {
return (
<input
type="color"
value={String(value ?? "#000000")}
onChange={(e) => onChange(e.target.value)}
/>
);
}
export const MY_WIDGETS: WidgetRegistry = {
...DEFAULT_WIDGETS,
"color-picker": MyColorWidget, // key matches "x-ui-widget": "color-picker" in schema
};
// Usage:
<JsonSchemaFormRenderer schema={schema} data={data} widgets={MY_WIDGETS} onUpdate={onUpdate} showActions />WidgetProps includes formData (the full current form state) — use this to
build dependent widgets that react to sibling field values.
Dynamic schema (edit vs. create)
To hide fields that are immutable after creation, delete them from a cloned
schema. Also exclude them from data — if a key exists in data but not
the schema, the form engine treats it as a dirty field immediately:
const schema = useMemo(() => {
if (!isEditing) return BASE_SCHEMA;
const s = structuredClone(BASE_SCHEMA);
delete s.properties?.app_key; // read-only after creation
return s;
}, [isEditing]);
const data = isEditing
? { name: entity.name } // omit app_key from data too
: {};Standalone field components
Use field components directly outside the renderer — in sidebars, toolbars, or any custom form layout:
import {
FormInput, FormTextarea, FormSelect,
FormCheckbox, FormMultiSelect, FormTagsInput,
} from "@petrarca/sonnet-forms";
<FormInput
label="Name"
description="Your full name"
value={name}
onChange={(e) => setName(e.currentTarget.value)}
error={nameError}
/>
<FormSelect
label="Role"
options={[{ value: "admin", label: "Admin" }, { value: "user", label: "User" }]}
value={role}
onChange={setRole}
clearable={false}
/>
<FormCheckbox
label="Active"
checked={active}
onChange={(checked) => setActive(checked === true)}
/>
<FormTextarea
label="Notes"
value={notes}
onChange={(e) => setNotes(e.currentTarget.value)}
autosize
minRows={3}
rightSection={<ClearButton />}
rightSectionWidth={40}
/>Common props on all field components:
| Prop | Purpose |
|---|---|
| label | Field label. Pass false to suppress. |
| description | Help text below the field |
| error | Error message (also sets error styling) |
| compact | Tighter vertical spacing |
| disabled | Disables the field |
| wrapperClassName | Class on the outer wrapper div |
License
See LICENSE.md.
