@rachelallyson/hero-hook-form 2.6.0 → 2.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.
@@ -76,7 +76,7 @@ function useFormHelper({
76
76
 
77
77
  // src/components/FormField.tsx
78
78
  import React17 from "react";
79
- import { useWatch as useWatch3 } from "react-hook-form";
79
+ import { get, useWatch as useWatch3 } from "react-hook-form";
80
80
 
81
81
  // src/fields/AutocompleteField.tsx
82
82
  import React from "react";
@@ -438,18 +438,23 @@ function FieldArrayField({
438
438
  }) {
439
439
  const {
440
440
  addButtonText = "Add Item",
441
+ defaultItem,
442
+ enableReordering = false,
441
443
  fields: fieldConfigs,
442
444
  max = 10,
443
445
  min = 0,
444
446
  name,
445
- removeButtonText = "Remove"
447
+ removeButtonText = "Remove",
448
+ renderAddButton,
449
+ renderItem,
450
+ reorderButtonText = { down: "\u2193", up: "\u2191" }
446
451
  } = config;
447
452
  const form = useFormContext3();
448
453
  if (!form || !form.control) {
449
454
  return null;
450
455
  }
451
456
  const { control } = form;
452
- const { append, fields, remove } = useFieldArray({
457
+ const { append, fields, move, remove } = useFieldArray({
453
458
  control,
454
459
  name
455
460
  // FieldArray name
@@ -458,18 +463,22 @@ function FieldArrayField({
458
463
  const canRemove = fields.length > min;
459
464
  const handleAdd = () => {
460
465
  if (canAdd) {
461
- const defaultValues = fieldConfigs.reduce((acc, fieldConfig) => {
462
- const fieldName = fieldConfig.name;
463
- if (fieldConfig.type === "checkbox" || fieldConfig.type === "switch") {
464
- acc[fieldName] = false;
465
- } else if (fieldConfig.type === "slider") {
466
- acc[fieldName] = 0;
467
- } else {
468
- acc[fieldName] = "";
469
- }
470
- return acc;
471
- }, {});
472
- append(defaultValues);
466
+ if (defaultItem) {
467
+ append(defaultItem());
468
+ } else {
469
+ const defaultValues = fieldConfigs.reduce((acc, fieldConfig) => {
470
+ const fieldName = fieldConfig.name;
471
+ if (fieldConfig.type === "checkbox" || fieldConfig.type === "switch") {
472
+ acc[fieldName] = false;
473
+ } else if (fieldConfig.type === "slider") {
474
+ acc[fieldName] = 0;
475
+ } else {
476
+ acc[fieldName] = "";
477
+ }
478
+ return acc;
479
+ }, {});
480
+ append(defaultValues);
481
+ }
473
482
  }
474
483
  };
475
484
  const handleRemove = (index) => {
@@ -477,60 +486,131 @@ function FieldArrayField({
477
486
  remove(index);
478
487
  }
479
488
  };
480
- return /* @__PURE__ */ React8.createElement("div", { className }, /* @__PURE__ */ React8.createElement("div", { className: "space-y-4" }, fields.map((field2, index) => /* @__PURE__ */ React8.createElement(
481
- "div",
482
- {
483
- key: field2.id,
484
- className: "border border-gray-200 rounded-lg p-4 space-y-4"
485
- },
486
- /* @__PURE__ */ React8.createElement("div", { className: "flex justify-between items-center" }, /* @__PURE__ */ React8.createElement("h4", { className: "text-sm font-medium text-gray-700" }, config.label, " #", index + 1), canRemove && /* @__PURE__ */ React8.createElement(
489
+ const handleMoveUp = (index) => {
490
+ if (index > 0) {
491
+ move(index, index - 1);
492
+ }
493
+ };
494
+ const handleMoveDown = (index) => {
495
+ if (index < fields.length - 1) {
496
+ move(index, index + 1);
497
+ }
498
+ };
499
+ const renderFieldArrayItems = () => {
500
+ return fields.map((field2, index) => {
501
+ const canMoveUp = enableReordering && index > 0;
502
+ const canMoveDown = enableReordering && index < fields.length - 1;
503
+ const itemCanRemove = canRemove;
504
+ const fieldElements = fieldConfigs.map((fieldConfig) => {
505
+ const fieldName = fieldConfig.name;
506
+ const fullPath = `${name}.${index}.${fieldName}`;
507
+ let processedConfig = { ...fieldConfig, name: fullPath };
508
+ if ("dependsOn" in fieldConfig && fieldConfig.dependsOn && typeof fieldConfig.dependsOn === "string") {
509
+ const dependsOnPath = fieldConfig.dependsOn;
510
+ if (!dependsOnPath.startsWith(`${name}.`)) {
511
+ processedConfig = {
512
+ ...processedConfig,
513
+ dependsOn: `${name}.${index}.${dependsOnPath}`,
514
+ // Preserve dependsOnValue if it exists
515
+ ..."dependsOnValue" in fieldConfig && {
516
+ dependsOnValue: fieldConfig.dependsOnValue
517
+ }
518
+ };
519
+ }
520
+ }
521
+ return /* @__PURE__ */ React8.createElement(
522
+ FormField,
523
+ {
524
+ key: `${fieldConfig.name}-${index}`,
525
+ config: processedConfig,
526
+ form,
527
+ submissionState: {
528
+ error: void 0,
529
+ isSubmitted: false,
530
+ isSubmitting: false,
531
+ isSuccess: false
532
+ }
533
+ }
534
+ );
535
+ });
536
+ if (renderItem) {
537
+ return /* @__PURE__ */ React8.createElement(React8.Fragment, { key: field2.id }, renderItem({
538
+ canMoveDown,
539
+ canMoveUp,
540
+ canRemove: itemCanRemove,
541
+ children: /* @__PURE__ */ React8.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4" }, fieldElements),
542
+ field: field2,
543
+ fields,
544
+ index,
545
+ onMoveDown: () => handleMoveDown(index),
546
+ onMoveUp: () => handleMoveUp(index),
547
+ onRemove: () => handleRemove(index)
548
+ }));
549
+ }
550
+ return /* @__PURE__ */ React8.createElement(
551
+ "div",
552
+ {
553
+ key: field2.id,
554
+ className: "border border-gray-200 rounded-lg p-4 space-y-4"
555
+ },
556
+ /* @__PURE__ */ React8.createElement("div", { className: "flex justify-between items-center" }, /* @__PURE__ */ React8.createElement("h4", { className: "text-sm font-medium text-gray-700" }, config.label, " #", index + 1), /* @__PURE__ */ React8.createElement("div", { className: "flex gap-2" }, enableReordering && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(
557
+ Button2,
558
+ {
559
+ size: "sm",
560
+ variant: "light",
561
+ isDisabled: !canMoveUp,
562
+ onPress: () => handleMoveUp(index),
563
+ "aria-label": `Move ${config.label} ${index + 1} up`
564
+ },
565
+ reorderButtonText.up
566
+ ), /* @__PURE__ */ React8.createElement(
567
+ Button2,
568
+ {
569
+ size: "sm",
570
+ variant: "light",
571
+ isDisabled: !canMoveDown,
572
+ onPress: () => handleMoveDown(index),
573
+ "aria-label": `Move ${config.label} ${index + 1} down`
574
+ },
575
+ reorderButtonText.down
576
+ )), itemCanRemove && /* @__PURE__ */ React8.createElement(
577
+ Button2,
578
+ {
579
+ size: "sm",
580
+ variant: "light",
581
+ color: "danger",
582
+ startContent: "\u{1F5D1}\uFE0F",
583
+ onPress: () => handleRemove(index),
584
+ "aria-label": `${removeButtonText} ${config.label} ${index + 1}`
585
+ },
586
+ removeButtonText
587
+ ))),
588
+ /* @__PURE__ */ React8.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4" }, fieldElements)
589
+ );
590
+ });
591
+ };
592
+ const renderAddButtonElement = () => {
593
+ if (renderAddButton) {
594
+ return renderAddButton({
595
+ canAdd,
596
+ onAdd: handleAdd
597
+ });
598
+ }
599
+ if (!canAdd) {
600
+ return null;
601
+ }
602
+ return /* @__PURE__ */ React8.createElement(
487
603
  Button2,
488
604
  {
489
- size: "sm",
490
- variant: "light",
491
- color: "danger",
492
- startContent: "\u{1F5D1}\uFE0F",
493
- onPress: () => handleRemove(index),
494
- "aria-label": `${removeButtonText} ${config.label} ${index + 1}`
605
+ variant: "bordered",
606
+ startContent: "\u2795",
607
+ onPress: handleAdd,
608
+ className: "w-full"
495
609
  },
496
- removeButtonText
497
- )),
498
- /* @__PURE__ */ React8.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4" }, fieldConfigs.map((fieldConfig) => /* @__PURE__ */ React8.createElement(
499
- FormField,
500
- {
501
- key: `${fieldConfig.name}-${index}`,
502
- config: {
503
- ...fieldConfig,
504
- name: `${name}.${index}.${fieldConfig.name}`
505
- },
506
- form,
507
- submissionState: {
508
- error: void 0,
509
- isSubmitted: false,
510
- isSubmitting: false,
511
- isSuccess: false
512
- }
513
- }
514
- )))
515
- )), canAdd && /* @__PURE__ */ React8.createElement(
516
- Button2,
517
- {
518
- variant: "bordered",
519
- startContent: "\u2795",
520
- onPress: handleAdd,
521
- className: "w-full"
522
- },
523
- addButtonText
524
- ), fields.length === 0 && /* @__PURE__ */ React8.createElement("div", { className: "text-center py-8 text-gray-500" }, /* @__PURE__ */ React8.createElement("p", null, "No ", config.label?.toLowerCase(), " added yet."), /* @__PURE__ */ React8.createElement(
525
- Button2,
526
- {
527
- variant: "bordered",
528
- startContent: "\u2795",
529
- onPress: handleAdd,
530
- className: "mt-2"
531
- },
532
- addButtonText
533
- ))));
610
+ addButtonText
611
+ );
612
+ };
613
+ return /* @__PURE__ */ React8.createElement("div", { className }, /* @__PURE__ */ React8.createElement("div", { className: "space-y-4" }, fields.length > 0 ? renderFieldArrayItems() : /* @__PURE__ */ React8.createElement("div", { className: "text-center py-8 text-gray-500" }, /* @__PURE__ */ React8.createElement("p", null, "No ", config.label?.toLowerCase(), " added yet."), renderAddButtonElement()), fields.length > 0 && renderAddButtonElement()));
534
614
  }
535
615
 
536
616
  // src/fields/FileField.tsx
@@ -1056,7 +1136,7 @@ var FormField = React17.memo(
1056
1136
  return null;
1057
1137
  }
1058
1138
  if (config.dependsOn) {
1059
- const dependentValue = watchedValues[config.dependsOn];
1139
+ const dependentValue = get(watchedValues, config.dependsOn);
1060
1140
  if (config.dependsOnValue !== void 0 && dependentValue !== config.dependsOnValue) {
1061
1141
  return null;
1062
1142
  }
@@ -2340,6 +2420,44 @@ function ZodForm({
2340
2420
  ))));
2341
2421
  }
2342
2422
 
2423
+ // src/components/SimpleForm.tsx
2424
+ import React24 from "react";
2425
+ function SimpleForm({
2426
+ className,
2427
+ defaultValues,
2428
+ field: field2,
2429
+ hideSubmitButton = false,
2430
+ onError,
2431
+ onSubmit,
2432
+ onSuccess,
2433
+ schema,
2434
+ submitButton,
2435
+ subtitle,
2436
+ title
2437
+ }) {
2438
+ return /* @__PURE__ */ React24.createElement(
2439
+ ZodForm,
2440
+ {
2441
+ className,
2442
+ config: {
2443
+ defaultValues,
2444
+ fields: [field2],
2445
+ schema
2446
+ },
2447
+ onError,
2448
+ onSubmit,
2449
+ onSuccess,
2450
+ showResetButton: false,
2451
+ submitButtonText: hideSubmitButton ? "" : "Submit",
2452
+ subtitle,
2453
+ title,
2454
+ submitButtonProps: hideSubmitButton && submitButton ? {
2455
+ style: { display: "none" }
2456
+ } : {}
2457
+ }
2458
+ );
2459
+ }
2460
+
2343
2461
  // src/builders/BasicFormBuilder.ts
2344
2462
  var BasicFormBuilder = class {
2345
2463
  constructor() {
@@ -3748,6 +3866,131 @@ function useMemoizedFieldProps(props, deps) {
3748
3866
  return useMemo2(() => props, deps);
3749
3867
  }
3750
3868
 
3869
+ // src/utils/arraySync.ts
3870
+ function syncArrays(options) {
3871
+ const { current, existing, getId } = options;
3872
+ const existingMap = /* @__PURE__ */ new Map();
3873
+ const currentMap = /* @__PURE__ */ new Map();
3874
+ existing.forEach((item) => {
3875
+ const id = getId(item);
3876
+ if (id !== void 0) {
3877
+ existingMap.set(id, item);
3878
+ }
3879
+ });
3880
+ current.forEach((item) => {
3881
+ const id = getId(item);
3882
+ if (id !== void 0) {
3883
+ currentMap.set(id, item);
3884
+ }
3885
+ });
3886
+ const toDelete = [];
3887
+ existingMap.forEach((item, id) => {
3888
+ if (!currentMap.has(id)) {
3889
+ toDelete.push(item);
3890
+ }
3891
+ });
3892
+ const toUpdate = [];
3893
+ existingMap.forEach((existingItem, id) => {
3894
+ const currentItem = currentMap.get(id);
3895
+ if (currentItem) {
3896
+ toUpdate.push({ current: currentItem, existing: existingItem });
3897
+ }
3898
+ });
3899
+ const toCreate = [];
3900
+ currentMap.forEach((item, id) => {
3901
+ if (!existingMap.has(id)) {
3902
+ toCreate.push(item);
3903
+ }
3904
+ });
3905
+ return {
3906
+ toCreate,
3907
+ toDelete,
3908
+ toUpdate
3909
+ };
3910
+ }
3911
+
3912
+ // src/utils/createFieldArrayCustomConfig.tsx
3913
+ import React25 from "react";
3914
+ import { useFieldArray as useFieldArray2 } from "react-hook-form";
3915
+ import { Button as Button6 } from "@heroui/react";
3916
+ function createFieldArrayCustomConfig(options) {
3917
+ const {
3918
+ className,
3919
+ defaultItem,
3920
+ enableReordering = false,
3921
+ label,
3922
+ max = 10,
3923
+ min = 0,
3924
+ name,
3925
+ renderAddButton,
3926
+ renderItem
3927
+ } = options;
3928
+ return {
3929
+ className,
3930
+ label,
3931
+ name,
3932
+ // ArrayPath is compatible with Path for CustomFieldConfig
3933
+ render: ({ control, errors, form }) => {
3934
+ const { append, fields, move, remove } = useFieldArray2({
3935
+ control,
3936
+ name
3937
+ });
3938
+ const canAdd = fields.length < max;
3939
+ const canRemove = fields.length > min;
3940
+ const handleAdd = () => {
3941
+ if (canAdd) {
3942
+ if (defaultItem) {
3943
+ append(defaultItem());
3944
+ } else {
3945
+ append({});
3946
+ }
3947
+ }
3948
+ };
3949
+ const handleRemove = (index) => {
3950
+ if (canRemove) {
3951
+ remove(index);
3952
+ }
3953
+ };
3954
+ const handleMoveUp = (index) => {
3955
+ if (enableReordering && index > 0) {
3956
+ move(index, index - 1);
3957
+ }
3958
+ };
3959
+ const handleMoveDown = (index) => {
3960
+ if (enableReordering && index < fields.length - 1) {
3961
+ move(index, index + 1);
3962
+ }
3963
+ };
3964
+ return /* @__PURE__ */ React25.createElement("div", { className }, /* @__PURE__ */ React25.createElement("div", { className: "space-y-4" }, fields.map((field2, index) => {
3965
+ const canMoveUp = enableReordering && index > 0;
3966
+ const canMoveDown = enableReordering && index < fields.length - 1;
3967
+ return /* @__PURE__ */ React25.createElement(React25.Fragment, { key: field2.id }, renderItem({
3968
+ canMoveDown,
3969
+ canMoveUp,
3970
+ control,
3971
+ errors,
3972
+ field: field2,
3973
+ fields,
3974
+ form,
3975
+ index,
3976
+ onMoveDown: () => handleMoveDown(index),
3977
+ onMoveUp: () => handleMoveUp(index),
3978
+ onRemove: () => handleRemove(index)
3979
+ }));
3980
+ }), fields.length === 0 && renderAddButton ? /* @__PURE__ */ React25.createElement("div", { className: "text-center py-8 text-gray-500" }, /* @__PURE__ */ React25.createElement("p", null, "No ", label?.toLowerCase() || "items", " added yet."), renderAddButton({ canAdd, onAdd: handleAdd })) : null, fields.length > 0 && renderAddButton ? renderAddButton({ canAdd, onAdd: handleAdd }) : canAdd && /* @__PURE__ */ React25.createElement(
3981
+ Button6,
3982
+ {
3983
+ variant: "bordered",
3984
+ onPress: handleAdd,
3985
+ className: "w-full"
3986
+ },
3987
+ "Add Item"
3988
+ )));
3989
+ },
3990
+ type: "custom"
3991
+ };
3992
+ }
3993
+
3751
3994
  // src/builders/validation-helpers.ts
3752
3995
  import { z as z4 } from "zod";
3753
3996
  var validationPatterns = {
@@ -3921,6 +4164,7 @@ export {
3921
4164
  RadioGroupField,
3922
4165
  SelectField,
3923
4166
  ServerActionForm,
4167
+ SimpleForm,
3924
4168
  SliderField,
3925
4169
  SubmitButton,
3926
4170
  SwitchField,
@@ -3936,6 +4180,7 @@ export {
3936
4180
  createEmailSchema,
3937
4181
  createField,
3938
4182
  createFieldArrayBuilder,
4183
+ createFieldArrayCustomConfig,
3939
4184
  createFieldArrayItemBuilder,
3940
4185
  createFileSchema,
3941
4186
  createFormTestUtils,
@@ -3969,6 +4214,7 @@ export {
3969
4214
  shallowEqual,
3970
4215
  simulateFieldInput,
3971
4216
  simulateFormSubmission,
4217
+ syncArrays,
3972
4218
  throttle,
3973
4219
  useDebouncedFieldValidation,
3974
4220
  useDebouncedValidation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rachelallyson/hero-hook-form",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Typed form helpers that combine React Hook Form and HeroUI components.",
5
5
  "author": "Rachel Higley",
6
6
  "homepage": "https://rachelallyson.github.io/hero-hook-form/",