All files / src/features/GiveOnboarding/hooks useFormStep.ts

87.5% Statements 21/24
83.33% Branches 5/6
100% Functions 8/8
87.5% Lines 21/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114                                                                                        105x 105x 105x 154x     105x             105x         9x   8x       8x   8x         8x 8x         8x               1x       105x   102x               105x 30x 30x 4x         105x    
import {
  useForm,
  UseFormReturn,
  SubmitHandler,
  FieldValues,
  Resolver,
  DefaultValues,
  DeepPartial,
  DeepMap,
} from "react-hook-form";
import { useImperativeHandle, ForwardedRef, useEffect } from "react";
import { IStep } from "@features/GiveOnboarding/types";
import {
  IOnSubmitOptions,
  StepHandle,
} from "@features/GiveOnboarding/types/handlers";
import { setStepClean, setStepDirty } from "@redux/slices/onboardingWizard";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@redux/types/store";
 
interface UseFormStepConfig<TFormValues extends FieldValues> {
  data_key: IStep;
  resolver: Resolver<TFormValues, any>;
  defaultValues?: DefaultValues<TFormValues>;
  context?: any;
  onSubmitStepData: (
    formData: {
      data: TFormValues;
      dirtyFields: Partial<
        Readonly<DeepMap<DeepPartial<TFormValues>, boolean>>
      >;
    },
    onParentValid?: SubmitHandler<TFormValues>, // Callback to signal success to parent
    onParentInvalid?: (error: any) => void, // Callback to signal failure (form or API) to parent
  ) => Promise<void> | void;
}
 
export function useFormStep<TFormValues extends FieldValues>(
  ref: ForwardedRef<StepHandle<TFormValues>>,
  config: UseFormStepConfig<TFormValues>,
): {
  methods: UseFormReturn<TFormValues>;
  submit: (options: IOnSubmitOptions<TFormValues>) => Promise<void>;
} {
  const dispatch = useDispatch();
  const { data_key, resolver, defaultValues, onSubmitStepData } = config;
  const isDirty = useSelector(
    (state: RootState) => state.onboardingWizard.dirtySteps[data_key],
  );
 
  const methods = useForm<TFormValues>({
    mode: "onSubmit",
    resolver,
    defaultValues,
    context: config.context,
  });
 
  const submit = ({
    onInvalid,
    onSubmitValidated,
    onValid,
  }: IOnSubmitOptions<TFormValues>) =>
    methods.handleSubmit(
      async (formData) => {
        onSubmitValidated?.();
        // onSubmitStepData will call parentOnValid or parentOnInvalid
        // after its own logic (e.g., API call)
 
        const dirtyFields = Object.keys(methods.formState.dirtyFields);
 
        Iif (dirtyFields.length === 0 && !isDirty) {
          onValid?.(formData); //is in edit form and the user did not changed any field
          return;
        }
 
        try {
          await onSubmitStepData(
            { data: formData, dirtyFields: methods.formState.dirtyFields },
            onValid,
            onInvalid,
          );
          dispatch(setStepClean(data_key));
        } catch (e) {
          // Catch any unhandled errors from onSubmitStepData
          onInvalid?.({ type: "api", error: e });
        }
      },
      (formErrors) => {
        // Handle react-hook-form validation errors
        onInvalid?.({ type: "formValidation", error: formErrors });
      },
    )();
 
  useImperativeHandle(
    ref,
    () => ({
      data_key,
      methods,
      submit: submit, // Immediately invoke handleSubmit
    }),
    [methods, data_key, onSubmitStepData],
  );
 
  useEffect(() => {
    return () => {
      if (methods.formState.isDirty) {
        dispatch(setStepDirty(data_key));
      }
    };
  }, []);
 
  return { methods, submit };
}