rsuite
Version:
A suite of react components
355 lines (346 loc) • 11.7 kB
JavaScript
'use client';
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports.default = useFormValidate;
var _react = require("react");
var _omit = _interopRequireDefault(require("lodash/omit"));
var _set = _interopRequireDefault(require("lodash/set"));
var _hooks = require("../../internals/hooks");
var _nameToPath = require("../../useFormControl/utils/nameToPath");
function useFormValidate(_formError, props) {
const {
formValue,
getCombinedModel,
onCheck,
onError,
nestedField,
resolver
} = props;
const [realFormError, setFormError] = (0, _hooks.useControlled)(_formError, {});
const checkOptions = {
nestedObject: nestedField
};
const realFormErrorRef = (0, _react.useRef)(realFormError);
realFormErrorRef.current = realFormError;
/**
* Returns true when an error value is considered non-empty (i.e. the field has an error).
*/
const isValidError = error => error !== undefined && error !== null && error !== '';
/**
* Merges resolver errors into the current form error state, removing entries that
* are no longer invalid according to the latest resolver result.
*/
const mergeResolverErrors = (current, resolverErrors) => {
const next = {
...current
};
Object.keys({
...current,
...resolverErrors
}).forEach(key => {
if (isValidError(resolverErrors[key])) {
next[key] = resolverErrors[key];
} else {
delete next[key];
}
});
return next;
};
/**
* Validate the form data and return a boolean.
* The error message after verification is returned in the callback.
*
* When a `resolver` is provided and the resolver returns a Promise (async resolver),
* this method cannot resolve the result synchronously. In that case it returns `false`
* immediately and you should use `checkAsync()` instead.
* @param callback
*/
const check = (0, _hooks.useEventCallback)(callback => {
if (resolver) {
const result = resolver(formValue || {});
// Async resolver: cannot handle synchronously
if (result instanceof Promise) {
if (process.env.NODE_ENV !== 'production') {
console.warn('[rsuite] The `resolver` provided to <Form> returns a Promise. ' + 'Use `checkAsync()` or rely on `onSubmit` for async validation.');
}
return false;
}
const {
errors
} = result;
const hasError = Object.keys(errors).length > 0;
setFormError(errors);
onCheck?.(errors);
callback?.(errors);
if (hasError) {
onError?.(errors);
}
return !hasError;
}
const formError = {};
let errorCount = 0;
const model = getCombinedModel();
const checkField = (key, type, value, formErrorObj) => {
model.setSchemaOptionsForAllType(formValue || {});
const checkResult = type.check(value, formValue, key);
if (checkResult.hasError === true) {
errorCount += 1;
formErrorObj[key] = checkResult?.errorMessage || checkResult;
}
// Check nested object
if (type?.objectTypeSchemaSpec) {
Object.entries(type.objectTypeSchemaSpec).forEach(([nestedKey, nestedType]) => {
formErrorObj[key] = formErrorObj[key] || {
object: {}
};
checkField(nestedKey, nestedType, value?.[nestedKey], formErrorObj[key].object);
});
}
};
Object.entries(model.getSchemaSpec()).forEach(([key, type]) => {
checkField(key, type, formValue[key], formError);
});
setFormError(formError);
onCheck?.(formError);
callback?.(formError);
if (errorCount > 0) {
onError?.(formError);
return false;
}
return true;
});
const checkFieldForNextValue = (0, _hooks.useEventCallback)((fieldName, nextValue, callback) => {
if (resolver) {
const result = resolver(nextValue);
if (result instanceof Promise) {
if (process.env.NODE_ENV !== 'production') {
console.warn('[rsuite] The `resolver` provided to <Form> returns a Promise. ' + 'Use `checkAsync()` or `checkForFieldAsync()` for async validation.');
}
return false;
}
const {
errors
} = result;
const fieldError = errors[fieldName];
const hasFieldError = isValidError(fieldError);
// Merge resolver errors with existing errors, clearing fields that now pass
const nextFormError = mergeResolverErrors(realFormError, errors);
setFormError(nextFormError);
onCheck?.(nextFormError);
const callbackResult = {
hasError: hasFieldError,
errorMessage: fieldError
};
callback?.(hasFieldError ? callbackResult : {
hasError: false
});
if (Object.keys(nextFormError).length > 0) {
onError?.(nextFormError);
}
return !hasFieldError;
}
const model = getCombinedModel();
const resultOfCurrentField = model.checkForField(fieldName, nextValue, checkOptions);
let nextFormError = {
...realFormError
};
/**
* when using proxy of schema-typed, we need to use getCheckResult to get all errors,
* but if nestedField is used, it is impossible to distinguish whether the nested object has an error here,
* so nestedField does not support proxy here
*/
if (nestedField) {
nextFormError = (0, _set.default)(nextFormError, (0, _nameToPath.nameToPath)(fieldName), resultOfCurrentField);
setFormError(nextFormError);
onCheck?.(nextFormError);
callback?.(resultOfCurrentField);
if (resultOfCurrentField.hasError) {
onError?.(nextFormError);
}
return !resultOfCurrentField.hasError;
} else {
const allResults = model.getCheckResult();
let hasError = false;
Object.keys(allResults).forEach(key => {
const currentResult = allResults[key];
if (currentResult.hasError) {
nextFormError[key] = currentResult.errorMessage || currentResult;
hasError = true;
} else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {
[key]: _,
...rest
} = nextFormError;
nextFormError = rest;
}
});
setFormError(nextFormError);
onCheck?.(nextFormError);
callback?.(resultOfCurrentField);
if (hasError) {
onError?.(nextFormError);
}
return !hasError;
}
});
/**
* Check the data field
* @param fieldName
* @param callback
*/
const checkForField = (0, _hooks.useEventCallback)((fieldName, callback) => {
return checkFieldForNextValue(fieldName, formValue || {}, callback);
});
/**
* Check form data asynchronously and return a Promise
*/
const checkAsync = (0, _hooks.useEventCallback)(() => {
if (resolver) {
return Promise.resolve(resolver(formValue || {})).then(({
errors
}) => {
const hasError = Object.keys(errors).length > 0;
onCheck?.(errors);
setFormError(errors);
if (hasError) {
onError?.(errors);
}
return {
hasError,
formError: errors
};
});
}
const promises = [];
const keys = [];
const model = getCombinedModel();
Object.keys(model.getSchemaSpec()).forEach(key => {
keys.push(key);
promises.push(model.checkForFieldAsync(key, formValue || {}, checkOptions));
});
return Promise.all(promises).then(values => {
const formError = {};
let errorCount = 0;
for (let i = 0; i < values.length; i++) {
if (values[i].hasError) {
errorCount += 1;
formError[keys[i]] = values[i].errorMessage;
}
}
onCheck?.(formError);
setFormError(formError);
if (errorCount > 0) {
onError?.(formError);
}
return {
hasError: errorCount > 0,
formError
};
});
});
const checkFieldAsyncForNextValue = (0, _hooks.useEventCallback)((fieldName, nextValue) => {
if (resolver) {
return Promise.resolve(resolver(nextValue)).then(({
errors
}) => {
const fieldError = errors[fieldName];
const hasFieldError = isValidError(fieldError);
const nextFormError = mergeResolverErrors(realFormError, errors);
onCheck?.(nextFormError);
setFormError(nextFormError);
if (Object.keys(nextFormError).length > 0) {
onError?.(nextFormError);
}
return {
hasError: hasFieldError,
errorMessage: fieldError
};
});
}
const model = getCombinedModel();
return model.checkForFieldAsync(fieldName, nextValue, checkOptions).then(resultOfCurrentField => {
let nextFormError = {
...realFormError
};
/**
* when using proxy of schema-typed, we need to use getCheckResult to get all errors,
* but if nestedField is used, it is impossible to distinguish whether the nested object has an error here,
* so nestedField does not support proxy here
*/
if (nestedField) {
nextFormError = (0, _set.default)(nextFormError, (0, _nameToPath.nameToPath)(fieldName), resultOfCurrentField);
onCheck?.(nextFormError);
setFormError(nextFormError);
if (resultOfCurrentField.hasError) {
onError?.(nextFormError);
}
return resultOfCurrentField;
} else {
const allResults = model.getCheckResult();
let hasError = false;
Object.keys(allResults).forEach(key => {
const currentResult = allResults[key];
if (currentResult.hasError) {
nextFormError[key] = currentResult.errorMessage || currentResult;
hasError = true;
} else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {
[key]: _,
...rest
} = nextFormError;
nextFormError = rest;
}
});
setFormError(nextFormError);
onCheck?.(nextFormError);
if (hasError) {
onError?.(nextFormError);
}
return resultOfCurrentField;
}
});
});
/**
* Asynchronously check form fields and return Promise
* @param fieldName
*/
const checkForFieldAsync = (0, _hooks.useEventCallback)(fieldName => {
return checkFieldAsyncForNextValue(fieldName, formValue || {});
});
const onRemoveError = (0, _react.useCallback)(name => {
/**
* when this function is called when the children component is unmount,
* it's an old render frame so use Ref to get future error
*/
const formError = (0, _omit.default)(realFormErrorRef.current, [nestedField ? (0, _nameToPath.nameToPath)(name) : name]);
realFormErrorRef.current = formError;
setFormError(formError);
onCheck?.(formError);
return formError;
}, [nestedField, onCheck, setFormError]);
const cleanErrors = (0, _hooks.useEventCallback)(() => {
setFormError({});
});
const resetErrors = (0, _hooks.useEventCallback)((formError = {}) => {
setFormError(formError);
});
const cleanErrorForField = (0, _hooks.useEventCallback)(fieldName => {
setFormError((0, _omit.default)(realFormError, [nestedField ? (0, _nameToPath.nameToPath)(fieldName) : fieldName]));
});
return {
formError: realFormError,
check,
checkForField,
checkFieldForNextValue,
checkAsync,
checkForFieldAsync,
checkFieldAsyncForNextValue,
cleanErrors,
resetErrors,
cleanErrorForField,
onRemoveError
};
}