@petrarca/sonnet-forms 0.7.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 CHANGED
@@ -91,8 +91,8 @@ actually enforces are in [getting started](docs/usage/getting-started.md).
91
91
  [templates](docs/usage/templates.md),
92
92
  [field components](docs/usage/field-components.md) and
93
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.
94
+ - [Design docs](docs/design/index.md) -- form layout, nested fields, the
95
+ change-tracking model, and custom validation.
96
96
  - [Workspace documentation](../../docs/README.md) -- how the packages layer and
97
97
  the shared schema extension contract.
98
98
 
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ 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, FieldError } from '@petrarca/sonnet-core/schema';
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
8
  import { SaveWhenInvalid, ValidationDisplay } from '@petrarca/sonnet-core/config';
@@ -673,13 +673,18 @@ interface FormActionsProps {
673
673
  saveLabel?: string;
674
674
  cancelLabel?: string;
675
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;
676
681
  /**
677
682
  * Optional content rendered left-aligned in the action row (e.g. a secondary
678
683
  * action like "Test connection"). Cancel/Save stay grouped on the right.
679
684
  */
680
685
  leadingActions?: ReactNode;
681
686
  }
682
- declare function FormActions({ onSave, onCancel, disabled, hasChanges, isValid, saveWhenInvalid, 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;
683
688
 
684
689
  type Props = {
685
690
  properties: Record<string, any>;
@@ -1026,6 +1031,71 @@ declare function getDefaultWidgetForType(type: string): string;
1026
1031
  */
1027
1032
  declare const DEFAULT_WIDGETS: WidgetRegistry;
1028
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
+
1029
1099
  /**
1030
1100
  * JsonSchemaFormRenderer - Generic form renderer based on JSON Schema
1031
1101
  *
@@ -1132,6 +1202,22 @@ type JsonSchemaFormRendererProps = {
1132
1202
  * a partial registry -- unspecified keys fall back to DEFAULT_TEMPLATES.
1133
1203
  */
1134
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[];
1135
1221
  /**
1136
1222
  * When true, renders with `display: contents` instead of a SimpleStack.
1137
1223
  *
@@ -1248,6 +1334,29 @@ interface UseFormEngineOptions {
1248
1334
  * synchronous -- pre-load referenced schemas into a map and pass a lookup.
1249
1335
  */
1250
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;
1251
1360
  }
1252
1361
  interface UseFormEngineResult {
1253
1362
  /** The current working form data (for controlled field values) */
@@ -1263,6 +1372,8 @@ interface UseFormEngineResult {
1263
1372
  orderedProperties: [string, JsonSchemaProperty][];
1264
1373
  hasChanges: boolean;
1265
1374
  isValid: boolean;
1375
+ /** True while any async validator's promise is in flight. */
1376
+ isValidating: boolean;
1266
1377
  /**
1267
1378
  * Current validation errors, each attributed to its field by JSON Pointer.
1268
1379
  * Lets the renderer surface problems for fields that are not on screen.
@@ -1277,7 +1388,7 @@ interface UseFormEngineResult {
1277
1388
  handleSave: () => void;
1278
1389
  handleCancel: () => void;
1279
1390
  }
1280
- 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;
1281
1392
 
1282
1393
  /**
1283
1394
  * useResolvedSchema - Schema resolution and property ordering
@@ -1336,6 +1447,17 @@ interface NestedFormContextValue {
1336
1447
  * it unchanged.
1337
1448
  */
1338
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;
1339
1461
  }
1340
1462
 
1341
1463
  /**
@@ -1434,4 +1556,4 @@ declare const DEFAULT_TEMPLATES: TemplateRegistry;
1434
1556
  */
1435
1557
  declare function groupTemplateKey(variant: string | undefined): string;
1436
1558
 
1437
- 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 };