@petrarca/sonnet-forms 0.6.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/dist/index.js CHANGED
@@ -1483,16 +1483,18 @@ function FormActions({
1483
1483
  disabled = false,
1484
1484
  hasChanges,
1485
1485
  isValid,
1486
+ saveWhenInvalid = "reveal",
1486
1487
  saveLabel = "Save",
1487
1488
  cancelLabel = "Cancel",
1488
1489
  showCancel = true,
1489
1490
  leadingActions
1490
1491
  }) {
1492
+ const saveDisabled = disabled || !hasChanges || saveWhenInvalid === "disable" && !isValid;
1491
1493
  return /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between mt-4", children: [
1492
1494
  /* @__PURE__ */ jsx10("div", { className: "flex items-center gap-2", children: leadingActions }),
1493
1495
  /* @__PURE__ */ jsxs9(SimpleGroup, { justify: "flex-end", children: [
1494
1496
  showCancel && /* @__PURE__ */ jsx10(Button3, { variant: "outline", onClick: onCancel, disabled, children: cancelLabel }),
1495
- /* @__PURE__ */ jsx10(Button3, { onClick: onSave, disabled: disabled || !hasChanges || !isValid, children: saveLabel })
1497
+ /* @__PURE__ */ jsx10(Button3, { onClick: onSave, disabled: saveDisabled, children: saveLabel })
1496
1498
  ] })
1497
1499
  ] });
1498
1500
  }
@@ -1533,7 +1535,7 @@ function ExtraPropertiesCard({ properties, schema }) {
1533
1535
  var ExtraPropertiesCard_default = ExtraPropertiesCard;
1534
1536
 
1535
1537
  // src/JsonSchemaFormRenderer.tsx
1536
- import React13, { useCallback as useCallback8, useMemo as useMemo8 } from "react";
1538
+ import React13, { useCallback as useCallback8, useContext as useContext2, useMemo as useMemo8 } from "react";
1537
1539
  import { SimpleStack } from "@petrarca/sonnet-ui";
1538
1540
  import { AlertCircle as AlertCircle2 } from "lucide-react";
1539
1541
  import {
@@ -2194,9 +2196,11 @@ function resolveLayout(uiSchema, propertySchema) {
2194
2196
  return uiSchema?.["x-ui-layout"] ?? propertySchema["x-ui-layout"];
2195
2197
  }
2196
2198
  function ObjectWidget({
2199
+ id,
2197
2200
  propertySchema,
2198
2201
  value,
2199
2202
  onChange,
2203
+ onBlur,
2200
2204
  registry,
2201
2205
  uiSchema,
2202
2206
  disabled,
@@ -2233,7 +2237,9 @@ function ObjectWidget({
2233
2237
  readOnly: readonly,
2234
2238
  widgets: registry,
2235
2239
  uiSchema: effectiveUiSchema,
2236
- displayContents
2240
+ displayContents,
2241
+ idPrefix: id,
2242
+ onBlur: onBlur ? () => onBlur(id, value) : void 0
2237
2243
  }
2238
2244
  );
2239
2245
  if (displayContents) return /* @__PURE__ */ jsx21(Fragment3, { children: content });
@@ -2991,7 +2997,8 @@ function OneOfWidget({
2991
2997
  disabled,
2992
2998
  readOnly: readonly,
2993
2999
  widgets: registry,
2994
- uiSchema: subUiSchema
3000
+ uiSchema: subUiSchema,
3001
+ idPrefix: id
2995
3002
  }
2996
3003
  )
2997
3004
  }
@@ -3414,7 +3421,8 @@ function FormFieldRenderer({
3414
3421
  registry,
3415
3422
  globalOptions = {},
3416
3423
  formData,
3417
- idPrefix = "field"
3424
+ idPrefix = "field",
3425
+ rawErrors
3418
3426
  }) {
3419
3427
  const label = getFieldTitle(fieldName, property);
3420
3428
  const { description, placeholder } = resolvePresentationText(
@@ -3455,7 +3463,8 @@ function FormFieldRenderer({
3455
3463
  disabled: flags.disabled,
3456
3464
  readonly: flags.readOnly,
3457
3465
  options: mergedOptions,
3458
- formData
3466
+ formData,
3467
+ rawErrors
3459
3468
  }
3460
3469
  );
3461
3470
  return applyFieldWidth(widget, uiSchema, mergedOptions);
@@ -3471,7 +3480,8 @@ import {
3471
3480
  resolveSchema,
3472
3481
  resolveRefs,
3473
3482
  validateFormData,
3474
- diffFormData
3483
+ diffFormData,
3484
+ ensureFormItemIds
3475
3485
  } from "@petrarca/sonnet-core/schema";
3476
3486
 
3477
3487
  // src/hooks/orderProperties.ts
@@ -3537,8 +3547,14 @@ function useFormEngine({
3537
3547
  () => resolveRefs(schema, refResolver),
3538
3548
  [schema, refResolver]
3539
3549
  );
3550
+ const seed = useState9(
3551
+ () => ensureFormItemIds(
3552
+ applySchemaDefaults2(data, refResolvedSchema),
3553
+ refResolvedSchema
3554
+ )
3555
+ )[0];
3540
3556
  const [originalData, setOriginalData] = useState9(
3541
- () => applySchemaDefaults2(data, refResolvedSchema)
3557
+ () => seed
3542
3558
  );
3543
3559
  const [originalDataInitialized, setOriginalDataInitialized] = useState9(false);
3544
3560
  const [nonSchemaData, setNonSchemaData] = useState9(
@@ -3550,9 +3566,7 @@ function useFormEngine({
3550
3566
  );
3551
3567
  }
3552
3568
  );
3553
- const [formData, setFormData] = useState9(
3554
- () => applySchemaDefaults2(data, refResolvedSchema)
3555
- );
3569
+ const [formData, setFormData] = useState9(() => seed);
3556
3570
  const formDataRef = useRef5(formData);
3557
3571
  formDataRef.current = formData;
3558
3572
  const [pendingChanges, setPendingChanges] = useState9({});
@@ -3567,6 +3581,9 @@ function useFormEngine({
3567
3581
  return orderProperties(properties2, orderArray);
3568
3582
  }, [resolvedSchema, uiSchema]);
3569
3583
  const [initialDefaultsSent, setInitialDefaultsSent] = useState9(false);
3584
+ const [edited, setEdited] = useState9({});
3585
+ const [touched, setTouched] = useState9({});
3586
+ const [submitAttempted, setSubmitAttempted] = useState9(false);
3570
3587
  const properties = getSchemaProperties(resolvedSchema);
3571
3588
  useEffect3(() => {
3572
3589
  if (!initialDefaultsSent) {
@@ -3597,7 +3614,10 @@ function useFormEngine({
3597
3614
  useEffect3(() => {
3598
3615
  if (isEqual(prevDataRef.current, data)) return;
3599
3616
  prevDataRef.current = data;
3600
- const newData = applySchemaDefaults2(data, refResolvedSchema);
3617
+ const newData = ensureFormItemIds(
3618
+ applySchemaDefaults2(data, refResolvedSchema),
3619
+ refResolvedSchema
3620
+ );
3601
3621
  setFormData(newData);
3602
3622
  const schemaPropertyNames = Object.keys(properties);
3603
3623
  const result = computeBaselineUpdate(
@@ -3644,6 +3664,9 @@ function useFormEngine({
3644
3664
  const handleFieldChange = useCallback7(
3645
3665
  (fieldName, value) => {
3646
3666
  if (readOnly) return;
3667
+ setEdited(
3668
+ (prev) => prev[fieldName] ? prev : { ...prev, [fieldName]: true }
3669
+ );
3647
3670
  const immediate = isImmediateField(fieldName, properties);
3648
3671
  if (deferStateUpdates && !immediate) {
3649
3672
  setPendingChanges((prev) => ({ ...prev, [fieldName]: value }));
@@ -3684,6 +3707,11 @@ function useFormEngine({
3684
3707
  const handleFieldBlur = useCallback7(
3685
3708
  (fieldName) => {
3686
3709
  if (readOnly) return;
3710
+ if (edited[fieldName]) {
3711
+ setTouched(
3712
+ (prev) => prev[fieldName] ? prev : { ...prev, [fieldName]: true }
3713
+ );
3714
+ }
3687
3715
  flushPendingField(fieldName);
3688
3716
  const latest = formDataRef.current;
3689
3717
  if (!showActions && deferChanges && onChange) {
@@ -3697,6 +3725,7 @@ function useFormEngine({
3697
3725
  },
3698
3726
  [
3699
3727
  readOnly,
3728
+ edited,
3700
3729
  pendingChanges,
3701
3730
  deferChanges,
3702
3731
  deferStateUpdates,
@@ -3707,6 +3736,7 @@ function useFormEngine({
3707
3736
  ]
3708
3737
  );
3709
3738
  const handleSave = useCallback7(() => {
3739
+ setSubmitAttempted(true);
3710
3740
  if (!hasChanges && !allowPristineSave) return;
3711
3741
  const finalData = deferStateUpdates && Object.keys(pendingChanges).length > 0 ? { ...formData, ...pendingChanges } : formData;
3712
3742
  const completeData = { ...nonSchemaData, ...finalData };
@@ -3736,6 +3766,9 @@ function useFormEngine({
3736
3766
  const handleCancel = useCallback7(() => {
3737
3767
  setFormData({ ...originalData });
3738
3768
  setPendingChanges({});
3769
+ setEdited({});
3770
+ setTouched({});
3771
+ setSubmitAttempted(false);
3739
3772
  onCancel?.();
3740
3773
  }, [originalData, onCancel]);
3741
3774
  return {
@@ -3746,6 +3779,8 @@ function useFormEngine({
3746
3779
  hasChanges,
3747
3780
  isValid,
3748
3781
  fieldErrors,
3782
+ touched,
3783
+ submitAttempted,
3749
3784
  handleFieldChange,
3750
3785
  handleFieldBlur,
3751
3786
  handleSave,
@@ -3754,6 +3789,10 @@ function useFormEngine({
3754
3789
  }
3755
3790
 
3756
3791
  // src/JsonSchemaFormRenderer.tsx
3792
+ import {
3793
+ useValidationDisplay,
3794
+ useSaveWhenInvalid
3795
+ } from "@petrarca/sonnet-core/config";
3757
3796
  import { jsx as jsx37, jsxs as jsxs23 } from "react/jsx-runtime";
3758
3797
  function stripFids(data) {
3759
3798
  const result = {};
@@ -3901,6 +3940,22 @@ function layoutFieldsInRows(fields, rowDefs, defaultRowGap) {
3901
3940
  );
3902
3941
  return elements.concat(collectUnplacedFields(fields, placed));
3903
3942
  }
3943
+ function useRootFormData(ownData) {
3944
+ return useContext2(NestedFormContext)?.rootData ?? ownData;
3945
+ }
3946
+ function blurHandlerNeeded({
3947
+ deferStateUpdates,
3948
+ deferChanges,
3949
+ hasOnBlur,
3950
+ validationDisplay
3951
+ }) {
3952
+ return deferStateUpdates || deferChanges || hasOnBlur || validationDisplay === "onTouched";
3953
+ }
3954
+ function fieldErrorsVisible(validationDisplay, submitAttempted, isTouched) {
3955
+ if (validationDisplay === "always") return true;
3956
+ if (submitAttempted) return true;
3957
+ return validationDisplay === "onTouched" && !!isTouched;
3958
+ }
3904
3959
  function pointerRoot(pointer) {
3905
3960
  const token = pointer.split("/")[1] ?? "";
3906
3961
  return token.replace(/~1/g, "/").replace(/~0/g, "~");
@@ -3973,6 +4028,7 @@ function FormContainer({
3973
4028
  disabled,
3974
4029
  hasChanges,
3975
4030
  isValid,
4031
+ saveWhenInvalid,
3976
4032
  saveLabel,
3977
4033
  cancelLabel,
3978
4034
  showCancel,
@@ -3995,6 +4051,7 @@ function FormContainer({
3995
4051
  disabled,
3996
4052
  hasChanges,
3997
4053
  isValid,
4054
+ saveWhenInvalid,
3998
4055
  saveLabel,
3999
4056
  cancelLabel,
4000
4057
  showCancel,
@@ -4037,6 +4094,8 @@ function JsonSchemaFormRenderer(props) {
4037
4094
  hasChanges,
4038
4095
  isValid,
4039
4096
  fieldErrors,
4097
+ touched,
4098
+ submitAttempted,
4040
4099
  handleFieldChange,
4041
4100
  handleFieldBlur,
4042
4101
  handleSave,
@@ -4081,15 +4140,35 @@ function JsonSchemaFormRenderer(props) {
4081
4140
  ),
4082
4141
  [resolvedTemplates]
4083
4142
  );
4143
+ const rootData = useRootFormData(displayData);
4084
4144
  const contextValue = useMemo8(
4085
- () => ({ renderer: nestedRenderer, templates: resolvedTemplates }),
4086
- [nestedRenderer, resolvedTemplates]
4145
+ () => ({
4146
+ renderer: nestedRenderer,
4147
+ templates: resolvedTemplates,
4148
+ rootData
4149
+ }),
4150
+ [nestedRenderer, resolvedTemplates, rootData]
4151
+ );
4152
+ const validationDisplay = useValidationDisplay(props.validationDisplay);
4153
+ const saveWhenInvalid = useSaveWhenInvalid(props.saveWhenInvalid);
4154
+ const needsBlurHandler = blurHandlerNeeded({
4155
+ deferStateUpdates,
4156
+ deferChanges,
4157
+ hasOnBlur: !!onBlur,
4158
+ validationDisplay
4159
+ });
4160
+ const showFieldErrors = useCallback8(
4161
+ (fieldName) => fieldErrorsVisible(
4162
+ validationDisplay,
4163
+ submitAttempted,
4164
+ touched[fieldName]
4165
+ ),
4166
+ [validationDisplay, submitAttempted, touched]
4087
4167
  );
4088
- const needsBlurHandler = deferStateUpdates || !!onBlur;
4089
4168
  const fields = useMemo8(
4090
4169
  () => orderedProperties.flatMap(([fieldName, property]) => {
4091
4170
  const rule = uiSchema[fieldName]?.["x-ui-rule"] ?? property["x-ui-rule"];
4092
- const { visible, enabled } = evaluateUiRule(rule, displayData);
4171
+ const { visible, enabled } = evaluateUiRule(rule, rootData);
4093
4172
  if (!visible) return [];
4094
4173
  return [
4095
4174
  /* @__PURE__ */ jsx37(
@@ -4108,7 +4187,8 @@ function JsonSchemaFormRenderer(props) {
4108
4187
  registry: widgets,
4109
4188
  globalOptions,
4110
4189
  formData: displayData,
4111
- idPrefix
4190
+ idPrefix,
4191
+ rawErrors: showFieldErrors(fieldName) ? fieldErrors.filter((e) => pointerRoot(e.pointer) === fieldName).map((e) => e.message) : void 0
4112
4192
  },
4113
4193
  fieldName
4114
4194
  )
@@ -4117,6 +4197,7 @@ function JsonSchemaFormRenderer(props) {
4117
4197
  [
4118
4198
  orderedProperties,
4119
4199
  displayData,
4200
+ rootData,
4120
4201
  resolvedSchema,
4121
4202
  disabled,
4122
4203
  readOnly,
@@ -4126,6 +4207,8 @@ function JsonSchemaFormRenderer(props) {
4126
4207
  globalOptions,
4127
4208
  idPrefix,
4128
4209
  needsBlurHandler,
4210
+ showFieldErrors,
4211
+ fieldErrors,
4129
4212
  handleFieldChange,
4130
4213
  handleFieldBlur
4131
4214
  ]
@@ -4145,7 +4228,8 @@ function JsonSchemaFormRenderer(props) {
4145
4228
  groupDefs,
4146
4229
  defaultRowGap,
4147
4230
  fieldErrors,
4148
- displayData,
4231
+ // Tab rules are x-ui-rule too: root-anchored like field rules.
4232
+ rootData,
4149
4233
  groupTemplate
4150
4234
  );
4151
4235
  }
@@ -4159,15 +4243,15 @@ function JsonSchemaFormRenderer(props) {
4159
4243
  groupDefs,
4160
4244
  defaultRowGap,
4161
4245
  fieldErrors,
4162
- displayData,
4246
+ rootData,
4163
4247
  groupTemplate
4164
4248
  ]);
4165
4249
  const hiddenErrorCount = useMemo8(() => {
4166
4250
  if (!groupDefs?.length || fieldErrors.length === 0) return 0;
4167
- const hidden = hiddenGroupFieldNames(groupDefs, displayData);
4251
+ const hidden = hiddenGroupFieldNames(groupDefs, rootData);
4168
4252
  if (hidden.size === 0) return 0;
4169
4253
  return fieldErrors.filter((e) => hidden.has(pointerRoot(e.pointer))).length;
4170
- }, [groupDefs, fieldErrors, displayData]);
4254
+ }, [groupDefs, fieldErrors, rootData]);
4171
4255
  return /* @__PURE__ */ jsx37(NestedFormContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx37(
4172
4256
  FormContainer,
4173
4257
  {
@@ -4181,6 +4265,7 @@ function JsonSchemaFormRenderer(props) {
4181
4265
  disabled,
4182
4266
  hasChanges: hasChanges || allowPristineSave,
4183
4267
  isValid,
4268
+ saveWhenInvalid,
4184
4269
  saveLabel,
4185
4270
  cancelLabel,
4186
4271
  showCancel,