UNPKG

react-hook-form

Version:

Performant, flexible and extensible forms library for React Hooks

1,128 lines (1,075 loc) • 80 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react')) : typeof define === 'function' && define.amd ? define(['exports', 'react'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReactHookForm = {}, global.React)); }(this, (function (exports, React) { 'use strict'; var isHTMLElement = (value) => value instanceof HTMLElement; const EVENTS = { BLUR: 'blur', CHANGE: 'change', INPUT: 'input', }; const VALIDATION_MODE = { onBlur: 'onBlur', onChange: 'onChange', onSubmit: 'onSubmit', onTouched: 'onTouched', all: 'all', }; const VALUE = 'value'; const SELECT = 'select'; const UNDEFINED = 'undefined'; const INPUT_VALIDATION_RULES = { max: 'max', min: 'min', maxLength: 'maxLength', minLength: 'minLength', pattern: 'pattern', required: 'required', validate: 'validate', }; function attachEventListeners({ ref }, shouldAttachChangeEvent, handleChange) { if (isHTMLElement(ref) && handleChange) { ref.addEventListener(shouldAttachChangeEvent ? EVENTS.CHANGE : EVENTS.INPUT, handleChange); ref.addEventListener(EVENTS.BLUR, handleChange); } } var isNullOrUndefined = (value) => value == null; var isArray = (value) => Array.isArray(value); const isObjectType = (value) => typeof value === 'object'; var isObject = (value) => !isNullOrUndefined(value) && !isArray(value) && isObjectType(value) && !(value instanceof Date); var isKey = (value) => !isArray(value) && (/^\w*$/.test(value) || !/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/.test(value)); var stringToPath = (input) => { const result = []; input.replace(/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g, (match, mathNumber, mathQuote, originalString) => { result.push(mathQuote ? originalString.replace(/\\(\\)?/g, '$1') : mathNumber || match); }); return result; }; function set(object, path, value) { let index = -1; const tempPath = isKey(path) ? [path] : stringToPath(path); const length = tempPath.length; const lastIndex = length - 1; while (++index < length) { const key = tempPath[index]; let newValue = value; if (index !== lastIndex) { const objValue = object[key]; newValue = isObject(objValue) || isArray(objValue) ? objValue : !isNaN(+tempPath[index + 1]) ? [] : {}; } object[key] = newValue; object = object[key]; } return object; } var transformToNestObject = (data) => Object.entries(data).reduce((previous, [key, value]) => { if (!isKey(key)) { set(previous, key, value); return previous; } return Object.assign(Object.assign({}, previous), { [key]: value }); }, {}); var isUndefined = (val) => val === undefined; var filterOutFalsy = (value) => value.filter(Boolean); var get = (obj, path, defaultValue) => { const result = filterOutFalsy(path.split(/[,[\].]+?/)).reduce((result, key) => (isNullOrUndefined(result) ? result : result[key]), obj); return isUndefined(result) || result === obj ? isUndefined(obj[path]) ? defaultValue : obj[path] : result; }; var focusOnErrorField = (fields, fieldErrors) => { for (const key in fields) { if (get(fieldErrors, key)) { const field = fields[key]; if (field) { if (field.ref.focus) { field.ref.focus(); break; } else if (field.options) { field.options[0].ref.focus(); break; } } } } }; var removeAllEventListeners = (ref, validateWithStateUpdate) => { if (isHTMLElement(ref) && ref.removeEventListener) { ref.removeEventListener(EVENTS.INPUT, validateWithStateUpdate); ref.removeEventListener(EVENTS.CHANGE, validateWithStateUpdate); ref.removeEventListener(EVENTS.BLUR, validateWithStateUpdate); } }; const defaultReturn = { isValid: false, value: '', }; var getRadioValue = (options) => isArray(options) ? options.reduce((previous, option) => option && option.ref.checked ? { isValid: true, value: option.ref.value, } : previous, defaultReturn) : defaultReturn; var getMultipleSelectValue = (options) => [...options] .filter(({ selected }) => selected) .map(({ value }) => value); var isRadioInput = (element) => element.type === 'radio'; var isFileInput = (element) => element.type === 'file'; var isCheckBoxInput = (element) => element.type === 'checkbox'; var isMultipleSelect = (element) => element.type === `${SELECT}-multiple`; const defaultResult = { value: false, isValid: false, }; const validResult = { value: true, isValid: true }; var getCheckboxValue = (options) => { if (isArray(options)) { if (options.length > 1) { const values = options .filter((option) => option && option.ref.checked) .map(({ ref: { value } }) => value); return { value: values, isValid: !!values.length }; } const { checked, value, attributes } = options[0].ref; return checked ? attributes && !isUndefined(attributes.value) ? isUndefined(value) || value === '' ? validResult : { value: value, isValid: true } : validResult : defaultResult; } return defaultResult; }; function getFieldValue(fieldsRef, name, unmountFieldsStateRef) { const field = fieldsRef.current[name]; if (field) { const { ref: { value, disabled }, ref, } = field; if (disabled) { return; } if (isFileInput(ref)) { return ref.files; } if (isRadioInput(ref)) { return getRadioValue(field.options).value; } if (isMultipleSelect(ref)) { return getMultipleSelectValue(ref.options); } if (isCheckBoxInput(ref)) { return getCheckboxValue(field.options).value; } return value; } if (unmountFieldsStateRef) { return get(unmountFieldsStateRef.current, name); } } function isDetached(element) { if (!element) { return true; } if (!(element instanceof HTMLElement) || element.nodeType === Node.DOCUMENT_NODE) { return false; } return isDetached(element.parentNode); } var isEmptyObject = (value) => isObject(value) && !Object.keys(value).length; var isBoolean = (value) => typeof value === 'boolean'; function baseGet(object, updatePath) { const path = updatePath.slice(0, -1); const length = path.length; let index = 0; while (index < length) { object = isUndefined(object) ? index++ : object[updatePath[index++]]; } return object; } function unset(object, path) { const updatePath = isKey(path) ? [path] : stringToPath(path); const childObject = updatePath.length == 1 ? object : baseGet(object, updatePath); const key = updatePath[updatePath.length - 1]; let previousObjRef = undefined; if (childObject) { delete childObject[key]; } for (let k = 0; k < updatePath.slice(0, -1).length; k++) { let index = -1; let objectRef = undefined; const currentPaths = updatePath.slice(0, -(k + 1)); const currentPathsLength = currentPaths.length - 1; if (k > 0) { previousObjRef = object; } while (++index < currentPaths.length) { const item = currentPaths[index]; objectRef = objectRef ? objectRef[item] : object[item]; if (currentPathsLength === index && ((isObject(objectRef) && isEmptyObject(objectRef)) || (isArray(objectRef) && !objectRef.filter((data) => (isObject(data) && !isEmptyObject(data)) || isBoolean(data)).length))) { previousObjRef ? delete previousObjRef[item] : delete object[item]; } previousObjRef = objectRef; } } return object; } const isSameRef = (fieldValue, ref) => fieldValue && fieldValue.ref === ref; function findRemovedFieldAndRemoveListener(fieldsRef, handleChange, field, unmountFieldsStateRef, shouldUnregister, forceDelete) { const { ref, ref: { name, type }, } = field; const fieldRef = fieldsRef.current[name]; if (!shouldUnregister) { const value = getFieldValue(fieldsRef, name, unmountFieldsStateRef); if (!isUndefined(value)) { set(unmountFieldsStateRef.current, name, value); } } if (!type) { delete fieldsRef.current[name]; return; } if ((isRadioInput(ref) || isCheckBoxInput(ref)) && fieldRef) { const { options } = fieldRef; if (isArray(options) && options.length) { filterOutFalsy(options).forEach((option, index) => { const { ref } = option; if ((ref && isDetached(ref) && isSameRef(option, ref)) || forceDelete) { removeAllEventListeners(ref, handleChange); unset(options, `[${index}]`); } }); if (options && !filterOutFalsy(options).length) { delete fieldsRef.current[name]; } } else { delete fieldsRef.current[name]; } } else if ((isDetached(ref) && isSameRef(fieldRef, ref)) || forceDelete) { removeAllEventListeners(ref, handleChange); delete fieldsRef.current[name]; } } var isString = (value) => typeof value === 'string'; function deepMerge(target, source) { if (!isObject(target) || !isObject(source)) { return source; } for (const key in source) { const targetValue = target[key]; const sourceValue = source[key]; try { if (isObject(targetValue) && isObject(sourceValue)) { target[key] = deepMerge(targetValue, sourceValue); } else { target[key] = sourceValue; } } catch (_a) { } } return target; } var getFieldsValues = (fieldsRef, unmountFieldsStateRef, search) => { const output = {}; for (const name in fieldsRef.current) { if (isUndefined(search) || (isString(search) ? name.startsWith(search) : isArray(search) && search.find((data) => name.startsWith(data)))) { output[name] = getFieldValue(fieldsRef, name); } } return deepMerge(Object.assign({}, ((unmountFieldsStateRef || {}).current || {})), transformToNestObject(output)); }; var isSameError = (error, { type, types = {}, message }) => isObject(error) && error.type === type && error.message === message && Object.keys(error.types || {}).length === Object.keys(types).length && Object.entries(error.types || {}).every(([key, value]) => types[key] === value); function shouldRenderBasedOnError({ errors, name, error, validFields, fieldsWithValidation, }) { const isFieldValid = isEmptyObject(error); const isFormValid = isEmptyObject(errors); const currentFieldError = get(error, name); const existFieldError = get(errors, name); if (isFieldValid && get(validFields, name)) { return false; } if (isFormValid !== isFieldValid || (!isFormValid && !existFieldError) || (isFieldValid && get(fieldsWithValidation, name) && !get(validFields, name))) { return true; } return currentFieldError && !isSameError(existFieldError, currentFieldError); } var isRegex = (value) => value instanceof RegExp; const isValueMessage = (value) => isObject(value) && !isRegex(value); var getValueAndMessage = (validationData) => isValueMessage(validationData) ? validationData : { value: validationData, message: '', }; var isFunction = (value) => typeof value === 'function'; var isMessage = (value) => isString(value) || (isObject(value) && React.isValidElement(value)); function getValidateError(result, ref, type = 'validate') { if (isMessage(result) || (isBoolean(result) && !result)) { return { type, message: isMessage(result) ? result : '', ref, }; } } var appendErrors = (name, validateAllFieldCriteria, errors, type, message) => { if (validateAllFieldCriteria) { const error = errors[name]; return Object.assign(Object.assign({}, error), { types: Object.assign(Object.assign({}, (error && error.types ? error.types : {})), { [type]: message || true }) }); } return {}; }; var validateField = async (fieldsRef, validateAllFieldCriteria, { ref, ref: { type, value }, options, required, maxLength, minLength, min, max, pattern, validate, }, unmountFieldsStateRef) => { const fields = fieldsRef.current; const name = ref.name; const error = {}; const isRadio = isRadioInput(ref); const isCheckBox = isCheckBoxInput(ref); const isRadioOrCheckbox = isRadio || isCheckBox; const isEmpty = value === ''; const appendErrorsCurry = appendErrors.bind(null, name, validateAllFieldCriteria, error); const getMinMaxMessage = (exceedMax, maxLengthMessage, minLengthMessage, maxType = INPUT_VALIDATION_RULES.maxLength, minType = INPUT_VALIDATION_RULES.minLength) => { const message = exceedMax ? maxLengthMessage : minLengthMessage; error[name] = Object.assign({ type: exceedMax ? maxType : minType, message, ref }, (exceedMax ? appendErrorsCurry(maxType, message) : appendErrorsCurry(minType, message))); }; if (required && ((!isRadio && !isCheckBox && (isEmpty || isNullOrUndefined(value))) || (isBoolean(value) && !value) || (isCheckBox && !getCheckboxValue(options).isValid) || (isRadio && !getRadioValue(options).isValid))) { const { value: requiredValue, message: requiredMessage } = isMessage(required) ? { value: !!required, message: required } : getValueAndMessage(required); if (requiredValue) { error[name] = Object.assign({ type: INPUT_VALIDATION_RULES.required, message: requiredMessage, ref: isRadioOrCheckbox ? (fields[name].options || [])[0].ref : ref }, appendErrorsCurry(INPUT_VALIDATION_RULES.required, requiredMessage)); if (!validateAllFieldCriteria) { return error; } } } if (!isNullOrUndefined(min) || !isNullOrUndefined(max)) { let exceedMax; let exceedMin; const { value: maxValue, message: maxMessage } = getValueAndMessage(max); const { value: minValue, message: minMessage } = getValueAndMessage(min); if (type === 'number' || (!type && !isNaN(value))) { const valueNumber = ref.valueAsNumber || parseFloat(value); if (!isNullOrUndefined(maxValue)) { exceedMax = valueNumber > maxValue; } if (!isNullOrUndefined(minValue)) { exceedMin = valueNumber < minValue; } } else { const valueDate = ref.valueAsDate || new Date(value); if (isString(maxValue)) { exceedMax = valueDate > new Date(maxValue); } if (isString(minValue)) { exceedMin = valueDate < new Date(minValue); } } if (exceedMax || exceedMin) { getMinMaxMessage(!!exceedMax, maxMessage, minMessage, INPUT_VALIDATION_RULES.max, INPUT_VALIDATION_RULES.min); if (!validateAllFieldCriteria) { return error; } } } if (isString(value) && !isEmpty && (maxLength || minLength)) { const { value: maxLengthValue, message: maxLengthMessage, } = getValueAndMessage(maxLength); const { value: minLengthValue, message: minLengthMessage, } = getValueAndMessage(minLength); const inputLength = value.toString().length; const exceedMax = !isNullOrUndefined(maxLengthValue) && inputLength > maxLengthValue; const exceedMin = !isNullOrUndefined(minLengthValue) && inputLength < minLengthValue; if (exceedMax || exceedMin) { getMinMaxMessage(!!exceedMax, maxLengthMessage, minLengthMessage); if (!validateAllFieldCriteria) { return error; } } } if (pattern && !isEmpty) { const { value: patternValue, message: patternMessage } = getValueAndMessage(pattern); if (isRegex(patternValue) && !patternValue.test(value)) { error[name] = Object.assign({ type: INPUT_VALIDATION_RULES.pattern, message: patternMessage, ref }, appendErrorsCurry(INPUT_VALIDATION_RULES.pattern, patternMessage)); if (!validateAllFieldCriteria) { return error; } } } if (validate) { const fieldValue = getFieldValue(fieldsRef, name, unmountFieldsStateRef); const validateRef = isRadioOrCheckbox && options ? options[0].ref : ref; if (isFunction(validate)) { const result = await validate(fieldValue); const validateError = getValidateError(result, validateRef); if (validateError) { error[name] = Object.assign(Object.assign({}, validateError), appendErrorsCurry(INPUT_VALIDATION_RULES.validate, validateError.message)); if (!validateAllFieldCriteria) { return error; } } } else if (isObject(validate)) { let validationResult = {}; for (const [key, validateFunction] of Object.entries(validate)) { if (!isEmptyObject(validationResult) && !validateAllFieldCriteria) { break; } const validateResult = await validateFunction(fieldValue); const validateError = getValidateError(validateResult, validateRef, key); if (validateError) { validationResult = Object.assign(Object.assign({}, validateError), appendErrorsCurry(key, validateError.message)); if (validateAllFieldCriteria) { error[name] = validationResult; } } } if (!isEmptyObject(validationResult)) { error[name] = Object.assign({ ref: validateRef }, validationResult); if (!validateAllFieldCriteria) { return error; } } } } return error; }; var isPrimitive = (value) => isNullOrUndefined(value) || !isObjectType(value); const getPath = (path, values) => { const getInnerPath = (value, key, isObject) => { const pathWithIndex = isObject ? `${path}.${key}` : `${path}[${key}]`; return isPrimitive(value) ? pathWithIndex : getPath(pathWithIndex, value); }; return Object.entries(values) .map(([key, value]) => getInnerPath(value, key, isObject(values))) .flat(Infinity); }; var assignWatchFields = (fieldValues, fieldName, watchFields, inputValue, isSingleField) => { let value; watchFields.add(fieldName); if (isEmptyObject(fieldValues)) { value = undefined; } else { value = get(fieldValues, fieldName); if (isObject(value) || isArray(value)) { getPath(fieldName, value).forEach((name) => watchFields.add(name)); } } return isUndefined(value) ? isSingleField ? inputValue : get(inputValue, fieldName) : value; }; var skipValidation = ({ isOnBlur, isOnChange, isOnTouch, isTouched, isReValidateOnBlur, isReValidateOnChange, isBlurEvent, isSubmitted, isOnAll, }) => { if (isOnAll) { return false; } else if (!isSubmitted && isOnTouch) { return !(isTouched || isBlurEvent); } else if (isSubmitted ? isReValidateOnBlur : isOnBlur) { return !isBlurEvent; } else if (isSubmitted ? isReValidateOnChange : isOnChange) { return isBlurEvent; } return true; }; var getFieldArrayParentName = (name) => name.substring(0, name.indexOf('[')); function deepEqual(object1 = [], object2 = []) { const keys1 = Object.keys(object1); const keys2 = Object.keys(object2); if (keys1.length !== keys2.length) { return false; } for (const key of keys1) { const val1 = object1[key]; const val2 = object2[key]; if ((isObject(val1) || isArray(val1)) && (isObject(val2) || isArray(val2)) ? !deepEqual(val1, val2) : val1 !== val2) { return false; } } return true; } const isMatchFieldArrayName = (name, searchName) => RegExp(`^${searchName}[\\d+]`.replace(/\[/g, '\\[').replace(/\]/g, '\\]')).test(name); var isNameInFieldArray = (names, name) => [...names].some((current) => isMatchFieldArrayName(name, current)); var isSelectInput = (element) => element.type === `${SELECT}-one`; function onDomRemove(fieldsRef, removeFieldEventListenerAndRef) { const observer = new MutationObserver(() => { for (const field of Object.values(fieldsRef.current)) { if (field && field.options) { for (const option of field.options) { if (option && option.ref && isDetached(option.ref)) { removeFieldEventListenerAndRef(field); } } } else if (field && isDetached(field.ref)) { removeFieldEventListenerAndRef(field); } } }); observer.observe(window.document, { childList: true, subtree: true, }); return observer; } var modeChecker = (mode) => ({ isOnSubmit: !mode || mode === VALIDATION_MODE.onSubmit, isOnBlur: mode === VALIDATION_MODE.onBlur, isOnChange: mode === VALIDATION_MODE.onChange, isOnAll: mode === VALIDATION_MODE.all, isOnTouch: mode === VALIDATION_MODE.onTouched, }); var isRadioOrCheckboxFunction = (ref) => isRadioInput(ref) || isCheckBoxInput(ref); const isWindowUndefined = typeof window === UNDEFINED; const isWeb = typeof document !== UNDEFINED && !isWindowUndefined && !isUndefined(window.HTMLElement); const isProxyEnabled = isWeb ? 'Proxy' in window : typeof Proxy !== UNDEFINED; function useForm({ mode = VALIDATION_MODE.onSubmit, reValidateMode = VALIDATION_MODE.onChange, resolver, context, defaultValues = {}, shouldFocusError = true, shouldUnregister = true, criteriaMode, } = {}) { const fieldsRef = React.useRef({}); const fieldArrayDefaultValues = React.useRef({}); const watchFieldsRef = React.useRef(new Set()); const watchFieldsHookRef = React.useRef({}); const watchFieldsHookRenderRef = React.useRef({}); const fieldsWithValidationRef = React.useRef({}); const validFieldsRef = React.useRef({}); const defaultValuesRef = React.useRef(defaultValues); const defaultValuesAtRenderRef = React.useRef({}); const isUnMount = React.useRef(false); const isWatchAllRef = React.useRef(false); const handleChangeRef = React.useRef(); const unmountFieldsStateRef = React.useRef({}); const resetFieldArrayFunctionRef = React.useRef({}); const contextRef = React.useRef(context); const resolverRef = React.useRef(resolver); const fieldArrayNamesRef = React.useRef(new Set()); const modeRef = React.useRef(modeChecker(mode)); const { current: { isOnSubmit, isOnTouch }, } = modeRef; const isValidateAllFieldCriteria = criteriaMode === VALIDATION_MODE.all; const [formState, setFormState] = React.useState({ isDirty: false, dirtyFields: {}, isSubmitted: false, submitCount: 0, touched: {}, isSubmitting: false, isValid: !isOnSubmit, errors: {}, }); const readFormStateRef = React.useRef({ isDirty: !isProxyEnabled, dirtyFields: !isProxyEnabled, isSubmitted: isOnSubmit, submitCount: !isProxyEnabled, touched: !isProxyEnabled || isOnTouch, isSubmitting: !isProxyEnabled, isValid: !isProxyEnabled, errors: !isProxyEnabled, }); const formStateRef = React.useRef(formState); const observerRef = React.useRef(); const { current: { isOnBlur: isReValidateOnBlur, isOnChange: isReValidateOnChange }, } = React.useRef(modeChecker(reValidateMode)); contextRef.current = context; resolverRef.current = resolver; formStateRef.current = formState; const updateFormState = React.useCallback((state = {}) => !isUnMount.current && setFormState(Object.assign(Object.assign({}, formStateRef.current), state)), []); const shouldRenderBaseOnError = React.useCallback((name, error, shouldRender = false, state = {}, isValid) => { let shouldReRender = shouldRender || shouldRenderBasedOnError({ errors: formStateRef.current.errors, error, name, validFields: validFieldsRef.current, fieldsWithValidation: fieldsWithValidationRef.current, }); const previousError = get(formStateRef.current.errors, name); if (isEmptyObject(error)) { if (get(fieldsWithValidationRef.current, name) || resolverRef.current) { set(validFieldsRef.current, name, true); shouldReRender = shouldReRender || previousError; } unset(formStateRef.current.errors, name); } else { unset(validFieldsRef.current, name); shouldReRender = shouldReRender || !previousError || !isSameError(previousError, error[name]); set(formStateRef.current.errors, name, error[name]); } if (shouldReRender || !isEmptyObject(state)) { updateFormState(Object.assign(Object.assign(Object.assign({}, state), { errors: formStateRef.current.errors }), (resolverRef.current ? { isValid: !!isValid } : {}))); } }, []); const setFieldValue = React.useCallback(({ ref, options }, rawValue) => { const value = isWeb && isHTMLElement(ref) && isNullOrUndefined(rawValue) ? '' : rawValue; if (isRadioInput(ref) && options) { options.forEach(({ ref: radioRef }) => (radioRef.checked = radioRef.value === value)); } else if (isFileInput(ref) && !isString(value)) { ref.files = value; } else if (isMultipleSelect(ref)) { [...ref.options].forEach((selectRef) => (selectRef.selected = value.includes(selectRef.value))); } else if (isCheckBoxInput(ref) && options) { options.length > 1 ? options.forEach(({ ref: checkboxRef }) => (checkboxRef.checked = String(value).includes(checkboxRef.value))) : (options[0].ref.checked = !!value); } else { ref.value = value; } }, []); const updateAndGetDirtyState = React.useCallback((name, shouldRender = true) => { if (!fieldsRef.current[name] || (!readFormStateRef.current.isDirty && !readFormStateRef.current.dirtyFields)) { return {}; } const isFieldDirty = defaultValuesAtRenderRef.current[name] !== getFieldValue(fieldsRef, name, unmountFieldsStateRef); const isDirtyFieldExist = get(formStateRef.current.dirtyFields, name); const isFieldArray = isNameInFieldArray(fieldArrayNamesRef.current, name); const previousIsDirty = formStateRef.current.isDirty; isFieldDirty ? set(formStateRef.current.dirtyFields, name, true) : unset(formStateRef.current.dirtyFields, name); const state = { isDirty: (isFieldArray && !deepEqual(get(getValues(), getFieldArrayParentName(name)), get(defaultValuesRef.current, getFieldArrayParentName(name)))) || !isEmptyObject(formStateRef.current.dirtyFields), dirtyFields: formStateRef.current.dirtyFields, }; const isChanged = (readFormStateRef.current.isDirty && previousIsDirty !== state.isDirty) || (readFormStateRef.current.dirtyFields && isDirtyFieldExist !== get(formStateRef.current.dirtyFields, name)); if (isChanged && shouldRender) { formStateRef.current = Object.assign(Object.assign({}, formStateRef.current), state); updateFormState(Object.assign({}, state)); } return isChanged ? state : {}; }, []); const executeValidation = React.useCallback(async (name, skipReRender) => { if (fieldsRef.current[name]) { const error = await validateField(fieldsRef, isValidateAllFieldCriteria, fieldsRef.current[name], unmountFieldsStateRef); shouldRenderBaseOnError(name, error, skipReRender); return isEmptyObject(error); } return false; }, [shouldRenderBaseOnError, isValidateAllFieldCriteria]); const executeSchemaOrResolverValidation = React.useCallback(async (payload) => { const { errors } = await resolverRef.current(getValues(), contextRef.current, isValidateAllFieldCriteria); const previousFormIsValid = formStateRef.current.isValid; if (isArray(payload)) { const isInputsValid = payload .map((name) => { const error = get(errors, name); error ? set(formState.errors, name, error) : unset(formState.errors, name); return !error; }) .every(Boolean); updateFormState({ isValid: isEmptyObject(errors), errors: formState.errors, }); return isInputsValid; } else { const error = get(errors, payload); shouldRenderBaseOnError(payload, (error ? { [payload]: error } : {}), previousFormIsValid !== isEmptyObject(errors), {}, isEmptyObject(errors)); return !error; } }, [shouldRenderBaseOnError, isValidateAllFieldCriteria]); const trigger = React.useCallback(async (name) => { const fields = name || Object.keys(fieldsRef.current); if (resolverRef.current) { return executeSchemaOrResolverValidation(fields); } if (isArray(fields)) { const result = await Promise.all(fields.map(async (data) => await executeValidation(data, true))); return result.every(Boolean); } return await executeValidation(fields); }, [executeSchemaOrResolverValidation, executeValidation]); const setInternalValues = React.useCallback((name, value, { shouldDirty, shouldValidate }) => { getPath(name, value).forEach((fieldName) => { const data = {}; const field = fieldsRef.current[fieldName]; if (field) { set(data, name, value); setFieldValue(field, get(data, fieldName)); if (shouldDirty) { updateAndGetDirtyState(fieldName); } if (shouldValidate) { trigger(fieldName); } } }); }, [trigger, setFieldValue, updateAndGetDirtyState]); const setInternalValue = React.useCallback((name, value, config) => { if (fieldsRef.current[name]) { setFieldValue(fieldsRef.current[name], value); config.shouldDirty && updateAndGetDirtyState(name); } else if (!isPrimitive(value)) { setInternalValues(name, value, config); } set(unmountFieldsStateRef.current, name, value); }, [updateAndGetDirtyState, setFieldValue, setInternalValues]); const isFieldWatched = (name) => isWatchAllRef.current || watchFieldsRef.current.has(name) || watchFieldsRef.current.has((name.match(/\w+/) || [])[0]); const renderWatchedInputs = (name, found = true) => { if (!isEmptyObject(watchFieldsHookRef.current)) { for (const key in watchFieldsHookRef.current) { if (!name || watchFieldsHookRef.current[key].has(name) || watchFieldsHookRef.current[key].has(getFieldArrayParentName(name)) || !watchFieldsHookRef.current[key].size) { watchFieldsHookRenderRef.current[key](); found = false; } } } return found; }; function setValue(name, value, config = {}) { setInternalValue(name, value, config); if (isFieldWatched(name)) { updateFormState(); } renderWatchedInputs(name); if (config.shouldValidate) { trigger(name); } } handleChangeRef.current = handleChangeRef.current ? handleChangeRef.current : async ({ type, target }) => { const name = target.name; const field = fieldsRef.current[name]; let error; let isValid; if (field) { const isBlurEvent = type === EVENTS.BLUR; const shouldSkipValidation = skipValidation(Object.assign({ isBlurEvent, isReValidateOnChange, isReValidateOnBlur, isTouched: !!get(formStateRef.current.touched, name), isSubmitted: formStateRef.current.isSubmitted }, modeRef.current)); let state = updateAndGetDirtyState(name, false); let shouldRender = !isEmptyObject(state) || isFieldWatched(name); if (isBlurEvent && !get(formStateRef.current.touched, name) && readFormStateRef.current.touched) { set(formStateRef.current.touched, name, true); state = Object.assign(Object.assign({}, state), { touched: formStateRef.current.touched }); } if (shouldSkipValidation) { renderWatchedInputs(name); return ((!isEmptyObject(state) || (shouldRender && isEmptyObject(state))) && updateFormState(state)); } if (resolverRef.current) { const { errors } = await resolverRef.current(getValues(), contextRef.current, isValidateAllFieldCriteria); const previousFormIsValid = formStateRef.current.isValid; error = (get(errors, name) ? { [name]: get(errors, name) } : {}); isValid = isEmptyObject(errors); if (previousFormIsValid !== isValid) { shouldRender = true; } } else { error = await validateField(fieldsRef, isValidateAllFieldCriteria, field, unmountFieldsStateRef); } renderWatchedInputs(name); shouldRenderBaseOnError(name, error, shouldRender, state, isValid); } }; function getValues(payload) { if (isString(payload)) { return getFieldValue(fieldsRef, payload, unmountFieldsStateRef); } if (isArray(payload)) { const data = {}; for (const name of payload) { set(data, name, getFieldValue(fieldsRef, name, unmountFieldsStateRef)); } return data; } return getFieldsValues(fieldsRef, unmountFieldsStateRef); } const validateResolver = React.useCallback(async (values = {}) => { const { errors } = await resolverRef.current(Object.assign(Object.assign(Object.assign({}, defaultValuesRef.current), getValues()), values), contextRef.current, isValidateAllFieldCriteria); const previousFormIsValid = formStateRef.current.isValid; const isValid = isEmptyObject(errors); if (previousFormIsValid !== isValid) { updateFormState({ isValid, }); } }, [isValidateAllFieldCriteria]); const removeFieldEventListener = React.useCallback((field, forceDelete) => findRemovedFieldAndRemoveListener(fieldsRef, handleChangeRef.current, field, unmountFieldsStateRef, shouldUnregister, forceDelete), [shouldUnregister]); const removeFieldEventListenerAndRef = React.useCallback((field, forceDelete) => { if (field) { removeFieldEventListener(field, forceDelete); if (shouldUnregister) { unset(validFieldsRef.current, field.ref.name); unset(fieldsWithValidationRef.current, field.ref.name); unset(defaultValuesAtRenderRef.current, field.ref.name); unset(formState.errors, field.ref.name); unset(formStateRef.current.dirtyFields, field.ref.name); unset(formStateRef.current.touched, field.ref.name); updateFormState({ errors: formState.errors, isDirty: !isEmptyObject(formStateRef.current.dirtyFields), dirtyFields: formStateRef.current.dirtyFields, touched: formStateRef.current.touched, }); resolverRef.current && validateResolver(); } } }, [validateResolver, removeFieldEventListener]); function clearErrors(name) { name && (isArray(name) ? name : [name]).forEach((inputName) => unset(formState.errors, inputName)); updateFormState({ errors: name ? formState.errors : {}, }); } function setError(name, error) { set(formState.errors, name, Object.assign(Object.assign({}, error), { ref: (fieldsRef.current[name] || {}).ref })); updateFormState({ isValid: false, errors: formState.errors, }); } const watchInternal = React.useCallback((fieldNames, defaultValue, watchId) => { const watchFields = watchId ? watchFieldsHookRef.current[watchId] : watchFieldsRef.current; const combinedDefaultValues = isUndefined(defaultValue) ? defaultValuesRef.current : defaultValue; const fieldValues = getFieldsValues(fieldsRef, unmountFieldsStateRef, fieldNames); if (isString(fieldNames)) { return assignWatchFields(fieldValues, fieldNames, watchFields, isUndefined(defaultValue) ? get(combinedDefaultValues, fieldNames) : defaultValue, true); } if (isArray(fieldNames)) { return fieldNames.reduce((previous, name) => (Object.assign(Object.assign({}, previous), { [name]: assignWatchFields(fieldValues, name, watchFields, combinedDefaultValues) })), {}); } if (isUndefined(watchId)) { isWatchAllRef.current = true; } return transformToNestObject((!isEmptyObject(fieldValues) && fieldValues) || combinedDefaultValues); }, []); function watch(fieldNames, defaultValue) { return watchInternal(fieldNames, defaultValue); } function unregister(name) { (isArray(name) ? name : [name]).forEach((fieldName) => removeFieldEventListenerAndRef(fieldsRef.current[fieldName], true)); } function registerFieldRef(ref, validateOptions = {}) { { if (!ref.name) { return console.warn('📋 Field is missing `name` attribute', ref, `https://react-hook-form.com/api#useForm`); } if (fieldArrayNamesRef.current.has(ref.name.split(/\[\d+\]$/)[0]) && !RegExp(`^${ref.name.split(/\[\d+\]$/)[0]}[\\d+].\\w+` .replace(/\[/g, '\\[') .replace(/\]/g, '\\]')).test(ref.name)) { return console.warn('📋 `name` prop should be in object shape: name="test[index].name"', ref, 'https://react-hook-form.com/api#useFieldArray'); } } const { name, type, value } = ref; const fieldRefAndValidationOptions = Object.assign({ ref }, validateOptions); const fields = fieldsRef.current; const isRadioOrCheckbox = isRadioOrCheckboxFunction(ref); const compareRef = (currentRef) => isWeb && (!isHTMLElement(ref) || currentRef === ref); let field = fields[name]; let isEmptyDefaultValue = true; let isFieldArray; let defaultValue; if (field && (isRadioOrCheckbox ? isArray(field.options) && filterOutFalsy(field.options).find((option) => { return value === option.ref.value && compareRef(option.ref); }) : compareRef(field.ref))) { fields[name] = Object.assign(Object.assign({}, field), validateOptions); return; } if (type) { field = isRadioOrCheckbox ? Object.assign({ options: [ ...filterOutFalsy((field && field.options) || []), { ref, }, ], ref: { type, name } }, validateOptions) : Object.assign({}, fieldRefAndValidationOptions); } else { field = fieldRefAndValidationOptions; } fields[name] = field; const isEmptyUnmountFields = isUndefined(get(unmountFieldsStateRef.current, name)); if (!isEmptyObject(defaultValuesRef.current) || !isEmptyUnmountFields) { defaultValue = get(isEmptyUnmountFields ? defaultValuesRef.current : unmountFieldsStateRef.current, name); isEmptyDefaultValue = isUndefined(defaultValue); isFieldArray = isNameInFieldArray(fieldArrayNamesRef.current, name); if (!isEmptyDefaultValue && !isFieldArray) { setFieldValue(field, defaultValue); } } if (resolver && !isFieldArray && readFormStateRef.current.isValid) { validateResolver(); } else if (!isEmptyObject(validateOptions)) { set(fieldsWithValidationRef.current, name, true); if (!isOnSubmit && readFormStateRef.current.isValid) { validateField(fieldsRef, isValidateAllFieldCriteria, field, unmountFieldsStateRef).then((error) => { const previousFormIsValid = formStateRef.current.isValid; isEmptyObject(error) ? set(validFieldsRef.current, name, true) : unset(validFieldsRef.current, name); if (previousFormIsValid !== isEmptyObject(error)) { updateFormState(); } }); } } if (!defaultValuesAtRenderRef.current[name] && !(isFieldArray && isEmptyDefaultValue)) { const fieldValue = getFieldValue(fieldsRef, name, unmountFieldsStateRef); defaultValuesAtRenderRef.current[name] = isEmptyDefaultValue ? isObject(fieldValue) ? Object.assign({}, fieldValue) : fieldValue : defaultValue; } if (type) { attachEventListeners(isRadioOrCheckbox && field.options ? field.options[field.options.length - 1] : field, isRadioOrCheckbox || isSelectInput(ref), handleChangeRef.current); } } function register(refOrValidationOptions, rules) { if (!isWindowUndefined) { if (isString(refOrValidationOptions)) { registerFieldRef({ name: refOrValidationOptions }, rules); } else if (isObject(refOrValidationOptions) && 'name' in refOrValidationOptions) { registerFieldRef(refOrValidationOptions, rules); } else { return (ref) => ref && registerFieldRef(ref, refOrValidationOptions); } } } const handleSubmit = React.useCallback((onValid, onInvalid) => async (e) => { if (e && e.preventDefault) { e.preventDefault(); e.persist(); } let fieldErrors = {}; let fieldValues = getFieldsValues(fieldsRef, unmountFieldsStateRef); if (readFormStateRef.current.isSubmitting) { updateFormState({ isSubmitting: true, }); } try { if (resolverRef.current) { const { errors, values } = await resolverRef.current(fieldValues, contextRef.current, isValidateAllFieldCriteria); formState.errors = errors; fieldErrors = errors; fieldValues = values; } else { for (const field of Object.values(fieldsRef.current)) { if (field) { const { ref: { name }, } = field; const fieldError = await validateField(fieldsRef, isValidateAllFieldCriteria, field, unmountFieldsStateRef); if (fieldError[name]) { set(fieldErrors, name, fieldError[name]); unset(validFieldsRef.current, name); } else if (get(fieldsWithValidationRef.current, name)) { unset(formState.errors, name); set(validFieldsRef.current, name, true); } } } } if (isEmptyObject(fieldErrors) && Object.keys(formState.errors).every((name) => Object.keys(fieldsRef.current).includes(name))) { updateFormState({ errors: {}, }); await onValid(fieldValues, e); } else { formState.errors = Object.assign(Object.assign({}, formState.errors), fieldErrors); if (onInvalid) {