react-hook-form
Version:
Performant, flexible and extensible forms library for React Hooks
1,128 lines (1,075 loc) • 80 kB
JavaScript
(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) {