All files / app/src/shared/lib/form form.tsx

86.95% Statements 60/69
100% Branches 19/19
75% Functions 3/4
86.95% Lines 60/69

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 1051x                                               1x 113x 113x 113x 113x 113x 113x 113x 113x 113x 113x 113x 113x   113x 113x 113x 113x 113x   113x 113x   113x                     113x 193x 177x   177x 146x 146x   31x 31x 31x 171x 177x 177x 177x 16x 193x   113x 217x 77x 77x   217x 25x 25x   115x 217x   113x 113x 113x 113x 113x 113x 113x 113x 113x 113x   113x 113x 113x   113x  
import { Children, cloneElement, FormHTMLAttributes, isValidElement, ReactNode, useState } from 'react'
import { FieldErrors, FieldValues, FormProvider, useForm, UseFormProps, UseFormReturn } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import { ObjectSchema, AnySchema } from 'yup'
import { BaseEntity } from '@/shared/@types/global'
import { useFormErrors } from '@/shared/hooks'
import { object } from 'yup'
import merge from 'lodash/merge'
 
type FormChildren<T extends FieldValues> =
  | ReactNode
  | ReactNode[]
  | ((methods: UseFormReturn<T> & { isLoading: boolean }) => ReactNode)
 
export interface FormProps<T extends FieldValues = FieldValues>
  extends Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'onError' | 'children'> {
  children?: FormChildren<T>
  validationSchema?: ObjectSchema<Record<Exclude<keyof T, keyof BaseEntity>, AnySchema>, object>
  formParams?: UseFormProps<T>
  className?: string
  onSubmit?: (data: T, methods?: UseFormReturn<T>) => Promise<unknown> | void
  onError?: (errors?: FieldErrors<T>, methods?: UseFormReturn<T>) => Promise<unknown> | void
}
 
export const Form = <TFormValues extends FieldValues = FieldValues>({
  children,
  validationSchema,
  formParams = {},
  className = '',
  onSubmit,
  onError,
  ...rest
}: FormProps<TFormValues>) => {
  const methods = useForm({
    ...formParams,
    resolver: yupResolver(validationSchema || object({}).notRequired()),
  })
 
  const {
    formState: { errors },
    handleSubmit,
    register,
  } = methods
 
  const { getErrorByName } = useFormErrors(errors)
  const [isLoading, setIsLoading] = useState(false)
 
  const onFormSubmit = async (data: TFormValues) => {
    try {
      setIsLoading(true)
      await onSubmit?.(data, methods)
    } catch (error) {
      return Promise.reject(error)
    } finally {
      setIsLoading(false)
    }
  }
 
  const normalizeChildren = (child: ReactNode) => {
    if (isValidElement(child)) {
      const name = child.props.name
 
      if (!name) {
        return cloneElement(child, child.props, normalize(child.props.children))
      }
 
      return cloneElement(child, {
        ...child.props,
        ...getErrorByName(name),
        ...(!child.props.control && { ...register(name) }),
        key: name,
      })
    }
    return child
  }
 
  const normalize = (childs?: ReactNode | ReactNode[]): ReactNode | ReactNode[] => {
    if (!childs) {
      return null
    }
 
    if (Array.isArray(childs)) {
      return Children.map(childs, normalizeChildren)
    }
 
    return normalizeChildren(childs)
  }
 
  return (
    <FormProvider {...methods}>
      <form
        onSubmit={handleSubmit(
          data => onFormSubmit(data),
          data => onError?.(data, methods)
        )}
        className={className}
        {...rest}
        noValidate
      >
        {normalize(typeof children === 'function' ? children(merge(methods, { isLoading })) : children)}
      </form>
    </FormProvider>
  )
}