const isCheckboxInput = isBoolean(value);

const shouldReValidateOnBlur = isOnBlur || isReValidateOnBlur;

import * as React from 'react';
import attachEventListeners from './logic/attachEventListeners';
import transformToNestObject from './logic/transformToNestObject';
import focusErrorField from './logic/focusErrorField';
import findRemovedFieldAndRemoveListener from './logic/findRemovedFieldAndRemoveListener';
import getFieldsValues from './logic/getFieldsValues';
import getFieldValue from './logic/getFieldValue';
import shouldUpdateWithError from './logic/shouldUpdateWithError';
import validateField from './logic/validateField';
import validateWithSchema from './logic/validateWithSchema';
import getDefaultValue from './logic/getDefaultValue';
import assignWatchFields from './logic/assignWatchFields';
import skipValidation from './logic/skipValidation';
import getFieldValueByName from './logic/getFieldValueByName';
import getIsFieldsDifferent from './logic/getIsFieldsDifferent';
import isNameInFieldArray from './logic/isNameInFieldArray';
import isCheckBoxInput from './utils/isCheckBoxInput';
import isEmptyObject from './utils/isEmptyObject';
import isRadioInput from './utils/isRadioInput';
import isFileInput from './utils/isFileInput';
import isObject from './utils/isObject';
import isBoolean from './utils/isBoolean';
import isPrimitive from './utils/isPrimitive';
import isFunction from './utils/isFunction';
import isArray from './utils/isArray';
import isString from './utils/isString';
import isSameError from './utils/isSameError';
import isUndefined from './utils/isUndefined';
import isFileListObject from './utils/isFileListObject';
import isEmptyString from './utils/isEmptyString';
import onDomRemove from './utils/onDomRemove';
import get from './utils/get';
import set from './utils/set';
import unset from './utils/unset';
import isMultipleSelect from './utils/isMultipleSelect';
import modeChecker from './utils/validationModeChecker';
import isNullOrUndefined from './utils/isNullOrUndefined';
import isRadioOrCheckboxFunction from './utils/isRadioOrCheckbox';
import isHTMLElement from './utils/isHTMLElement';
import { EVENTS, UNDEFINED, VALIDATION_MODE } from './constants';
import { FormContextValues } from './contextTypes';
import {
  DeepPartial,
  FieldValues,
  FieldName,
  FieldValue,
  FieldErrors,
  Field,
  FieldRefs,
  UseFormOptions,
  ValidationOptions,
  OnSubmit,
  FieldElement,
  FormStateProxy,
  ReadFormState,
  ManualFieldError,
  MultipleFieldErrors,
  Ref,
  HandleChange,
  Touched,
  FieldError,
  RadioOrCheckboxOption,
  OmitResetState,
  Message,
} from './types';

const { useRef, useState, useCallback, useEffect } = React;

export function useForm<
  FormValues extends FieldValues = FieldValues,
  ValidationContext extends object = object
>({
  mode = VALIDATION_MODE.onSubmit,
  reValidateMode = VALIDATION_MODE.onChange,
  validationSchema,
  validationResolver,
  validationContext,
  defaultValues = {},
  submitFocusError = true,
  validateCriteriaMode,
}: UseFormOptions<FormValues, ValidationContext> = {}): FormContextValues<
  FormValues
> {
  const fieldsRef = useRef<FieldRefs<FormValues>>({});
  const validateAllFieldCriteria = validateCriteriaMode === 'all';
  const errorsRef = useRef<FieldErrors<FormValues>>({});
  const touchedFieldsRef = useRef<Touched<FormValues>>({});
  const fieldArrayDefaultValues = useRef<Record<string, unknown[]>>({});
  const watchFieldsRef = useRef(new Set<FieldName<FormValues>>());
  const dirtyFieldsRef = useRef(new Set<FieldName<FormValues>>());
  const fieldsWithValidationRef = useRef(new Set<FieldName<FormValues>>());
  const validFieldsRef = useRef(new Set<FieldName<FormValues>>());
  const isValidRef = useRef(true);
  const defaultRenderValuesRef = useRef<
    DeepPartial<Record<FieldName<FormValues>, FieldValue<FormValues>>>
  >({});
  const defaultValuesRef = useRef<
    FieldValue<FormValues> | DeepPartial<FormValues>
  >(defaultValues);
  const isUnMount = useRef(false);
  const isWatchAllRef = useRef(false);
  const isSubmittedRef = useRef(false);
  const isDirtyRef = useRef(false);
  const submitCountRef = useRef(0);
  const isSubmittingRef = useRef(false);
  const handleChangeRef = useRef<HandleChange>();
  const resetFieldArrayFunctionRef = useRef({});
  const validationContextRef = useRef(validationContext);
  const fieldArrayNamesRef = useRef<Set<string>>(new Set());
  const [, render] = useState();
  const { isOnBlur, isOnSubmit, isOnChange } = useRef(
    modeChecker(mode),
  ).current;
  const isWindowUndefined = typeof window === UNDEFINED;
  const shouldValidateCallback = !!(validationSchema || validationResolver);
  const isWeb =
    typeof document !== UNDEFINED &&
    !isWindowUndefined &&
    !isUndefined(window.HTMLElement);
  const isProxyEnabled = isWeb && 'Proxy' in window;
  const readFormStateRef = useRef<ReadFormState>({
    dirty: !isProxyEnabled,
    dirtyFields: !isProxyEnabled,
    isSubmitted: isOnSubmit,
    submitCount: !isProxyEnabled,
    touched: !isProxyEnabled,
    isSubmitting: !isProxyEnabled,
    isValid: !isProxyEnabled,
  });
  const {
    isOnBlur: isReValidateOnBlur,
    isOnSubmit: isReValidateOnSubmit,
  } = useRef(modeChecker(reValidateMode)).current;

  const reRender = useCallback(() => {
    if (!isUnMount.current) {
      render({});
    }
  }, []);

  const shouldRenderBaseOnError = useCallback(
    (
      name: FieldName<FormValues>,
      error: FieldErrors<FormValues>,
      shouldRender?,
      skipReRender?,
    ): boolean | void => {
      let shouldReRender =
        shouldRender ||
        shouldUpdateWithError<FormValues>({
          errors: errorsRef.current,
          error,
          name,
          validFields: validFieldsRef.current,
          fieldsWithValidation: fieldsWithValidationRef.current,
        });

      if (isEmptyObject(error)) {
        if (
          fieldsWithValidationRef.current.has(name) ||
          shouldValidateCallback
        ) {
          validFieldsRef.current.add(name);
          shouldReRender = shouldReRender || get(errorsRef.current, name);
        }

        errorsRef.current = unset(errorsRef.current, [name]);
      } else {
        validFieldsRef.current.delete(name);
        shouldReRender = shouldReRender || !get(errorsRef.current, name);

        set(errorsRef.current, name, error[name]);
      }

      if (shouldReRender && !skipReRender) {
        reRender();
        return true;
      }
    },
    [reRender, shouldValidateCallback],
  );

  const setFieldValue = useCallback(
    (
      field: Field,
      rawValue:
        | FieldValue<FormValues>
        | DeepPartial<FormValues>
        | undefined
        | null
        | boolean,
    ): boolean => {
      const ref = field.ref;
      const options = field.options;
      const { type } = ref;
      const value =
        isWeb && isHTMLElement(ref) && isNullOrUndefined(rawValue)
          ? ''
          : rawValue;

      if (isRadioInput(ref) && options) {
        options.forEach(
          ({ ref: radioRef }: { ref: HTMLInputElement }) =>
            (radioRef.checked = radioRef.value === value),
        );
      } else if (isFileInput(ref)) {
        if (
          isEmptyString(value as string) ||
          isFileListObject(value as object)
        ) {
          ref.files = value as FileList;
        } else {
          ref.value = value as string;
        }
      } else if (isMultipleSelect(ref)) {
        [...ref.options].forEach(
          selectRef =>
            (selectRef.selected = (value as string).includes(selectRef.value)),
        );
      } else if (isCheckBoxInput(ref) && options) {
        options.length > 1
          ? options.forEach(
              ({ ref: checkboxRef }) =>
                (checkboxRef.checked = (value as string).includes(
                  checkboxRef.value,
                )),
            )
          : (options[0].ref.checked = !!value);
      } else {
        ref.value = value;
      }

      return !!type;
    },
    [isWeb],
  );

  const setDirty = (name: FieldName<FormValues>): boolean => {
    if (
      !fieldsRef.current[name] ||
      (!readFormStateRef.current.dirty && !readFormStateRef.current.dirtyFields)
    ) {
      return false;
    }

    const isFieldArray = isNameInFieldArray(fieldArrayNamesRef.current, name);
    const previousDirtyFieldsLength = dirtyFieldsRef.current.size;
    let isDirty =
      defaultRenderValuesRef.current[name] !==
      getFieldValue(fieldsRef.current, fieldsRef.current[name]!.ref);

    if (isFieldArray) {
      const fieldArrayName = name.substring(0, name.indexOf('['));
      isDirty = getIsFieldsDifferent(
        getFieldValueByName(fieldsRef.current, fieldArrayName),
        get(defaultValuesRef.current, fieldArrayName),
      );
    }

    const isDirtyChanged =
      (isFieldArray ? isDirtyRef.current : dirtyFieldsRef.current.has(name)) !==
      isDirty;

    if (isDirty) {
      dirtyFieldsRef.current.add(name);
    } else {
      dirtyFieldsRef.current.delete(name);
    }

    isDirtyRef.current = isFieldArray ? isDirty : !!dirtyFieldsRef.current.size;
    return readFormStateRef.current.dirty
      ? isDirtyChanged
      : previousDirtyFieldsLength !== dirtyFieldsRef.current.size;
  };

  const setDirtyAndTouchedFields = useCallback(
    (fieldName: FieldName<FormValues>): void | boolean => {
      if (
        setDirty(fieldName) ||
        (!get(touchedFieldsRef.current, fieldName) &&
          readFormStateRef.current.touched)
      ) {
        return !!set(touchedFieldsRef.current, fieldName, true);
      }
    },
    [],
  );

  const setInternalValueBatch = useCallback(
    (
      name: FieldName<FormValues>,
      value: FieldValue<FormValues>,
      parentFieldName?: string,
    ) => {
      const isValueArray = isArray(value);

      for (const key in value) {
        const fieldName = `${parentFieldName || name}${
          isValueArray ? `[${key}]` : `.${key}`
        }`;

        if (isObject(value[key])) {
          setInternalValueBatch(name, value[key], fieldName);
        }

        const field = fieldsRef.current[fieldName];

        if (field) {
          setFieldValue(field, value[key]);
          setDirtyAndTouchedFields(fieldName);
        }
      }
    },
    [setFieldValue, setDirtyAndTouchedFields],
  );

  const setInternalValue = useCallback(
    (
      name: FieldName<FormValues>,
      value: FieldValue<FormValues> | null | undefined | boolean,
    ): boolean | void => {
      const field = fieldsRef.current[name];
      if (field) {
        setFieldValue(field as Field, value);

        const output = setDirtyAndTouchedFields(name);
        if (isBoolean(output)) {
          return output;
        }
      } else if (!isPrimitive(value)) {
        setInternalValueBatch(name, value);
      }
    },
    [setDirtyAndTouchedFields, setFieldValue, setInternalValueBatch],
  );

  const executeValidation = useCallback(
    async (
      name: FieldName<FormValues>,
      skipReRender?: boolean,
    ): Promise<boolean> => {
      const field = fieldsRef.current[name]!;

      if (!field) {
        return false;
      }

      const error = await validateField<FormValues>(
        fieldsRef,
        validateAllFieldCriteria,
        field,
      );

      shouldRenderBaseOnError(name, error, false, skipReRender);

      return isEmptyObject(error);
    },
    [shouldRenderBaseOnError, validateAllFieldCriteria],
  );

  const executeSchemaValidation = useCallback(
    async (
      payload: FieldName<FormValues> | FieldName<FormValues>[],
    ): Promise<boolean> => {
      const { errors } = await validateWithSchema<
        FormValues,
        ValidationContext
      >(
        validationSchema,
        validateAllFieldCriteria,
        getFieldValueByName(fieldsRef.current),
        validationResolver,
        validationContextRef.current,
      );
      const previousFormIsValid = isValidRef.current;
      isValidRef.current = isEmptyObject(errors);

      if (isArray(payload)) {
        payload.forEach(name => {
          const error = get(errors, name);

          if (error) {
            set(errorsRef.current, name, error);
          } else {
            unset(errorsRef.current, [name]);
          }
        });
        reRender();
      } else {
        shouldRenderBaseOnError(
          payload,
          (get(errors, payload)
            ? { [payload]: get(errors, payload) }
            : {}) as FieldErrors<FormValues>,
          previousFormIsValid !== isValidRef.current,
        );
      }

      return isEmptyObject(errorsRef.current);
    },
    [
      reRender,
      shouldRenderBaseOnError,
      validateAllFieldCriteria,
      validationResolver,
      validationSchema,
    ],
  );

  const triggerValidation = useCallback(
    async (
      payload?: FieldName<FormValues> | FieldName<FormValues>[] | string,
    ): Promise<boolean> => {
      const fields = payload || Object.keys(fieldsRef.current);

      if (shouldValidateCallback) {
        return executeSchemaValidation(fields);
      }

      if (isArray(fields)) {
        const result = await Promise.all(
          fields.map(async data => await executeValidation(data, true)),
        );
        reRender();
        return result.every(Boolean);
      }

      return await executeValidation(fields);
    },
    [
      executeSchemaValidation,
      executeValidation,
      reRender,
      shouldValidateCallback,
    ],
  );

  const isFieldWatched = (name: string) =>
    isWatchAllRef.current ||
    watchFieldsRef.current.has(name) ||
    watchFieldsRef.current.has((name.match(/\w+/) || [])[0]);

  function setValue<Name extends FieldName<FormValues>>(
    name: Name,
    value?: FormValues[Name],
    shouldValidate?: boolean,
  ): void;
  function setValue<Name extends FieldName<FormValues>>(
    namesWithValue: Record<Name, any>[],
    shouldValidate?: boolean,
  ): void;
  function setValue<Name extends FieldName<FormValues>>(
    names: Name | Record<Name, any>[],
    valueOrShouldValidate?: FormValues[Name] | boolean,
    shouldValidate?: boolean,
  ): void {
    let shouldRender = false;
    const isMultiple = isArray(names);

    (isMultiple
      ? (names as Record<Name, FormValues[Name]>[])
      : [names]
    ).forEach((name: any) => {
      const isStringFieldName = isString(name);
      shouldRender =
        setInternalValue(
          isStringFieldName ? name : Object.keys(name)[0],
          isStringFieldName
            ? valueOrShouldValidate
            : (Object.values(name)[0] as FormValues[Name]),
        ) || isMultiple
          ? true
          : isFieldWatched(name);
    });

    if (shouldRender || isMultiple) {
      reRender();
    }

    if (shouldValidate || (isMultiple && valueOrShouldValidate)) {
      triggerValidation(isMultiple ? undefined : (names as Name));
    }
  }

  handleChangeRef.current = handleChangeRef.current
    ? handleChangeRef.current
    : async ({ type, target }: Event): Promise<void | boolean> => {
        const name = target ? (target as Ref).name : '';
        const fields = fieldsRef.current;
        const errors = errorsRef.current;
        const field = fields[name];
        const currentError = get(errors, name);
        let error;

        if (!field) {
          return;
        }

        const isBlurEvent = type === EVENTS.BLUR;
        const shouldSkipValidation = skipValidation({
          hasError: !!currentError,
          isOnChange,
          isBlurEvent,
          isOnSubmit,
          isReValidateOnSubmit,
          isOnBlur,
          isReValidateOnBlur,
          isSubmitted: isSubmittedRef.current,
        });
        const shouldUpdateDirty = setDirty(name);
        let shouldUpdateState = isFieldWatched(name) || shouldUpdateDirty;

        if (
          isBlurEvent &&
          !get(touchedFieldsRef.current, name) &&
          readFormStateRef.current.touched
        ) {
          set(touchedFieldsRef.current, name, true);
          shouldUpdateState = true;
        }

        if (shouldSkipValidation) {
          return shouldUpdateState && reRender();
        }

        if (shouldValidateCallback) {
          const { errors } = await validateWithSchema<
            FormValues,
            ValidationContext
          >(
            validationSchema,
            validateAllFieldCriteria,
            getFieldValueByName(fields),
            validationResolver,
            validationContextRef.current,
          );
          const previousFormIsValid = isValidRef.current;
          isValidRef.current = isEmptyObject(errors);

          error = (get(errors, name)
            ? { [name]: get(errors, name) }
            : {}) as FieldErrors<FormValues>;

          if (previousFormIsValid !== isValidRef.current) {
            shouldUpdateState = true;
          }
        } else {
          error = await validateField<FormValues>(
            fieldsRef,
            validateAllFieldCriteria,
            field,
          );
        }

        if (!shouldRenderBaseOnError(name, error) && shouldUpdateState) {
          reRender();
        }
      };

  const validateSchemaIsValid = useCallback(
    (values: any = {}) => {
      const fieldValues = isEmptyObject(defaultValuesRef.current)
        ? getFieldsValues(fieldsRef.current)
        : defaultValuesRef.current;

      validateWithSchema<FormValues, ValidationContext>(
        validationSchema,
        validateAllFieldCriteria,
        transformToNestObject({
          ...fieldValues,
          ...values,
        }),
        validationResolver,
        validationContextRef.current,
      ).then(({ errors }) => {
        const previousFormIsValid = isValidRef.current;
        isValidRef.current = isEmptyObject(errors);

        if (previousFormIsValid !== isValidRef.current) {
          reRender();
        }
      });
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [reRender, validateAllFieldCriteria, validationResolver],
  );

  const removeFieldEventListener = useCallback(
    (field: Field, forceDelete?: boolean) => {
      if (!isUndefined(handleChangeRef.current) && field) {
        findRemovedFieldAndRemoveListener(
          fieldsRef.current,
          handleChangeRef.current,
          field,
          forceDelete,
        );
      }
    },
    [],
  );

  const removeFieldEventListenerAndRef = useCallback(
    (field: Field | undefined, forceDelete?: boolean) => {
      if (
        !field ||
        (field &&
          isNameInFieldArray(fieldArrayNamesRef.current, field.ref.name) &&
          !forceDelete)
      ) {
        return;
      }

      removeFieldEventListener(field, forceDelete);

      const { name } = field.ref;

      errorsRef.current = unset(errorsRef.current, [name]);
      touchedFieldsRef.current = unset(touchedFieldsRef.current, [name]);
      defaultRenderValuesRef.current = unset(defaultRenderValuesRef.current, [
        name,
      ]);
      [
        dirtyFieldsRef,
        fieldsWithValidationRef,
        validFieldsRef,
        watchFieldsRef,
      ].forEach(data => data.current.delete(name));

      if (
        readFormStateRef.current.isValid ||
        readFormStateRef.current.touched
      ) {
        reRender();

        if (shouldValidateCallback) {
          validateSchemaIsValid();
        }
      }
    },
    [
      reRender,
      shouldValidateCallback,
      validateSchemaIsValid,
      removeFieldEventListener,
    ],
  );

  function clearError(): void;
  function clearError(name: FieldName<FormValues>): void;
  function clearError(names: FieldName<FormValues>[]): void;
  function clearError(
    name?: FieldName<FormValues> | FieldName<FormValues>[],
  ): void {
    if (isUndefined(name)) {
      errorsRef.current = {};
    } else {
      unset(errorsRef.current, isArray(name) ? name : [name]);
    }

    reRender();
  }

  const setInternalError = ({
    name,
    type,
    types,
    message,
    preventRender,
  }: {
    name: FieldName<FormValues>;
    type: string;
    types?: MultipleFieldErrors;
    message?: Message;
    preventRender?: boolean;
  }) => {
    const field = fieldsRef.current[name];

    if (
      !isSameError(errorsRef.current[name] as FieldError, {
        type,
        message,
        types,
      })
    ) {
      set(errorsRef.current, name, {
        type,
        types,
        message,
        ref: field ? field.ref : {},
        isManual: true,
      });

      if (!preventRender) {
        reRender();
      }
    }
  };

  function setError(name: ManualFieldError<FormValues>[]): void;
  function setError(
    name: FieldName<FormValues>,
    type: MultipleFieldErrors,
  ): void;
  function setError(
    name: FieldName<FormValues>,
    type: string,
    message?: Message,
  ): void;
  function setError(
    name: FieldName<FormValues> | ManualFieldError<FormValues>[],
    type: string | MultipleFieldErrors = '',
    message?: Message,
  ): void {
    if (isString(name)) {
      setInternalError({
        name,
        ...(isObject(type)
          ? {
              types: type,
              type: '',
            }
          : {
              type,
              message,
            }),
      });
    } else if (isArray(name)) {
      name.forEach(error =>
        setInternalError({ ...error, preventRender: true }),
      );
      reRender();
    }
  }

  function watch(): FormValues;
  function watch(option: { nest: boolean }): FormValues;
  function watch<T extends FieldName<FormValues>>(
    field: T & string,
    defaultValue?: string,
  ): FormValues[T];
  function watch(
    fields: FieldName<FormValues>[] | string[],
    defaultValues?: DeepPartial<FormValues>,
  ): DeepPartial<FormValues>;
  function watch(
    fieldNames?:
      | FieldName<FormValues>
      | FieldName<FormValues>[]
      | { nest: boolean },
    defaultValue?: string | DeepPartial<FormValues>,
  ): FieldValue<FormValues> | DeepPartial<FormValues> | string | undefined {
    const combinedDefaultValues = isDirtyRef.current
      ? {}
      : isUndefined(defaultValue)
      ? isUndefined(defaultValuesRef.current)
        ? {}
        : defaultValuesRef.current
      : defaultValue;
    const fieldValues = getFieldsValues<FormValues>(
      fieldsRef.current,
      fieldNames,
    );
    const watchFields = watchFieldsRef.current;

    if (!isEmptyObject(combinedDefaultValues)) {
      readFormStateRef.current.dirty = true;
    }

    if (isString(fieldNames)) {
      return assignWatchFields<FormValues>(
        fieldValues,
        fieldNames,
        watchFields,
        combinedDefaultValues,
      );
    }

    if (isArray(fieldNames)) {
      return fieldNames.reduce(
        (previous, name) => ({
          ...previous,
          [name]: assignWatchFields<FormValues>(
            fieldValues,
            name,
            watchFields,
            combinedDefaultValues,
          ),
        }),
        {},
      );
    }

    isWatchAllRef.current = true;

    const result =
      (!isEmptyObject(fieldValues) && fieldValues) || combinedDefaultValues;

    return fieldNames && fieldNames.nest
      ? transformToNestObject(result as FieldValues)
      : result;
  }

  function unregister(name: FieldName<FormValues>): void;
  function unregister(names: FieldName<FormValues>[]): void;
  function unregister(
    names: FieldName<FormValues> | FieldName<FormValues>[],
  ): void {
    if (!isEmptyObject(fieldsRef.current)) {
      (isArray(names) ? names : [names]).forEach(fieldName =>
        removeFieldEventListenerAndRef(fieldsRef.current[fieldName], true),
      );
    }
  }

  function registerFieldsRef<Element extends FieldElement>(
    ref: Element,
    validateOptions: ValidationOptions | null = {},
  ): ((name: FieldName<FormValues>) => void) | void {
    if (!ref.name) {
      // eslint-disable-next-line no-console
      return console.warn('Missing name @', ref);
    }

    const { name, type, value } = ref;
    const fieldAttributes = {
      ref,
      ...validateOptions,
    };
    const fields = fieldsRef.current;
    const isRadioOrCheckbox = isRadioOrCheckboxFunction(ref);
    let currentField = fields[name] as Field;
    let isEmptyDefaultValue = true;
    let isFieldArray = false;
    let defaultValue;

    if (
      isRadioOrCheckbox
        ? currentField &&
          isArray(currentField.options) &&
          currentField.options
            .filter(Boolean)
            .find(({ ref }: Field) => value === ref.value)
        : currentField
    ) {
      fields[name as FieldName<FormValues>] = {
        ...currentField,
        ...validateOptions,
      };
      return;
    }

    if (type) {
      const mutationWatcher = onDomRemove(ref, () =>
        removeFieldEventListenerAndRef(fieldAttributes),
      );

      currentField = isRadioOrCheckbox
        ? {
            options: [
              ...((currentField && currentField.options) || []),
              {
                ref,
                mutationWatcher,
              } as RadioOrCheckboxOption,
            ],
            ref: { type, name },
            ...validateOptions,
          }
        : {
            ...fieldAttributes,
            mutationWatcher,
          };
    } else {
      currentField = fieldAttributes;
    }

    fields[name as FieldName<FormValues>] = currentField;

    if (!isEmptyObject(defaultValuesRef.current)) {
      defaultValue = getDefaultValue<FormValues>(
        defaultValuesRef.current,
        name,
      );
      isEmptyDefaultValue = isUndefined(defaultValue);
      isFieldArray = isNameInFieldArray(fieldArrayNamesRef.current, name);

      if (!isEmptyDefaultValue && !isFieldArray) {
        setFieldValue(currentField, defaultValue);
      }
    }

    if (
      shouldValidateCallback &&
      !isFieldArray &&
      readFormStateRef.current.isValid
    ) {
      validateSchemaIsValid();
    } else if (!isEmptyObject(validateOptions)) {
      fieldsWithValidationRef.current.add(name);

      if (!isOnSubmit && readFormStateRef.current.isValid) {
        validateField(fieldsRef, validateAllFieldCriteria, currentField).then(
          error => {
            const previousFormIsValid = isValidRef.current;
            if (isEmptyObject(error)) {
              validFieldsRef.current.add(name);
            } else {
              isValidRef.current = false;
            }

            if (previousFormIsValid !== isValidRef.current) {
              reRender();
            }
          },
        );
      }
    }

    if (
      !defaultRenderValuesRef.current[name] &&
      !(isFieldArray && isEmptyDefaultValue)
    ) {
      defaultRenderValuesRef.current[
        name as FieldName<FormValues>
      ] = isEmptyDefaultValue
        ? getFieldValue(fields, currentField.ref)
        : defaultValue;
    }

    if (!type) {
      return;
    }

    const fieldToAttachListener =
      isRadioOrCheckbox && currentField.options
        ? currentField.options[currentField.options.length - 1]
        : currentField;

    attachEventListeners({
      field: fieldToAttachListener,
      isRadioOrCheckbox,
      handleChange: handleChangeRef.current,
    });
  }

  function register<Element extends FieldElement = FieldElement>(): (
    ref: Element | null,
  ) => void;
  function register<Element extends FieldElement = FieldElement>(
    validationOptions: ValidationOptions,
  ): (ref: Element | null) => void;
  function register<Element extends FieldElement = FieldElement>(
    name: FieldName<FormValues>,
    validationOptions?: ValidationOptions,
  ): void;
  function register<Element extends FieldElement = FieldElement>(
    namesWithValidationOptions: Record<
      FieldName<FormValues>,
      ValidationOptions
    >,
  ): void;
  function register<Element extends FieldElement = FieldElement>(
    ref: Element,
    validationOptions?: ValidationOptions,
  ): void;
  function register<Element extends FieldElement = FieldElement>(
    refOrValidationOptions?: ValidationOptions | Element | null,
    validationOptions?: ValidationOptions,
  ): ((ref: Element | null) => void) | void {
    if (isWindowUndefined) {
      return;
    }

    if (isString(refOrValidationOptions)) {
      registerFieldsRef({ name: refOrValidationOptions }, validationOptions);
      return;
    }

    if (isObject(refOrValidationOptions) && 'name' in refOrValidationOptions) {
      registerFieldsRef(refOrValidationOptions, validationOptions);
      return;
    }

    return (ref: Element | null) =>
      ref && registerFieldsRef(ref, refOrValidationOptions);
  }

  const handleSubmit = useCallback(
    (callback: OnSubmit<FormValues>) => async (
      e?: React.BaseSyntheticEvent,
    ): Promise<void> => {
      if (e) {
        e.preventDefault();
        e.persist();
      }
      let fieldErrors: FieldErrors<FormValues> = {};
      let fieldValues: FieldValues = {};
      const fields = fieldsRef.current;

      if (readFormStateRef.current.isSubmitting) {
        isSubmittingRef.current = true;
        reRender();
      }

      try {
        if (shouldValidateCallback) {
          fieldValues = getFieldsValues(fields);
          const { errors, values } = await validateWithSchema<
            FormValues,
            ValidationContext
          >(
            validationSchema,
            validateAllFieldCriteria,
            transformToNestObject(fieldValues),
            validationResolver,
            validationContextRef.current,
          );
          errorsRef.current = errors;
          fieldErrors = errors;
          fieldValues = values;
        } else {
          for (const field of Object.values(fields)) {
            if (field) {
              const {
                ref,
                ref: { name },
              } = field;

              const fieldError = await validateField(
                fieldsRef,
                validateAllFieldCriteria,
                field,
              );

              if (fieldError[name]) {
                set(fieldErrors, name, fieldError[name]);
                validFieldsRef.current.delete(name);
              } else {
                if (fieldsWithValidationRef.current.has(name)) {
                  validFieldsRef.current.add(name);
                }

                fieldValues[name] = getFieldValue(fields, ref);
              }
            }
          }
        }

        if (isEmptyObject(fieldErrors)) {
          errorsRef.current = {};
          await callback(transformToNestObject(fieldValues), e);
        } else {
          if (submitFocusError && isWeb) {
            focusErrorField(fields, fieldErrors);
          }

          errorsRef.current = fieldErrors;
        }
      } finally {
        isSubmittedRef.current = true;
        isSubmittingRef.current = false;
        submitCountRef.current = submitCountRef.current + 1;
        reRender();
      }
    },
    [
      isWeb,
      reRender,
      shouldValidateCallback,
      submitFocusError,
      validateAllFieldCriteria,
      validationResolver,
      validationSchema,
    ],
  );

  const resetRefs = ({
    errors,
    dirty,
    isSubmitted,
    touched,
    isValid,
    submitCount,
  }: OmitResetState) => {
    fieldsRef.current = {};
    if (!errors) {
      errorsRef.current = {};
    }

    if (!touched) {
      touchedFieldsRef.current = {};
    }

    if (!isValid) {
      validFieldsRef.current = new Set();
      fieldsWithValidationRef.current = new Set();
      isValidRef.current = true;
    }

    if (!dirty) {
      dirtyFieldsRef.current = new Set();
      isDirtyRef.current = false;
    }

    if (!isSubmitted) {
      isSubmittedRef.current = false;
    }

    if (!submitCount) {
      submitCountRef.current = 0;
    }

    defaultRenderValuesRef.current = {};
    watchFieldsRef.current = new Set();
    isWatchAllRef.current = false;
  };

  const reset = (
    values?: DeepPartial<FormValues>,
    omitResetState: OmitResetState = {},
  ): void => {
    if (isWeb) {
      for (const value of Object.values(fieldsRef.current)) {
        if (value) {
          const { ref, options } = value;
          const inputRef =
            isRadioOrCheckboxFunction(ref) && isArray(options)
              ? options[0].ref
              : ref;

          if (isHTMLElement(inputRef)) {
            try {
              inputRef.closest('form')!.reset();
              break;
            } catch {}
          }
        }
      }
    }

    if (values) {
      defaultValuesRef.current = values;
    }

    Object.values(resetFieldArrayFunctionRef.current).forEach(
      resetFieldArray => isFunction(resetFieldArray) && resetFieldArray(),
    );

    resetRefs(omitResetState);

    reRender();
  };

  const getValues = (payload?: { nest: boolean }): FormValues => {
    const fieldValues = getFieldsValues(fieldsRef.current);
    const outputValues = isEmptyObject(fieldValues)
      ? defaultValuesRef.current
      : fieldValues;

    return payload && payload.nest
      ? transformToNestObject(outputValues)
      : outputValues;
  };

  useEffect(
    () => () => {
      isUnMount.current = true;
      fieldsRef.current &&
        process.env.NODE_ENV === 'production' &&
        Object.values(
          fieldsRef.current,
        ).forEach((field: Field | undefined): void =>
          removeFieldEventListenerAndRef(field, true),
        );
    },
    [removeFieldEventListenerAndRef],
  );

  if (!shouldValidateCallback) {
    isValidRef.current =
      validFieldsRef.current.size >= fieldsWithValidationRef.current.size &&
      isEmptyObject(errorsRef.current);
  }

  const formState = {
    dirty: isDirtyRef.current,
    dirtyFields: dirtyFieldsRef.current,
    isSubmitted: isSubmittedRef.current,
    submitCount: submitCountRef.current,
    touched: touchedFieldsRef.current,
    isSubmitting: isSubmittingRef.current,
    isValid: isOnSubmit
      ? isSubmittedRef.current && isEmptyObject(errorsRef.current)
      : isValidRef.current,
  };

  const commonProps = {
    triggerValidation,
    setValue: useCallback(setValue, [
      reRender,
      setInternalValue,
      triggerValidation,
    ]),
    register: useCallback(register, [
      defaultValuesRef.current,
      defaultRenderValuesRef.current,
    ]),
    unregister: useCallback(unregister, []),
    getValues: useCallback(getValues, []),
    formState: isProxyEnabled
      ? new Proxy<FormStateProxy<FormValues>>(formState, {
          get: (obj, prop: keyof FormStateProxy) => {
            if (prop in obj) {
              readFormStateRef.current[prop] = true;
              return obj[prop];
            }

            return {};
          },
        })
      : formState,
  };

  const control = {
    removeFieldEventListener,
    reRender,
    ...(shouldValidateCallback ? { validateSchemaIsValid } : {}),
    ...(isWatchAllRef.current ? {} : { watchFieldsRef }),
    mode: {
      isOnBlur,
      isOnSubmit,
      isOnChange,
    },
    reValidateMode: {
      isReValidateOnBlur,
      isReValidateOnSubmit,
    },
    errorsRef,
    touchedFieldsRef,
    fieldsRef,
    resetFieldArrayFunctionRef,
    fieldArrayDefaultValues,
    validFieldsRef,
    dirtyFieldsRef,
    fieldsWithValidationRef,
    fieldArrayNamesRef,
    isDirtyRef,
    readFormStateRef,
    defaultValuesRef,
    ...commonProps,
  };

  return {
    watch,
    control,
    handleSubmit,
    reset: useCallback(reset, []),
    clearError: useCallback(clearError, []),
    setError: useCallback(setError, []),
    errors: errorsRef.current,
    ...commonProps,
  };
}


// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import 'cypress/support/commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')


import React from 'react';
import { useForm } from 'react-hook-form';
import Joi from '@hapi/joi';

let renderCounter = 0;

const validationSchema = Joi.object({
  firstName: Joi.string().required(),
  lastName: Joi.string()
    .max(5)
    .required(),
  min: Joi.number()
    .min(10)
    .required(),
  max: Joi.number()
    .max(20)
    .required(),
  minDate: Joi.date().min('2019-08-01'),
  maxDate: Joi.date().max('2019-08-01'),
  minLength: Joi.string().min(2),
  minRequiredLength: Joi.string().required(),
  selectNumber: Joi.string().required(),
  pattern: Joi.string().required(),
  radio: Joi.string().required(),
  checkbox: Joi.required(),
});

const validationResolver = (data: any) => {
  const { error, value: values } = validationSchema.validate(data, {
    abortEarly: false,
  });

  return {
    values: error ? {} : values,
    errors: error
      ? error.details.reduce((previous, currentError) => {
        return {
          ...previous,
          [currentError.path[0]]: currentError,
        };
      }, {})
      : {},
  };
};

const BasicSchemaValidation: React.FC = (props: any) => {
  const { register, handleSubmit, errors } = useForm<{
    firstName: string;
    lastName: string;
    min: string;
    max: string;
    minDate: string;
    maxDate: string;
    minLength: string;
    minRequiredLength: string;
    selectNumber: string;
    pattern: string;
    radio: string;
    checkbox: string;
    multiple: string;
    validate: string;
  }>({
    validationResolver,
    mode: props.match.params.mode,
  });
  const onSubmit = () => {};

  renderCounter++;

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="firstName" ref={register} placeholder="firstName" />
      {errors.firstName &&
firstName error
}
      <input name="lastName" ref={register} placeholder="lastName" />
      {errors.lastName &&
lastName error
}
      <input type="number" name="min" ref={register} placeholder="min" />
      {errors.min &&
min error
}
      <input type="number" name="max" ref={register} placeholder="max" />
      {errors.max &&
max error
}
      <input type="date" name="minDate" ref={register} placeholder="minDate" />
      {errors.minDate &&
minDate error
}
      <input type="date" name="maxDate" ref={register} placeholder="maxDate" />
      {errors.maxDate &&
maxDate error
}
      <input name="minLength" ref={register} placeholder="minLength" />
      {errors.minLength &&
minLength error
}
      <input
        name="minRequiredLength"
        ref={register}
        placeholder="minRequiredLength"
      />
      {errors.minRequiredLength &&
minRequiredLength error
}
      <select name="selectNumber" ref={register}>
        <option value="">Select</option>
        <option value={1}>1</option>
        <option value={2}>1</option>
      </select>
      {errors.selectNumber &&
selectNumber error
}
      <input name="pattern" ref={register} placeholder="pattern" />
      {errors.pattern &&
pattern error
}
      Radio1
      <input type="radio" name="radio" ref={register} value="1" />
      Radio2
      <input type="radio" name="radio" ref={register} value="2" />
      Radio3
      <input type="radio" name="radio" ref={register} value="3" />
      {errors.radio &&
radio error
}
      <input type="checkbox" name="checkbox" ref={register} />
      {errors.checkbox &&
checkbox error
}
      <button>Submit</button>

{renderCounter}

    </form>
  );
};

export default BasicSchemaValidation;


export default ({
  isOnChange,
  hasError,
  isBlurEvent,
  isOnSubmit,
  isReValidateOnSubmit,
  isOnBlur,
  isReValidateOnBlur,
  isSubmitted,
}: {
  hasError: boolean;
  isOnChange: boolean;
  isBlurEvent?: boolean;
  isOnSubmit: boolean;
  isOnBlur: boolean;
  isReValidateOnSubmit: boolean;
  isReValidateOnBlur: boolean;
  isSubmitted: boolean;
}) =>
  (isOnChange && isBlurEvent) ||
  (isOnSubmit && isReValidateOnSubmit) ||
  (isOnSubmit && !isSubmitted) ||
  (isOnBlur && !isBlurEvent && !hasError) ||
  (isReValidateOnBlur && !isBlurEvent && hasError) ||
  (isReValidateOnSubmit && isSubmitted);


return valueRef.current;
},
}),
rules,
);
}, [
fieldArrayNamesRef,


import isArray from './isArray';
import { ArrayField } from '../types';

export default <FormArrayValues, KeyName extends string>(
  fields: Partial<ArrayField<FormArrayValues, KeyName>>[],
  indexA: number,
  indexB: number,
) => {
  if (isArray(fields)) {
    const temp = [fields[indexB], fields[indexA]];
    fields[indexA] = temp[0];
    fields[indexB] = temp[1];
  }
};


context('watchUseFieldArray', () => {
  it('should behaviour correctly when watching the field array', () => {
    cy.visit('http://localhost:3000/watch-field-array/normal');

    cy.get('#append').click();
    cy.get('#result').contains('[{"name":"2"}]');

    cy.get('#field0').type('test');
    cy.get('#result').contains('[{"name":"2test"}]');

    cy.get('#prepend').click();
    cy.get('#result').contains('[{"name":"9"},{"name":"2test"}]');

    cy.get('#append').click();
    cy.get('#append').click();
    cy.get('#append').click();
    cy.get('#result').contains(
      '[{"name":"9"},{"name":"2test"},{"name":"11"},{"name":"13"},{"name":"15"}]',
    );

    cy.get('#swap').click();
    cy.get('#result').contains(
      '[{"name":"9"},{"name":"11"},{"name":"2test"},{"name":"13"},{"name":"15"}]',
    );

    cy.get('#move').click();
    cy.get('#result').contains(
      '[{"name":"2test"},{"name":"9"},{"name":"11"},{"name":"13"},{"name":"15"}]',
    );

    cy.get('#insert').click();
    cy.get('#result').contains(
      '[{"name":"2test"},{"name":"21"},{"name":"9"},{"name":"11"},{"name":"13"},{"name":"15"}]',
    );

    cy.get('#remove').click();
    cy.get('#result').contains(
      '[{"name":"2test"},{"name":"9"},{"name":"11"},{"name":"13"},{"name":"15"}]',
    );

    cy.get('#removeAll').click();
    cy.get('#result').should('be.empty');
  });

  it('should return empty when items been removed and defaultValues are supplied', () => {
    cy.visit('http://localhost:3000/watch-field-array/default');

    cy.get('#delete0').click();
    cy.get('#delete0').click();
    cy.get('#delete0').click();

    cy.get('#result').should('be.empty');
  });
});


import * as React from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { useForm } from './';
import attachEventListeners from './logic/attachEventListeners';
import getFieldsValues from './logic/getFieldsValues';
import findRemovedFieldAndRemoveListener from './logic/findRemovedFieldAndRemoveListener';
import validateWithSchema from './logic/validateWithSchema';
import validateField from './logic/validateField';
import onDomRemove from './utils/onDomRemove';
import { VALIDATION_MODE } from './constants';
import { Control } from './types';

export const reconfigureControl = (
  controlOverrides: Partial<Control> = {},
): Control => ({
  defaultValuesRef: {
    current: {},
  },
  validFieldsRef: {
    current: new Set(),
  },
  fieldsWithValidationRef: {
    current: new Set(),
  },
  fieldArrayDefaultValues: {
    current: {},
  },
  watchFieldsRef: {
    current: new Set(),
  },
  dirtyFieldsRef: {
    current: new Set(),
  },
  validateSchemaIsValid: jest.fn(),
  getValues: jest.fn(),
  reRender: jest.fn(),
  setValue: jest.fn(),
  register: jest.fn(),
  unregister: jest.fn(),
  triggerValidation: jest.fn(),
  removeFieldEventListener: jest.fn(),
  errorsRef: { current: {} },
  touchedFieldsRef: { current: {} },
  mode: { isOnSubmit: false, isOnBlur: false, isOnChange: false },
  reValidateMode: {
    isReValidateOnBlur: false,
    isReValidateOnSubmit: false,
  },
  formState: {
    dirty: false,
    isSubmitted: false,
    dirtyFields: new Set(),
    submitCount: 0,
    touched: {},
    isSubmitting: false,
    isValid: false,
  },
  fieldsRef: {
    current: {},
  },
  resetFieldArrayFunctionRef: {
    current: {},
  },
  fieldArrayNamesRef: {
    current: new Set<string>(),
  },
  isDirtyRef: {
    current: false,
  },
  readFormStateRef: {
    current: {
      dirty: true,
      isSubmitted: false,
      submitCount: false,
      touched: false,
      isSubmitting: false,
      isValid: false,
      dirtyFields: false,
    },
  },
  ...controlOverrides,
});

jest.mock('./utils/onDomRemove');
jest.mock('./logic/findRemovedFieldAndRemoveListener');
jest.mock('./logic/validateField');
jest.mock('./logic/attachEventListeners');
jest.mock('./logic/getFieldsValues');
jest.mock('./logic/validateWithSchema');
jest.mock('./logic/transformToNestObject', () => ({
  default: (data: any) => data,
  esmodule: true,
}));

let nodeEnv: any;

describe('useForm', () => {
  beforeEach(() => {
    nodeEnv = process.env.NODE_ENV;
    process.env.NODE_ENV = 'production';
    jest.resetAllMocks();
  });

  afterEach(() => {
    process.env.NODE_ENV = nodeEnv;
  });

  describe('register', () => {
    it('should return undefined when ref is defined', () => {
      const { result } = renderHook(() => useForm());

      act(() => {
        expect(result.current.register(undefined as any)).toBeDefined();
      });
    });

    it('should return undefined when ref name is missing', () => {
      const { result } = renderHook(() => useForm());

      act(() => {
        expect(
          result.current.register({ name: 'test', type: 'input' }, {}),
        ).toBeUndefined();
      });
    });

    it('should register field and call attachEventListeners method', () => {
      const { result } = renderHook(() => useForm());

      act(() => {
        result.current.register({ type: 'input', name: 'test' });
      });

      expect(attachEventListeners).toHaveBeenCalledWith({
        field: {
          mutationWatcher: undefined,
          ref: {
            name: 'test',
            type: 'input',
          },
        },
        isRadioOrCheckbox: false,
        handleChange: expect.any(Function),
      });
      expect(onDomRemove).toHaveBeenCalled();
    });

    it('should register field for radio type and call attachEventListeners method', () => {
      const { result } = renderHook(() => useForm());

      act(() => {
        result.current.register({ type: 'radio', name: 'test' });
      });

      expect(attachEventListeners).toBeCalledWith({
        field: {
          mutationWatcher: undefined,
          ref: {
            name: 'test',
            type: 'radio',
          },
        },
        isRadioOrCheckbox: true,
        handleChange: expect.any(Function),
      });
      expect(onDomRemove).toBeCalled();
    });

    it('should register field for checkbox type and call attachEventListeners method', () => {
      const { result } = renderHook(() => useForm());

      act(() => {
        result.current.register({
          type: 'checkbox',
          name: 'test',
          attributes: {},
        });
      });

      expect(attachEventListeners).toBeCalledWith({
        field: {
          mutationWatcher: undefined,
          ref: {
            name: 'test',
            type: 'checkbox',
            attributes: {},
          },
        },
        isRadioOrCheckbox: true,
        handleChange: expect.any(Function),
      });
      expect(onDomRemove).toBeCalled();
    });

    it('should support register passed to ref', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        result.current.register({ required: true })!({
          type: 'text',
          name: 'test',
          value: 'testData',
        });
      });

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      await act(async () => {
        await result.current.handleSubmit(data => {
          expect(data).toEqual({
            test: 'testData',
          });
        })({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
    });

    it('should not register the same radio input', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        const { register } = result.current;
        register({ type: 'radio', name: 'test', value: '' });
        register({ type: 'radio', name: 'test', value: '' });
      });

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      await act(async () => {
        await result.current.handleSubmit(data => {
          expect(data).toEqual({
            test: '',
          });
        })({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
    });

    it('should not register the same checkbox input', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        const { register } = result.current;
        register({ type: 'checkbox', name: 'test', attributes: {} });
        register({ type: 'checkbox', name: 'test', attributes: {} });
      });

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      await act(async () => {
        await result.current.handleSubmit(data => {
          expect(data).toEqual({
            test: false,
          });
        })({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
    });

    it('react native - allow registration as part of the register call', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        result.current.register({ name: 'test' });
        result.current.setValue('test', '1');
      });

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      await act(async () => {
        await result.current.handleSubmit(data => {
          expect(data).toEqual({
            test: '1',
          });
        })({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
    });
  });

  describe('unregister', () => {
    it('should unregister an registered item', async () => {
      const { result } = renderHook(() => useForm());

      act(() => {
        result.current.register({ name: 'input' });
        result.current.unregister('input');
      });

      const callback = jest.fn();

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      await act(async () => {
        await result.current.handleSubmit(callback)({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });

      expect(findRemovedFieldAndRemoveListener).toBeCalled();
    });
  });

  describe('watch', () => {
    it('should watch individual input', () => {
      (getFieldsValues as any).mockImplementation(() => {
        return {};
      });
      const { result } = renderHook(() => useForm<{ test: string }>());

      expect(result.current.watch('test')).toBeUndefined();

      act(() => {
        result.current.register({ type: 'radio', name: 'test', value: '' });
      });

      (getFieldsValues as any).mockImplementation(() => {
        return { test: 'data' };
      });

      expect(result.current.watch('test')).toBe('data');
    });

    it('should watch array of inputs', () => {
      (getFieldsValues as any).mockImplementation(() => {
        return {};
      });
      const { result } = renderHook(() =>
        useForm<{ test: string; test1: string }>(),
      );

      expect(result.current.watch(['test', 'test1'])).toEqual({
        test: undefined,
        test1: undefined,
      });

      act(() => {
        result.current.register({ type: 'radio', name: 'test', value: '' });
        result.current.register({ type: 'radio', name: 'test1', value: '' });
      });

      (getFieldsValues as any).mockImplementation(() => {
        return {
          test: 'data1',
          test1: 'data2',
        };
      });

      expect(result.current.watch(['test', 'test1'])).toEqual({
        test: 'data1',
        test1: 'data2',
      });
    });

    it('should watch every fields', () => {
      const { result } = renderHook(() =>
        useForm<{ test: string; test1: string }>(),
      );

      expect(result.current.watch()).toEqual({});

      act(() => {
        result.current.register({ type: 'radio', name: 'test', value: '' });
        result.current.register({ type: 'radio', name: 'test1', value: '' });
      });

      (getFieldsValues as any).mockImplementation(() => {
        return {
          test: 'data1',
          test1: 'data2',
        };
      });

      expect(result.current.watch()).toEqual({
        test: 'data1',
        test1: 'data2',
      });
    });
  });

  describe('reset', () => {
    it('should reset the form and re-render the form', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        result.current.register({ name: 'test' });
        result.current.setValue('test', 'data');
      });

      (validateField as any).mockImplementation(async () => ({}));

      act(() => {
        result.current.register({ type: 'text', name: 'test' });
      });

      await act(async () => {
        await result.current.handleSubmit(data => {
          expect(data).toEqual({
            test: 'data',
          });
        })({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });

      expect(result.current.formState.isSubmitted).toBeTruthy();
      act(() => {
        result.current.reset();
      });
      expect(result.current.formState.isSubmitted).toBeFalsy();
    });
  });

  describe('setValue', () => {
    it('should set value of radio input correctly', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        result.current.register({ name: 'test', type: 'radio', value: '1' });
        result.current.register({ name: 'test', type: 'radio', value: '2' });
        result.current.setValue('test', '1');
      });

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      act(() => {
        result.current.register({ type: 'text', name: 'test' });
      });

      await act(async () => {
        await result.current.handleSubmit(data => {
          expect(data).toEqual({
            test: '1',
          });
        })({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
    });

    it('should set value of multiple checkbox input correctly', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        result.current.register({
          name: 'test',
          type: 'checkbox',
          value: '1',
          attributes: { value: '1' },
        });
        result.current.register({
          name: 'test',
          type: 'checkbox',
          value: '2',
          attributes: { value: '2' },
        });
        result.current.setValue('test', '1');
      });

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      act(() => {
        result.current.register({ type: 'text', name: 'test' });
      });

      await act(async () => {
        await result.current.handleSubmit(data => {
          expect(data).toEqual({
            test: ['1'],
          });
        })({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
    });

    it('should set value of single checkbox input correctly', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        result.current.register({
          name: 'test',
          type: 'checkbox',
          value: '1',
          attributes: { value: '1' },
        });
        result.current.setValue('test', '1');
      });

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      act(() => {
        result.current.register({ type: 'text', name: 'test' });
      });

      await act(async () => {
        await result.current.handleSubmit(data => {
          expect(data).toEqual({
            test: '1',
          });
        })({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
    });

    it('should set value of multiple select correctly', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        result.current.register({
          name: 'test',
          type: 'select-multiple',
          value: '1',
          options: [{ value: '1', selected: true }] as any,
        });
      });

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      act(() => {
        result.current.setValue('test', '1');
      });

      await act(async () => {
        await result.current.handleSubmit(data => {
          expect(data).toEqual({
            test: ['1'],
          });
        })({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
    });

    it('should return undefined when filed not found', () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        expect(result.current.setValue('test', '1')).toBeUndefined();
      });
    });

    it('should work with array fields', () => {
      const { result } = renderHook(() => useForm());

      act(() => {
        result.current.register('test1[0].test');
        result.current.register('test[0]');
        result.current.register('test[1]');
        result.current.register('test[2]');
      });

      act(() => {
        result.current.setValue('test', ['1', '2', '3']);

        expect(result.current.control.fieldsRef.current['test[0]']).toEqual({
          ref: { name: 'test[0]', value: '1' },
        });
        expect(result.current.control.fieldsRef.current['test[1]']).toEqual({
          ref: { name: 'test[1]', value: '2' },
        });
        expect(result.current.control.fieldsRef.current['test[2]']).toEqual({
          ref: { name: 'test[2]', value: '3' },
        });
      });
    });

    it('should worked with nested array fields with object', () => {
      const { result } = renderHook(() => useForm());

      act(() => {
        result.current.register('test[0].test');
        result.current.register('test[1].test');
        result.current.register('test[2].test');
      });

      act(() => {
        result.current.setValue('test', [
          { test: '1' },
          { test: '2' },
          { test: '3' },
        ]);

        expect(
          result.current.control.fieldsRef.current['test[0].test'],
        ).toEqual({
          ref: { name: 'test[0].test', value: '1' },
        });
        expect(
          result.current.control.fieldsRef.current['test[1].test'],
        ).toEqual({
          ref: { name: 'test[1].test', value: '2' },
        });
        expect(
          result.current.control.fieldsRef.current['test[2].test'],
        ).toEqual({
          ref: { name: 'test[2].test', value: '3' },
        });
      });
    });

    it('should work with object fields', () => {
      const { result } = renderHook(() => useForm());

      act(() => {
        result.current.register('test1[0].test');
        result.current.register('test.bill');
        result.current.register('test.luo');
        result.current.register('test.test');
      });

      act(() => {
        result.current.setValue('test', { bill: '1', luo: '2', test: '3' });

        expect(result.current.control.fieldsRef.current['test.bill']).toEqual({
          ref: { name: 'test.bill', value: '1' },
        });
        expect(result.current.control.fieldsRef.current['test.luo']).toEqual({
          ref: { name: 'test.luo', value: '2' },
        });
        expect(result.current.control.fieldsRef.current['test.test']).toEqual({
          ref: { name: 'test.test', value: '3' },
        });
      });
    });

    it('should work array of fields', () => {
      const { result } = renderHook(() => useForm());

      act(() => {
        result.current.register('test.bill');
        result.current.register('test.luo');
        result.current.register('test.test');
        result.current.register('array[0]');
        result.current.register('array[1]');
        result.current.register('array[2]');
        result.current.register('object.bill');
        result.current.register('object.luo');
        result.current.register('object.test');
      });

      act(() => {
        result.current.setValue([
          { 'test.bill': '1' },
          { array: ['1', '2', '3'] },
          { object: { bill: '1', luo: '2', test: '3' } },
        ]);

        expect(result.current.control.fieldsRef.current['test.bill']).toEqual({
          ref: { name: 'test.bill', value: '1' },
        });

        expect(result.current.control.fieldsRef.current['array[0]']).toEqual({
          ref: { name: 'array[0]', value: '1' },
        });
        expect(result.current.control.fieldsRef.current['array[1]']).toEqual({
          ref: { name: 'array[1]', value: '2' },
        });
        expect(result.current.control.fieldsRef.current['array[2]']).toEqual({
          ref: { name: 'array[2]', value: '3' },
        });

        expect(result.current.control.fieldsRef.current['object.bill']).toEqual(
          {
            ref: { name: 'object.bill', value: '1' },
          },
        );
        expect(result.current.control.fieldsRef.current['object.luo']).toEqual({
          ref: { name: 'object.luo', value: '2' },
        });
        expect(result.current.control.fieldsRef.current['object.test']).toEqual(
          {
            ref: { name: 'object.test', value: '3' },
          },
        );
      });
    });
  });

  describe('triggerValidation', () => {
    it('should return false when field is not found', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());
      await act(async () => {
        expect(await result.current.triggerValidation('test')).toBeFalsy();
      });
    });

    it('should return true when field is found and validation pass', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        result.current.register({ type: 'input', name: 'test' });
      });

      (validateField as any).mockImplementation(async () => ({}));

      await act(async () => {
        expect(await result.current.triggerValidation('test')).toBeTruthy();
      });
    });

    it('should update value when value is supplied', async () => {
      const { result } = renderHook(() =>
        useForm<{ test: string }>({
          mode: VALIDATION_MODE.onChange,
        }),
      );

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      act(() => {
        result.current.register(
          { type: 'input', name: 'test' },
          { required: true },
        );
      });

      await act(async () => {
        expect(await result.current.triggerValidation('test')).toBeTruthy();
      });
    });

    it('should trigger multiple fields validation', async () => {
      const { result } = renderHook(() =>
        useForm<{ test: string }>({
          mode: VALIDATION_MODE.onChange,
        }),
      );

      (validateField as any).mockImplementation(async () => ({}));

      act(() => {
        result.current.register({
          name: 'test',
        });
        result.current.register({
          name: 'test1',
        });
      });

      await act(async () => {
        await result.current.triggerValidation(['test', 'test1'] as any);
      });

      expect(validateField).toBeCalledWith(
        {
          current: {
            test: { ref: { name: 'test' } },
            test1: { ref: { name: 'test1' } },
          },
        },
        false,
        { ref: { name: 'test' } },
      );
      expect(validateField).toBeCalledWith(
        {
          current: {
            test: { ref: { name: 'test' } },
            test1: { ref: { name: 'test1' } },
          },
        },
        false,
        { ref: { name: 'test1' } },
      );
    });
  });

  describe('triggerValidation with schema', () => {
    it('should return the error with single field validation', async () => {
      const { result } = renderHook(() =>
        useForm<{ test: string }>({
          mode: VALIDATION_MODE.onChange,
          validationSchema: { test: 'test' },
        }),
      );

      (getFieldsValues as any).mockImplementation(() => {
        return {};
      });

      (validateWithSchema as any).mockImplementation(async (payload: any) => {
        return {
          errors: payload,
          values: {},
        };
      });

      act(() => {
        result.current.register(
          { type: 'input', name: 'test' },
          { required: true },
        );
      });

      await act(async () => {
        await result.current.triggerValidation('test');
        expect(result.current.errors).toEqual({ test: 'test' });
      });
    });

    it('should return the status of the requested field with single field validation', async () => {
      const { result } = renderHook(() =>
        useForm<{ test1: string; test2: string }>({
          mode: VALIDATION_MODE.onChange,
          validationSchema: { test2: 'test2' },
        }),
      );

      (getFieldsValues as any).mockImplementation(() => {
        return {};
      });

      (validateWithSchema as any).mockImplementation(async (payload: any) => {
        return {
          errors: payload,
          values: {},
        };
      });

      act(() => {
        result.current.register(
          { type: 'input', name: 'test1' },
          { required: false },
        );
        result.current.register(
          { type: 'input', name: 'test2' },
          { required: true },
        );
      });

      await act(async () => {
        const resultFalse = await result.current.triggerValidation('test2');
        expect(resultFalse).toEqual(false);

        expect(result.current.errors).toEqual({
          test2: 'test2',
        });
      });
    });

    it('should not trigger any error when schema validation result not found', async () => {
      const { result } = renderHook(() =>
        useForm<{ test: string }>({
          mode: VALIDATION_MODE.onChange,
          validationSchema: { test: 'test' },
        }),
      );

      (getFieldsValues as any).mockImplementation(() => {
        return {};
      });

      (validateWithSchema as any).mockImplementation(async () => {
        return {
          errors: {
            values: 'test',
          },
          result: {},
        };
      });

      act(() => {
        result.current.register(
          { type: 'input', name: 'test' },
          { required: true },
        );
      });

      await act(async () => {
        await result.current.triggerValidation('test');
      });

      expect(result.current.errors).toEqual({});
    });

    it('should support array of fields for schema validation', async () => {
      const { result } = renderHook(() =>
        useForm<{ test: string; test1: string }>({
          mode: VALIDATION_MODE.onChange,
          validationSchema: {},
        }),
      );

      (getFieldsValues as any).mockImplementation(() => {
        return {};
      });

      (validateWithSchema as any).mockImplementation(async () => {
        return {
          errors: {
            test1: 'test1',
            test: 'test',
          },
          result: {},
        };
      });

      act(() => {
        result.current.register(
          { type: 'input', name: 'test' },
          { required: true },
        );
      });

      await act(async () => {
        await result.current.triggerValidation(['test', 'test1']);

        expect(result.current.errors).toEqual({
          test: 'test',
          test1: 'test1',
        });
      });
    });

    it('should return the status of the requested fields with array of fields for validation', async () => {
      const { result } = renderHook(() =>
        useForm<{ test1: string; test2: string; test3: string }>({
          mode: VALIDATION_MODE.onChange,
          validationSchema: { test3: 'test3' },
        }),
      );

      (getFieldsValues as any).mockImplementation(() => {
        return {};
      });

      (validateWithSchema as any).mockImplementation(async (payload: any) => {
        return {
          errors: payload,
          values: {},
        };
      });

      act(() => {
        result.current.register(
          { type: 'input', name: 'test1' },
          { required: false },
        );
        result.current.register(
          { type: 'input', name: 'test2' },
          { required: false },
        );
        result.current.register(
          { type: 'input', name: 'test3' },
          { required: true },
        );
      });

      await act(async () => {
        const resultTrue = await result.current.triggerValidation([
          'test1',
          'test2',
        ]);
        expect(resultTrue).toEqual(true);
      });
    });

    it('should validate all fields when pass with undefined', async () => {
      const { result } = renderHook(() =>
        useForm<{ test1: string; test: string }>({
          mode: VALIDATION_MODE.onChange,
          validationSchema: { test: 'test' },
        }),
      );

      (getFieldsValues as any).mockImplementation(() => {
        return {};
      });

      (validateWithSchema as any).mockImplementation(async () => {
        return {
          errors: {
            test1: 'test1',
            test: 'test',
          },
          result: {},
        };
      });

      act(() => {
        result.current.register(
          { type: 'input', name: 'test' },
          { required: true },
        );
        result.current.register(
          { type: 'input', name: 'test1' },
          { required: true },
        );
      });

      await act(async () => {
        await result.current.triggerValidation();

        expect(result.current.errors).toEqual({
          test: 'test',
          test1: 'test1',
        });
      });
    });
  });

  describe('handleSubmit', () => {
    it('should invoke the callback when validation pass', async () => {
      const { result } = renderHook(() => useForm());
      const callback = jest.fn();

      await act(async () => {
        await result.current.handleSubmit(callback)({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
      expect(callback).toBeCalled();
    });

    it('should not invoke callback when there are errors', async () => {
      const { result } = renderHook(() => useForm<{ test: string }>());

      act(() => {
        result.current.register(
          { value: '', type: 'input', name: 'test' },
          { required: true },
        );
      });

      const callback = jest.fn();
      (validateField as any).mockImplementation(async () => {
        return { test: { type: 'test' } };
      });

      await act(async () => {
        await result.current.handleSubmit(callback)({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
      expect(callback).not.toBeCalled();
    });
  });

  describe('handleSubmit with validationSchema', () => {
    it('should invoke callback when error not found', async () => {
      const { result } = renderHook(() =>
        useForm<{ test: string }>({
          mode: VALIDATION_MODE.onSubmit,
          validationSchema: {},
        }),
      );

      (getFieldsValues as any).mockImplementation(() => {
        return {};
      });

      (validateWithSchema as any).mockImplementation(async () => {
        return {
          errors: {},
          values: {},
        };
      });

      act(() => {
        result.current.register(
          { value: '', type: 'input', name: 'test' },
          { required: true },
        );
      });

      const callback = jest.fn();
      (getFieldsValues as any).mockImplementation(async () => {
        return { test: 'test' };
      });

      await act(async () => {
        await result.current.handleSubmit(callback)({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
      expect(callback).toBeCalled();
    });
  });

  describe('getValues', () => {
    it('should call getFieldsValues and return all values', () => {
      const { result } = renderHook(() => useForm<{ test: string }>());
      act(() => {
        result.current.register({ value: 'test', type: 'input', name: 'test' });
      });
      (getFieldsValues as any).mockImplementation(async () => {
        return {};
      });
      act(() => {
        result.current.getValues();
      });
      expect(getFieldsValues).toBeCalled();
    });
  });

  describe('setError', () => {
    it('should only set an error when it is not existed', () => {
      const { result } = renderHook(() => useForm<{ input: string }>());
      act(() => {
        result.current.setError('input', 'test');
      });
      expect(result.current.errors).toEqual({
        input: {
          type: 'test',
          isManual: true,
          message: undefined,
          ref: {},
        },
      });

      act(() => {
        result.current.setError('input', 'test', 'test');
      });
      expect(result.current.errors).toEqual({
        input: {
          type: 'test',
          isManual: true,
          message: 'test',
          ref: {},
        },
      });

      act(() => {
        result.current.setError('input', 'test', <p>test</p>);
      });
      expect(result.current.errors).toEqual({
        input: {
          type: 'test',
          isManual: true,
          message: <p>test</p>,
          ref: {},
        },
      });

      act(() => {
        result.current.setError('input', { test1: 'test1', test2: 'test2' });
      });
      expect(result.current.errors).toEqual({
        input: {
          type: '',
          isManual: true,
          message: undefined,
          types: {
            test1: 'test1',
            test2: 'test2',
          },
          ref: {},
        },
      });

      act(() => {
        result.current.setError('input', {
          test1: 'test1',
          test2: <p>test2</p>,
        });
      });
      expect(result.current.errors).toEqual({
        input: {
          type: '',
          isManual: true,
          message: undefined,
          types: {
            test1: 'test1',
            test2: <p>test2</p>,
          },
          ref: {},
        },
      });
    });

    it('should remove error', () => {
      const { result } = renderHook(() => useForm<{ input: string }>());
      act(() => {
        result.current.setError('input', 'test', 'message');
        result.current.clearError('input');
      });
      expect(result.current.errors).toEqual({});
    });

    it('should remove nested error', () => {
      const { result } = renderHook(() =>
        useForm<{ input: { nested: string } }>(),
      );
      act(() => {
        result.current.setError('input.nested', 'test');
      });
      expect(result.current.errors.input?.nested).toBeDefined();
      act(() => {
        result.current.clearError('input.nested');
      });
      expect(result.current.errors.input?.nested).not.toBeDefined();
    });
  });

  describe('setErrors', () => {
    it('should set multiple errors for the form', () => {
      const { result } = renderHook(() =>
        useForm<{ input: string; input1: string; input2: string }>(),
      );
      act(() => {
        result.current.setError([
          {
            type: 'test',
            name: 'input',
            message: 'wow',
          },
          {
            type: 'test1',
            name: 'input1',
            message: 'wow1',
          },
          {
            type: 'test2',
            name: 'input2',
            message: <p>wow2</p>,
          },
        ]);
      });

      expect(result.current.errors).toEqual({
        input: {
          type: 'test',
          isManual: true,
          message: 'wow',
          ref: {},
        },
        input1: {
          type: 'test1',
          isManual: true,
          message: 'wow1',
          ref: {},
        },
        input2: {
          type: 'test2',
          isManual: true,
          message: <p>wow2</p>,
          ref: {},
        },
      });
    });
  });

  describe('formState', () => {
    it('should disable isValid for submit mode', () => {
      const { result } = renderHook(() => useForm<{ input: string }>());
      expect(result.current.formState.isValid).toBeFalsy();
    });

    it('should return true for onBlur mode by default', () => {
      const { result } = renderHook(() =>
        useForm<{ input: string }>({
          mode: VALIDATION_MODE.onBlur,
        }),
      );

      expect(result.current.formState.isValid).toBeTruthy();
    });

    it('should return true for onBlur when validation schema by default', () => {
      const { result } = renderHook(() =>
        useForm<{ input: string }>({
          mode: VALIDATION_MODE.onBlur,
          validationSchema: {},
        }),
      );

      expect(result.current.formState.isValid).toBeTruthy();
    });

    it('should return true for onChange mode by default', () => {
      const { result } = renderHook(() =>
        useForm<{ input: string }>({
          mode: VALIDATION_MODE.onChange,
        }),
      );

      expect(result.current.formState.isValid).toBeTruthy();
    });

    it('should return true when no validation is registered', () => {
      const { result } = renderHook(() =>
        useForm<{ input: string }>({
          mode: VALIDATION_MODE.onBlur,
        }),
      );

      act(() => {
        result.current.register({ type: 'text', name: 'test' });
      });

      expect(result.current.formState.isValid).toBeTruthy();
    });

    it('should return false when default value is not valid value', async () => {
      const { result } = renderHook(() =>
        useForm<{ input: string }>({
          mode: VALIDATION_MODE.onChange,
        }),
      );

      (validateField as any).mockImplementation(async () => {
        return { issue: 'issue' };
      });

      await act(async () => {
        result.current.formState.isValid;
      });

      await act(async () => {
        result.current.register(
          { name: 'issue', value: 'test' },
          { required: true },
        );
        result.current.register({ name: 'input' });
      });

      expect(result.current.formState.isValid).toBeFalsy();
    });

    it('should return true when default value meet the validation criteria', async () => {
      const { result } = renderHook(() =>
        useForm<{ input: string }>({
          mode: VALIDATION_MODE.onChange,
        }),
      );

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      await act(async () => {
        result.current.formState.isValid;
      });

      await act(async () => {
        result.current.register(
          { name: 'issue', value: 'test' },
          { required: true },
        );
        result.current.register({ name: 'input' }, { required: true });

        expect(result.current.formState.isValid).toBeTruthy();
      });
    });
  });

  describe('when component unMount', () => {
    it('should call unSubscribe', () => {
      const { result, unmount } = renderHook(() => useForm<{ test: string }>());

      result.current.register({ type: 'text', name: 'test' });
      unmount();
      expect(findRemovedFieldAndRemoveListener).toBeCalled();
    });
  });

  describe('when defaultValues is supplied', () => {
    it('should populate the input with them', async () => {
      const { result } = renderHook(() =>
        useForm<{ test: string; deep: { nested: string; values: string } }>({
          mode: VALIDATION_MODE.onSubmit,
          defaultValues: {
            test: 'data',
            deep: {
              values: '5',
            },
          },
        }),
      );

      (validateField as any).mockImplementation(async () => {
        return {};
      });

      act(() => {
        result.current.register({ type: 'text', name: 'test' });
        result.current.register({ type: 'text', name: 'deep.nested' });
        result.current.register({ type: 'text', name: 'deep.values' });
      });

      await act(async () => {
        await result.current.handleSubmit((data: any) => {
          expect(data).toEqual({
            test: 'data',
            'deep.nested': undefined,
            'deep.values': '5',
          });
        })({
          preventDefault: () => {},
          persist: () => {},
        } as React.SyntheticEvent);
      });
    });

    it('should infer from defaultValues without a type parameter', () => {
      const { result } = renderHook(() =>
        useForm({
          mode: VALIDATION_MODE.onSubmit,
          defaultValues: {
            test: 'data',
            deep: {
              values: '5',
            },
          },
        }),
      );

      (getFieldsValues as any).mockImplementation(async () => {
        return {};
      });

      act(() => {
        const test: string = result.current.getValues({ nest: true }).test;
        expect(test).toEqual('data');
        const deep: { values: string } = result.current.getValues({
          nest: true,
        }).deep;
        expect(deep).toEqual({ values: '5' });
      });
    });
  });
});


keep keep keep add keep keep keep keep keep

import * as React from 'react';
import isBoolean from './utils/isBoolean';
import isUndefined from './utils/isUndefined';
import get from './utils/get';
import getInputValue from './logic/getInputValue';
import skipValidation from './logic/skipValidation';
import isNameInFieldArray from './logic/isNameInFieldArray';
import { useFormContext } from './useFormContext';
import { VALIDATION_MODE, VALUE } from './constants';
import { Control, ControllerProps, EventFunction, Field } from './types';

const Controller = <
  As extends
    | React.ReactElement
    | React.ComponentType<any>
    | keyof JSX.IntrinsicElements,
  ControlProp extends Control = Control
>({
  name,
  rules,
  as: InnerComponent,
  onBlur,
  onChange,
  onChangeName = VALIDATION_MODE.onChange,
  onBlurName = VALIDATION_MODE.onBlur,
  valueName,
  defaultValue,
  control,
  onFocus,
  ...rest
}: ControllerProps<As, ControlProp>) => {
  const methods = useFormContext();
  const {
    defaultValuesRef,
    setValue,
    register,
    unregister,
    errorsRef,
    removeFieldEventListener,
    triggerValidation,
    mode: { isOnSubmit, isOnBlur, isOnChange },
    reValidateMode: { isReValidateOnBlur, isReValidateOnSubmit },
    formState: { isSubmitted },
    fieldsRef,
    fieldArrayNamesRef,
  } = control || methods.control;
  const [value, setInputStateValue] = React.useState(
    isUndefined(defaultValue)
      ? get(defaultValuesRef.current, name)
      : defaultValue,
  );
  const valueRef = React.useRef(value);
  const isCheckboxInput = isBoolean(value);
  const shouldReValidateOnBlur = isOnBlur || isReValidateOnBlur;

  const shouldValidate = () =>
    !skipValidation({
      hasError: !!get(errorsRef.current, name),
      isOnBlur,
      isOnSubmit,
      isOnChange,
      isReValidateOnBlur,
      isReValidateOnSubmit,
      isSubmitted,
    });

  const commonTask = (event: any) => {
    const data = getInputValue(event, isCheckboxInput);
    setInputStateValue(data);
    valueRef.current = data;
    return data;
  };

  const eventWrapper = (event: EventFunction) => (...arg: any[]) =>
    setValue(name, commonTask(event(arg)), shouldValidate());

  const handleChange = (event: any) => {
    const data = commonTask(event);
    setValue(name, data, shouldValidate());
  };

  const registerField = React.useCallback(() => {
    if (
      isNameInFieldArray(fieldArrayNamesRef.current, name) &&
      fieldsRef.current[name]
    ) {
      removeFieldEventListener(fieldsRef.current[name] as Field, true);
    }

    register(
      Object.defineProperty({ name, focus: onFocus }, VALUE, {
        set(data) {
          setInputStateValue(data);
          valueRef.current = data;
        },
        get() {
          return valueRef.current;
        },
      }),
      rules,
    );
  }, [
    fieldArrayNamesRef,
    fieldsRef,
    name,
    onFocus,
    register,
    removeFieldEventListener,
  ]);

  React.useEffect(() => {
    const fieldArrayNames = fieldArrayNamesRef.current;
    registerField();

    return () => {
      if (!isNameInFieldArray(fieldArrayNames, name)) {
        unregister(name);
      }
    };
  }, [name, unregister, fieldArrayNamesRef, registerField]);

  React.useEffect(() => {
    registerField();
  }, [registerField]);

  if (!fieldsRef.current[name]) {
    registerField();
    setInputStateValue(
      isUndefined(defaultValue)
        ? get(defaultValuesRef.current, name)
        : defaultValue,
    );
  }

  const props = {
    name,
    ...rest,
    ...(onChange
      ? { [onChangeName]: eventWrapper(onChange) }
      : { [onChangeName]: handleChange }),
    ...(onBlur || shouldReValidateOnBlur
      ? {
          [onBlurName]: (...args: any[]) => {
            if (onBlur) {
              onBlur(args);
            }

            if (shouldReValidateOnBlur) {
              triggerValidation(name);
            }
          },
        }
      : {}),
    ...{ [valueName || (isCheckboxInput ? 'checked' : VALUE)]: value },
  };

  return React.isValidElement(InnerComponent)
    ? React.cloneElement(InnerComponent, props)
    : React.createElement(InnerComponent as string, props);
};

export { Controller };


export default (value: string) => value === '';


---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Codesandbox link (Required)**
Include a codesandox will help us to investigate the issue quicker.

- [useForm](https://codesandbox.io/s/react-hook-form-useform-template-lsm61) Template
- [useFieldArray](https://codesandbox.io/s/react-hook-form-usefieldarray-template-q0jek) Template
- [Controller](https://codesandbox.io/s/react-hook-form-controller-template-yu0z5) Template

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


import isNullOrUndefined from './isNullOrUndefined';

describe('isNullOrUndefined', () => {
  it('should return true when object is null or undefined', () => {
    expect(isNullOrUndefined(null)).toBeTruthy();
    expect(isNullOrUndefined(undefined)).toBeTruthy();
  });

  it('should return false when object is neither null nor undefined', () => {
    expect(isNullOrUndefined(-1)).toBeFalsy();
    expect(isNullOrUndefined(0)).toBeFalsy();
    expect(isNullOrUndefined(1)).toBeFalsy();
    expect(isNullOrUndefined('')).toBeFalsy();
    expect(isNullOrUndefined({})).toBeFalsy();
    expect(isNullOrUndefined([])).toBeFalsy();
  });
});


// @ts-nocheck
import getFieldValue from './getFieldValue';

jest.mock('./getRadioValue', () => ({
  default: () => ({
    value: 2,
  }),
}));

jest.mock('./getMultipleSelectValue', () => ({
  default: () => 3,
}));

jest.mock('./getCheckboxValue', () => ({
  default: () => ({
    value: 'testValue',
  }),
}));

describe('getFieldValue', () => {
  it('should return correct value when type is radio', () => {
    expect(
      getFieldValue(
        {
          test: {
            ref: 'test',
          },
        },
        {
          type: 'radio',
          name: 'test',
        },
      ),
    ).toBe(2);
  });

  it('should return teh correct select value when type is select-multiple', () => {
    expect(
      getFieldValue(
        {
          test: {
            ref: {
              name: 'bill',
              value: 'test',
            },
          },
        },
        {
          type: 'select-multiple',
          name: 'bill',
        },
      ),
    ).toBe(3);
  });

  it('should return the correct value when type is checkbox', () => {
    expect(
      getFieldValue(
        {
          test: { ref: 'test' },
        },
        {
          type: 'checkbox',
          name: 'test',
        },
      ),
    ).toBe('testValue');
  });

  it('should return it value for other types', () => {
    expect(
      getFieldValue(
        {
          test: {
            ref: {
              name: 'bill',
              value: 'test',
            },
          },
        },
        {
          type: 'text',
          value: 'value',
        },
      ),
    ).toBe('value');
  });

  it('should return empty string when radio input value is not found', () => {
    expect(
      getFieldValue(
        {},
        {
          type: 'radio',
          value: 'value',
          name: 'test',
        },
      ),
    ).toEqual('');
  });

  it('should return false when checkbox input value is not found', () => {
    expect(
      getFieldValue(
        {},
        {
          type: 'checkbox',
          value: 'value',
          name: 'test',
        },
      ),
    ).toBeFalsy();
  });

  it('should return files for input type file', () => {
    expect(
      getFieldValue(
        {
          test: {
            ref: {
              files: 'files',
            },
          },
        },
        {
          type: 'file',
          files: 'files',
        },
      ),
    ).toEqual('files');
  });
});


Formulaires performants, flexibles et extensibles dotés d'une validation poussée et facile à utiliser.

[![CircleCI](https://badgen.net/circleci/github/react-hook-form/react-hook-form)](https://circleci.com/gh/react-hook-form/react-hook-form)
[![npm downloads](https://img.shields.io/npm/dm/react-hook-form.svg?style=flat-square)](https://www.npmjs.com/package/react-hook-form)
[![npm](https://img.shields.io/npm/dt/react-hook-form.svg?style=flat-square)](https://www.npmjs.com/package/react-hook-form)
![dep](https://badgen.net/david/dep/bluebill1049/react-hook-form)
[![npm](https://badgen.net/bundlephobia/minzip/react-hook-form)](https://badgen.net/bundlephobia/minzip/react-hook-form)
[![Coverage Status](https://coveralls.io/repos/github/bluebill1049/react-hook-form/badge.svg?branch=master)](https://coveralls.io/github/bluebill1049/react-hook-form?branch=master)

[![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=React+hooks+for+form+validation+without+the+hassle&url=https://github.com/bluebill1049/react-hook-form) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/react-hook-form)

🇦🇺English | 🇨🇳 简体中文 | 🇯🇵 日本語 | 🇰🇷한국어 | 🇫🇷 Français | 🇮🇹Italiano | 🇧🇷Português | 🇪🇸Español | 🇷🇺Русский

## Fonctionnalités

- Développé en privilegiant les performances et l'experience developpeur.
- Formulaires non-contrôlés.
- [Taille reduite](https://bundlephobia.com/result?p=react-hook-form@latest) sans aucune dépendance.
- Respecte les normes de validation HTML.
- Compatible avec React Native.
- Supporte [Yup](https://github.com/jquense/yup) pour de la validation via schéma.
- Supporte la validation native du navigateur.
- Possibilité de développer des formulaires rapidement grâce au [form builder](https://react-hook-form.com/form-builder).

## Installation

    $ npm install react-hook-form

## Liens

- [Motivation](https://medium.com/@bruce1049/form-validation-with-hook-in-3kb-c5414edf7d64)
- [Tutoriel vidéo](https://www.youtube.com/watch?v=-mFXqOaqgZk&t)
- [Bien demarrer](https://react-hook-form.com/get-started)
- [API](https://react-hook-form.com/api)
- [Exemples](https://github.com/bluebill1049/react-hook-form/tree/master/examples)
- [Démo](https://react-hook-form.com)
- [Form Builder](https://react-hook-form.com/form-builder)
- [FAQs](https://react-hook-form.com/faqs)

## Démarrage rapide

```jsx
import React from 'react';
import { useForm } from 'react-hook-form';

function App() {
  const { register, handleSubmit, errors } = useForm(); // initialise the hook
  const onSubmit = data => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="firstname" ref={register} /> {/* register an input */}
      <input name="lastname" ref={register({ required: true })} />
      {errors.lastname && 'Last name is required.'}
      <input name="age" ref={register({ pattern: /\d+/ })} />
      {errors.age && 'Please enter number for age.'}
      <input type="submit" />
    </form>
  );
}
```

## Contributeurs

Thanks goes to these wonderful people. [[Become a contributor](CONTRIBUTING.md)].

## Sponsors

Merci à tous nos sponsors ! [[Become a backer](https://opencollective.com/react-hook-form#backer)].


import isBoolean from '../utils/isBoolean';
import isMessage from '../utils/isMessage';
import { FieldError, ValidateResult, Ref } from '../types';

export default function getValidateError(
  result: ValidateResult,
  ref: Ref,
  type = 'validate',
): FieldError | void {
  if (isMessage(result) || (isBoolean(result) && !result)) {
    const message = isMessage(result) ? result : '';
    return {
      type,
      message,
      ref,
    };
  }
}