pankod/refine

View on GitHub
packages/mantine/src/hooks/form/useForm/index.ts

Summary

Maintainability
C
7 hrs
Test Coverage
import React, { useEffect } from "react";
import {
  useForm as useMantineForm,
  UseFormReturnType as UseMantineFormReturnType,
} from "@mantine/form";
import get from "lodash/get";
import has from "lodash/has";
import set from "lodash/set";
import { UseFormInput } from "@mantine/form/lib/types";
import {
  BaseRecord,
  HttpError,
  useForm as useFormCore,
  useWarnAboutChange,
  UseFormProps as UseFormCoreProps,
  UseFormReturnType as UseFormReturnTypeCore,
  useTranslate,
  useRefineContext,
  flattenObjectKeys,
} from "@refinedev/core";

type FormVariableType<TVariables, TTransformed> = ReturnType<
  NonNullable<
    UseFormInput<
      TVariables,
      (values: TVariables) => TTransformed
    >["transformValues"]
  >
>;

export type UseFormReturnType<
  TQueryFnData extends BaseRecord = BaseRecord,
  TError extends HttpError = HttpError,
  TVariables = Record<string, unknown>,
  TTransformed = TVariables,
  TData extends BaseRecord = TQueryFnData,
  TResponse extends BaseRecord = TData,
  TResponseError extends HttpError = TError,
> = UseMantineFormReturnType<
  TVariables,
  (values: TVariables) => TTransformed
> & {
  refineCore: UseFormReturnTypeCore<
    TQueryFnData,
    TError,
    FormVariableType<TVariables, TTransformed>,
    TData,
    TResponse,
    TResponseError
  >;
  saveButtonProps: {
    disabled: boolean;
    onClick: (e: React.PointerEvent<HTMLButtonElement>) => void;
  };
};

export type UseFormProps<
  TQueryFnData extends BaseRecord = BaseRecord,
  TError extends HttpError = HttpError,
  TVariables = Record<string, unknown>,
  TTransformed = TVariables,
  TData extends BaseRecord = TQueryFnData,
  TResponse extends BaseRecord = TData,
  TResponseError extends HttpError = TError,
> = {
  refineCoreProps?: UseFormCoreProps<
    TQueryFnData,
    TError,
    FormVariableType<TVariables, TTransformed>,
    TData,
    TResponse,
    TResponseError
  > & {
    warnWhenUnsavedChanges?: boolean;
  };
} & UseFormInput<TVariables, (values: TVariables) => TTransformed> & {
    /**
     * Disables server-side validation
     * @default false
     * @see {@link https://refine.dev/docs/advanced-tutorials/forms/server-side-form-validation/}
     */
    disableServerSideValidation?: boolean;
  };

export const useForm = <
  TQueryFnData extends BaseRecord = BaseRecord,
  TError extends HttpError = HttpError,
  TVariables = Record<string, unknown>,
  TTransformed = TVariables,
  TData extends BaseRecord = TQueryFnData,
  TResponse extends BaseRecord = TData,
  TResponseError extends HttpError = TError,
>({
  refineCoreProps,
  disableServerSideValidation: disableServerSideValidationProp = false,
  ...rest
}: UseFormProps<
  TQueryFnData,
  TError,
  TVariables,
  TTransformed,
  TData,
  TResponse,
  TResponseError
> = {}): UseFormReturnType<
  TQueryFnData,
  TError,
  TVariables,
  TTransformed,
  TData,
  TResponse,
  TResponseError
> => {
  const { options } = useRefineContext();
  const disableServerSideValidation =
    options?.disableServerSideValidation || disableServerSideValidationProp;

  const translate = useTranslate();

  const warnWhenUnsavedChangesProp = refineCoreProps?.warnWhenUnsavedChanges;

  const { warnWhenUnsavedChanges: warnWhenUnsavedChangesRefine, setWarnWhen } =
    useWarnAboutChange();
  const warnWhenUnsavedChanges =
    warnWhenUnsavedChangesProp ?? warnWhenUnsavedChangesRefine;

  const useMantineFormResult = useMantineForm<
    TVariables,
    (values: TVariables) => TTransformed
  >({
    ...rest,
  });

  const {
    setValues,
    onSubmit: onMantineSubmit,
    isDirty,
    resetDirty,
    setFieldError,
    values,
  } = useMantineFormResult;

  const useFormCoreResult = useFormCore<
    TQueryFnData,
    TError,
    FormVariableType<TVariables, TTransformed>,
    TData,
    TResponse,
    TResponseError
  >({
    ...refineCoreProps,
    onMutationError: (error, _variables, _context) => {
      if (disableServerSideValidation) {
        refineCoreProps?.onMutationError?.(error, _variables, _context);
        return;
      }

      const errors = error?.errors;

      for (const key in errors) {
        const fieldError = errors[key];

        let newError = "";

        if (Array.isArray(fieldError)) {
          newError = fieldError.join(" ");
        }

        if (typeof fieldError === "string") {
          newError = fieldError;
        }

        if (typeof fieldError === "boolean") {
          newError = "Field is not valid.";
        }

        if (typeof fieldError === "object" && "key" in fieldError) {
          const translatedMessage = translate(
            fieldError.key,
            fieldError.message,
          );

          newError = translatedMessage;
        }
        setFieldError(key, newError);
      }

      refineCoreProps?.onMutationError?.(error, _variables, _context);
    },
  });

  const { queryResult, formLoading, onFinish, onFinishAutoSave } =
    useFormCoreResult;

  useEffect(() => {
    if (typeof queryResult?.data !== "undefined") {
      const fields: any = {};

      const registeredFields = flattenObjectKeys(rest.initialValues ?? {});

      const data = queryResult?.data?.data ?? {};

      Object.keys(registeredFields).forEach((key) => {
        const hasValue = has(data, key);
        const dataValue = get(data, key);

        if (hasValue) {
          set(fields, key, dataValue);
        }
      });

      setValues(fields);
      resetDirty(fields);
    }
  }, [queryResult?.data]);

  const isValuesChanged = isDirty();

  useEffect(() => {
    if (warnWhenUnsavedChanges) {
      setWarnWhen(isValuesChanged);
    }
  }, [isValuesChanged]);

  useEffect(() => {
    if (isValuesChanged && refineCoreProps?.autoSave && values) {
      setWarnWhen(false);

      const transformedValues = rest.transformValues
        ? rest.transformValues(values)
        : (values as unknown as TTransformed);

      onFinishAutoSave(transformedValues).catch((error) => error);
    }
  }, [values]);

  const onSubmit: (typeof useMantineFormResult)["onSubmit"] =
    (handleSubmit, handleValidationFailure) => async (e) => {
      setWarnWhen(false);
      return await onMantineSubmit(handleSubmit, handleValidationFailure)(e);
    };

  const saveButtonProps = {
    disabled: formLoading,
    onClick: (e: React.PointerEvent<HTMLButtonElement>) => {
      onSubmit(
        (v) => onFinish(v).catch((error) => error),
        () => false,
        // @ts-expect-error event type is not compatible with pointer event
      )(e);
    },
  };

  return {
    ...useMantineFormResult,
    onSubmit,
    refineCore: useFormCoreResult,
    saveButtonProps,
  };
};