@fogpipe/forma-react 0.9.0 → 0.10.1

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.d.ts CHANGED
@@ -273,6 +273,11 @@ interface FieldWrapperProps {
273
273
  errors: FieldError[];
274
274
  touched: boolean;
275
275
  required: boolean;
276
+ /**
277
+ * Whether to show the required indicator in the UI.
278
+ * False for boolean fields since false is a valid answer.
279
+ */
280
+ showRequiredIndicator: boolean;
276
281
  visible: boolean;
277
282
  }
278
283
  /**
@@ -362,8 +367,13 @@ interface GetFieldPropsResult {
362
367
  visible: boolean;
363
368
  /** Whether field is enabled (not disabled) */
364
369
  enabled: boolean;
365
- /** Whether field is required */
370
+ /** Whether field is required (for validation) */
366
371
  required: boolean;
372
+ /**
373
+ * Whether to show the required indicator in the UI.
374
+ * False for boolean fields since false is a valid answer.
375
+ */
376
+ showRequiredIndicator: boolean;
367
377
  /** Whether field has been touched */
368
378
  touched: boolean;
369
379
  /** Validation errors for this field */
package/dist/index.js CHANGED
@@ -50,6 +50,18 @@ function formReducer(state, action) {
50
50
  return state;
51
51
  }
52
52
  }
53
+ function getDefaultBooleanValues(spec) {
54
+ var _a;
55
+ const defaults = {};
56
+ for (const fieldPath of spec.fieldOrder) {
57
+ const schemaProperty = (_a = spec.schema.properties) == null ? void 0 : _a[fieldPath];
58
+ const fieldDef = spec.fields[fieldPath];
59
+ if ((schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean") {
60
+ defaults[fieldPath] = false;
61
+ }
62
+ }
63
+ return defaults;
64
+ }
53
65
  function useForma(options) {
54
66
  const { spec: inputSpec, initialData = {}, onSubmit, onChange, validateOn = "blur", referenceData, validationDebounceMs = 0 } = options;
55
67
  const spec = useMemo(() => {
@@ -63,7 +75,8 @@ function useForma(options) {
63
75
  };
64
76
  }, [inputSpec, referenceData]);
65
77
  const [state, dispatch] = useReducer(formReducer, {
66
- data: initialData,
78
+ data: { ...getDefaultBooleanValues(spec), ...initialData },
79
+ // Boolean defaults merged UNDER initialData
67
80
  touched: {},
68
81
  isSubmitting: false,
69
82
  isSubmitted: false,
@@ -305,23 +318,24 @@ function useForma(options) {
305
318
  return fieldHandlers.current.get(path);
306
319
  }, [setValueAtPath, setFieldTouched]);
307
320
  const getFieldProps = useCallback((path) => {
321
+ var _a;
308
322
  const fieldDef = spec.fields[path];
309
323
  const handlers = getFieldHandlers(path);
310
324
  let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
311
325
  if (!fieldType || fieldType === "computed") {
312
- const schemaProperty = spec.schema.properties[path];
313
- if (schemaProperty) {
314
- if (schemaProperty.type === "number") fieldType = "number";
315
- else if (schemaProperty.type === "integer") fieldType = "integer";
316
- else if (schemaProperty.type === "boolean") fieldType = "boolean";
317
- else if (schemaProperty.type === "array") fieldType = "array";
318
- else if (schemaProperty.type === "object") fieldType = "object";
319
- else if ("enum" in schemaProperty && schemaProperty.enum) fieldType = "select";
320
- else if ("format" in schemaProperty) {
321
- if (schemaProperty.format === "date") fieldType = "date";
322
- else if (schemaProperty.format === "date-time") fieldType = "datetime";
323
- else if (schemaProperty.format === "email") fieldType = "email";
324
- else if (schemaProperty.format === "uri") fieldType = "url";
326
+ const schemaProperty2 = spec.schema.properties[path];
327
+ if (schemaProperty2) {
328
+ if (schemaProperty2.type === "number") fieldType = "number";
329
+ else if (schemaProperty2.type === "integer") fieldType = "integer";
330
+ else if (schemaProperty2.type === "boolean") fieldType = "boolean";
331
+ else if (schemaProperty2.type === "array") fieldType = "array";
332
+ else if (schemaProperty2.type === "object") fieldType = "object";
333
+ else if ("enum" in schemaProperty2 && schemaProperty2.enum) fieldType = "select";
334
+ else if ("format" in schemaProperty2) {
335
+ if (schemaProperty2.format === "date") fieldType = "date";
336
+ else if (schemaProperty2.format === "date-time") fieldType = "datetime";
337
+ else if (schemaProperty2.format === "email") fieldType = "email";
338
+ else if (schemaProperty2.format === "uri") fieldType = "url";
325
339
  }
326
340
  }
327
341
  }
@@ -331,6 +345,10 @@ function useForma(options) {
331
345
  const displayedErrors = showErrors ? fieldErrors : [];
332
346
  const hasErrors = displayedErrors.length > 0;
333
347
  const isRequired = required[path] ?? false;
348
+ const schemaProperty = spec.schema.properties[path];
349
+ const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
350
+ const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
351
+ const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
334
352
  return {
335
353
  name: path,
336
354
  value: getValueAtPath(path),
@@ -341,6 +359,7 @@ function useForma(options) {
341
359
  visible: visibility[path] !== false,
342
360
  enabled: enabled[path] !== false,
343
361
  required: isRequired,
362
+ showRequiredIndicator,
344
363
  touched: isTouched,
345
364
  errors: displayedErrors,
346
365
  onChange: handlers.onChange,
@@ -387,6 +406,8 @@ function useForma(options) {
387
406
  enabled: enabled[path] !== false,
388
407
  required: false,
389
408
  // TODO: Evaluate item field required
409
+ showRequiredIndicator: false,
410
+ // Item fields don't show required indicator
390
411
  touched: isTouched,
391
412
  errors: showErrors ? fieldErrors : [],
392
413
  onChange: handlers.onChange,
@@ -490,7 +511,7 @@ function DefaultLayout({ children, onSubmit, isSubmitting }) {
490
511
  }
491
512
  );
492
513
  }
493
- function DefaultFieldWrapper({ fieldPath, field, children, errors, required, visible }) {
514
+ function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredIndicator, visible }) {
494
515
  if (!visible) return null;
495
516
  const errorId = `${fieldPath}-error`;
496
517
  const descriptionId = field.description ? `${fieldPath}-description` : void 0;
@@ -498,8 +519,8 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, required, vis
498
519
  return /* @__PURE__ */ jsxs("div", { className: "field-wrapper", "data-field-path": fieldPath, children: [
499
520
  field.label && /* @__PURE__ */ jsxs("label", { htmlFor: fieldPath, children: [
500
521
  field.label,
501
- required && /* @__PURE__ */ jsx("span", { className: "required", "aria-hidden": "true", children: "*" }),
502
- required && /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (required)" })
522
+ showRequiredIndicator && /* @__PURE__ */ jsx("span", { className: "required", "aria-hidden": "true", children: "*" }),
523
+ showRequiredIndicator && /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (required)" })
503
524
  ] }),
504
525
  children,
505
526
  hasErrors && /* @__PURE__ */ jsx(
@@ -569,7 +590,6 @@ var FormRenderer = forwardRef(
569
590
  validateOn
570
591
  });
571
592
  const fieldRefs = useRef2(/* @__PURE__ */ new Map());
572
- const arrayHelpersCache = useRef2(/* @__PURE__ */ new Map());
573
593
  const focusField = useCallback2((path) => {
574
594
  const element = fieldRefs.current.get(path);
575
595
  element == null ? void 0 : element.focus();
@@ -607,6 +627,7 @@ var FormRenderer = forwardRef(
607
627
  return spec.fieldOrder;
608
628
  }, [spec.pages, spec.fieldOrder, forma.wizard]);
609
629
  const renderField = useCallback2((fieldPath) => {
630
+ var _a;
610
631
  const fieldDef = spec.fields[fieldPath];
611
632
  if (!fieldDef) return null;
612
633
  const isVisible = forma.visibility[fieldPath] !== false;
@@ -623,6 +644,9 @@ var FormRenderer = forwardRef(
623
644
  const required = forma.required[fieldPath] ?? false;
624
645
  const disabled = forma.enabled[fieldPath] === false;
625
646
  const schemaProperty = spec.schema.properties[fieldPath];
647
+ const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
648
+ const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
649
+ const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
626
650
  const baseProps = {
627
651
  name: fieldPath,
628
652
  field: fieldDef,
@@ -660,82 +684,33 @@ var FormRenderer = forwardRef(
660
684
  options: fieldDef.options ?? []
661
685
  };
662
686
  } else if (fieldType === "array" && fieldDef.itemFields) {
663
- const arrayValue = baseProps.value ?? [];
687
+ const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
664
688
  const minItems = fieldDef.minItems ?? 0;
665
689
  const maxItems = fieldDef.maxItems ?? Infinity;
666
690
  const itemFieldDefs = fieldDef.itemFields;
667
- if (!arrayHelpersCache.current.has(fieldPath)) {
668
- arrayHelpersCache.current.set(fieldPath, {
669
- push: (item) => {
670
- const currentArray = forma.data[fieldPath] ?? [];
671
- const newItem = item ?? createDefaultItem(itemFieldDefs);
672
- forma.setFieldValue(fieldPath, [...currentArray, newItem]);
673
- },
674
- insert: (index, item) => {
675
- const currentArray = forma.data[fieldPath] ?? [];
676
- const newArray = [...currentArray];
677
- newArray.splice(index, 0, item);
678
- forma.setFieldValue(fieldPath, newArray);
679
- },
680
- remove: (index) => {
681
- const currentArray = forma.data[fieldPath] ?? [];
682
- const newArray = [...currentArray];
683
- newArray.splice(index, 1);
684
- forma.setFieldValue(fieldPath, newArray);
685
- },
686
- move: (from, to) => {
687
- const currentArray = forma.data[fieldPath] ?? [];
688
- const newArray = [...currentArray];
689
- const [item] = newArray.splice(from, 1);
690
- newArray.splice(to, 0, item);
691
- forma.setFieldValue(fieldPath, newArray);
692
- },
693
- swap: (indexA, indexB) => {
694
- const currentArray = forma.data[fieldPath] ?? [];
695
- const newArray = [...currentArray];
696
- [newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];
697
- forma.setFieldValue(fieldPath, newArray);
698
- }
699
- });
700
- }
701
- const cachedHelpers = arrayHelpersCache.current.get(fieldPath);
691
+ const baseHelpers = forma.getArrayHelpers(fieldPath);
692
+ const pushWithDefault = (item) => {
693
+ const newItem = item ?? createDefaultItem(itemFieldDefs);
694
+ baseHelpers.push(newItem);
695
+ };
696
+ const getItemFieldPropsExtended = (index, fieldName) => {
697
+ const baseProps2 = baseHelpers.getItemFieldProps(index, fieldName);
698
+ const itemFieldDef = itemFieldDefs[fieldName];
699
+ return {
700
+ ...baseProps2,
701
+ itemIndex: index,
702
+ fieldName,
703
+ options: itemFieldDef == null ? void 0 : itemFieldDef.options
704
+ };
705
+ };
702
706
  const helpers = {
703
707
  items: arrayValue,
704
- push: cachedHelpers.push,
705
- insert: cachedHelpers.insert,
706
- remove: cachedHelpers.remove,
707
- move: cachedHelpers.move,
708
- swap: cachedHelpers.swap,
709
- getItemFieldProps: (index, fieldName) => {
710
- var _a;
711
- const itemFieldDef = itemFieldDefs[fieldName];
712
- const itemPath = `${fieldPath}[${index}].${fieldName}`;
713
- const itemValue = (_a = arrayValue[index]) == null ? void 0 : _a[fieldName];
714
- return {
715
- name: itemPath,
716
- value: itemValue,
717
- type: (itemFieldDef == null ? void 0 : itemFieldDef.type) ?? "text",
718
- label: (itemFieldDef == null ? void 0 : itemFieldDef.label) ?? fieldName,
719
- description: itemFieldDef == null ? void 0 : itemFieldDef.description,
720
- placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
721
- visible: true,
722
- enabled: !disabled,
723
- required: (itemFieldDef == null ? void 0 : itemFieldDef.requiredWhen) === "true",
724
- touched: forma.touched[itemPath] ?? false,
725
- errors: forma.errors.filter((e) => e.field === itemPath),
726
- onChange: (value) => {
727
- const currentArray = forma.data[fieldPath] ?? [];
728
- const newArray = [...currentArray];
729
- const item = newArray[index] ?? {};
730
- newArray[index] = { ...item, [fieldName]: value };
731
- forma.setFieldValue(fieldPath, newArray);
732
- },
733
- onBlur: () => forma.setFieldTouched(itemPath),
734
- itemIndex: index,
735
- fieldName,
736
- options: itemFieldDef == null ? void 0 : itemFieldDef.options
737
- };
738
- },
708
+ push: pushWithDefault,
709
+ insert: baseHelpers.insert,
710
+ remove: baseHelpers.remove,
711
+ move: baseHelpers.move,
712
+ swap: baseHelpers.swap,
713
+ getItemFieldProps: getItemFieldPropsExtended,
739
714
  minItems,
740
715
  maxItems,
741
716
  canAdd: arrayValue.length < maxItems,
@@ -768,6 +743,7 @@ var FormRenderer = forwardRef(
768
743
  errors,
769
744
  touched,
770
745
  required,
746
+ showRequiredIndicator,
771
747
  visible: isVisible,
772
748
  children: React.createElement(Component, componentProps)
773
749
  },