UNPKG

rsuite

Version:

A suite of react components

355 lines (346 loc) 11.7 kB
'use client'; "use strict"; 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 }; }