@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
468 lines (467 loc) • 16.4 kB
JavaScript
"use client";
import { useRef, useCallback, useMemo } from 'react';
import { FormError, isZodSchema, createZodValidator, zodErrorsToOneFormError } from "../utils/index.js";
import pointer from "../utils/json-pointer/index.js";
import { isAsync } from "../../../shared/helpers/isAsync.js";
import useProcessManager from "./useProcessManager.js";
import useUpdateEffect from "../../../shared/helpers/useUpdateEffect.js";
export default function useFieldValidation({
finalSchema,
hasZodSchema,
onChangeValidatorProp,
onBlurValidator,
validateInitially,
validateUnchanged,
validateContinuously,
identifier,
disabled,
emptyValue,
required,
hasDataContext,
getAjvInstanceDataContext,
setFieldEventListener,
getValueByPath,
getSourceValue,
exportValidators,
props,
dataContext,
combinedErrorMessages,
makeIteratePath,
errorPrioritization,
sectionPath,
hasSectionSchema,
dataContextSchema,
valueRef,
changedRef,
transformers,
schemaValidatorRef,
asyncProcessRef,
validatedValueRef,
changeEventResultRef,
localErrorInitiatorRef,
error,
persistErrorState,
clearErrorState,
revealError,
hideError,
setFieldState,
ensureErrorMessageObject,
asyncBehaviorIsEnabled,
defineAsyncProcess,
forceUpdate,
revealErrorRef
}) {
const {
startProcess
} = useProcessManager();
const onChangeValidator = useMemo(() => {
if (onChangeValidatorProp) {
return onChangeValidatorProp;
}
if (validateContinuously && onBlurValidator) {
return onBlurValidator;
}
return undefined;
}, [onChangeValidatorProp, validateContinuously, onBlurValidator]);
const onChangeValidatorRef = useRef(onChangeValidator);
useUpdateEffect(() => {
onChangeValidatorRef.current = onChangeValidator;
}, [onChangeValidator]);
const onBlurValidatorRef = useRef(onBlurValidator);
useUpdateEffect(() => {
onBlurValidatorRef.current = onBlurValidator;
}, [onBlurValidator]);
const getAjvInstance = useCallback(() => {
if (hasDataContext) {
return getAjvInstanceDataContext?.();
}
return undefined;
}, [hasDataContext, getAjvInstanceDataContext]);
if (!schemaValidatorRef.current && finalSchema) {
if (hasZodSchema) {
schemaValidatorRef.current = createZodValidator(finalSchema);
} else {
schemaValidatorRef.current = getAjvInstance()?.compile?.(finalSchema);
}
}
useUpdateEffect(() => {
if (finalSchema) {
if (hasZodSchema) {
schemaValidatorRef.current = createZodValidator(finalSchema);
} else {
schemaValidatorRef.current = getAjvInstance()?.compile?.(finalSchema);
}
} else {
schemaValidatorRef.current = undefined;
}
validateValue();
}, [finalSchema, hasZodSchema]);
const connectWithPathListenerRef = useRef(() => {
runOnChangeValidator();
runOnBlurValidator();
});
const handleConnectWithPath = useCallback(path => {
setFieldEventListener?.(path, 'onPathChange', connectWithPathListenerRef.current);
return {
getValue: () => getValueByPath(path)
};
}, [getValueByPath, setFieldEventListener]);
const additionalArgsRef = useRef({
validators: exportValidators,
props,
dataContext,
getValueByPath,
getSourceValue,
setFieldEventListener
});
additionalArgsRef.current.validators = exportValidators;
additionalArgsRef.current.props = props;
const additionalArgs = useMemo(() => {
const args = {
errorMessages: combinedErrorMessages,
...additionalArgsRef.current,
connectWithPath: path => {
return handleConnectWithPath(path);
},
connectWithItemPath: itemPath => {
return handleConnectWithPath(makeIteratePath(itemPath));
}
};
return args;
}, [combinedErrorMessages, handleConnectWithPath, makeIteratePath]);
const callStackRef = useRef([]);
const hasBeenCalledRef = useCallback(validator => {
const result = callStackRef.current.includes(validator);
callStackRef.current.push(validator);
return result;
}, []);
const callValidatorFnAsync = useCallback(async (validator, value = valueRef.current) => {
if (typeof validator !== 'function') {
return undefined;
}
const result = await validator(value, additionalArgs);
if (Array.isArray(result)) {
const errors = [];
for (const validatorOrError of result) {
if (validatorOrError instanceof Error) {
errors.push(validatorOrError);
} else if (!hasBeenCalledRef(validatorOrError)) {
const result = await callValidatorFnAsync(validatorOrError, value);
if (result instanceof Error) {
callStackRef.current = [];
return result;
}
}
}
if (errors.length > 0) {
return new FormError('Error', {
errors
});
}
callStackRef.current = [];
} else {
return ensureErrorMessageObject(result);
}
}, [additionalArgs, hasBeenCalledRef, ensureErrorMessageObject, valueRef]);
const callValidatorFnSync = useCallback((validator, value = valueRef.current) => {
if (typeof validator !== 'function') {
return undefined;
}
const result = validator(value, additionalArgs);
if (Array.isArray(result)) {
const hasAsyncValidator = result.some(validator => isAsync(validator));
if (hasAsyncValidator) {
return new Promise(resolve => {
callValidatorFnAsync(validator, value).then(result => {
resolve(result);
});
});
}
const errors = [];
for (const validatorOrError of result) {
if (validatorOrError instanceof Error) {
errors.push(validatorOrError);
} else if (!hasBeenCalledRef(validatorOrError)) {
const result = callValidatorFnSync(validatorOrError, value);
if (result instanceof Error) {
callStackRef.current = [];
return result;
}
}
}
if (errors.length > 0) {
return new FormError('Error', {
errors
});
}
callStackRef.current = [];
} else {
return ensureErrorMessageObject(result);
}
}, [additionalArgs, callValidatorFnAsync, hasBeenCalledRef, ensureErrorMessageObject, valueRef]);
const validatorCacheRef = useRef({
onChangeValidator: null,
onBlurValidator: null
});
const revealOnChangeValidatorResult = useCallback(({
result,
unchangedValue
}) => {
const runAsync = isAsync(onChangeValidatorRef.current);
if (unchangedValue) {
persistErrorState(runAsync ? 'gracefully' : 'weak', 'onChangeValidator', result);
if (validateInitially && !changedRef.current || validateUnchanged || validateContinuously || runAsync) {
window.requestAnimationFrame(() => {
if (localErrorInitiatorRef.current === 'onChangeValidator') {
revealError();
forceUpdate();
}
});
}
}
if (runAsync) {
defineAsyncProcess(undefined);
if (unchangedValue) {
setFieldState(result instanceof Error ? 'error' : 'complete');
} else {
setFieldState('pending');
}
}
}, [validateContinuously, defineAsyncProcess, persistErrorState, revealError, setFieldState, validateInitially, validateUnchanged, changedRef, localErrorInitiatorRef]);
const callOnChangeValidator = useCallback(async () => {
if (typeof onChangeValidatorRef.current !== 'function') {
return {};
}
const tmpValue = valueRef.current;
let result = isAsync(onChangeValidatorRef.current) ? await callValidatorFnAsync(onChangeValidatorRef.current) : callValidatorFnSync(onChangeValidatorRef.current);
if (result instanceof Promise) {
result = await result;
}
const unchangedValue = tmpValue === valueRef.current;
return {
result,
unchangedValue
};
}, [callValidatorFnAsync, callValidatorFnSync, valueRef]);
const startOnChangeValidatorValidation = useCallback(async () => {
if (typeof onChangeValidatorRef.current !== 'function') {
return undefined;
}
if (isAsync(onChangeValidatorRef.current)) {
defineAsyncProcess('onChangeValidator');
setFieldState('validating');
hideError();
}
const tmpValue = valueRef.current;
let result = isAsync(onChangeValidatorRef.current) ? await callValidatorFnAsync(onChangeValidatorRef.current) : callValidatorFnSync(onChangeValidatorRef.current);
if (result instanceof Promise) {
result = await result;
}
const unchangedValue = tmpValue === valueRef.current;
revealOnChangeValidatorResult({
result,
unchangedValue
});
return {
result
};
}, [callValidatorFnAsync, callValidatorFnSync, defineAsyncProcess, hideError, revealOnChangeValidatorResult, setFieldState, valueRef]);
const runOnChangeValidator = useCallback(async () => {
if (!onChangeValidatorRef.current) {
return undefined;
}
const {
result,
unchangedValue
} = await callOnChangeValidator();
if (String(result) !== String(validatorCacheRef.current.onChangeValidator)) {
if (result) {
revealOnChangeValidatorResult({
result,
unchangedValue
});
} else {
hideError();
clearErrorState();
}
}
validatorCacheRef.current.onChangeValidator = result || null;
}, [callOnChangeValidator, clearErrorState, hideError, revealOnChangeValidatorResult]);
const callOnBlurValidator = useCallback(async ({
overrideValue = null
} = {}) => {
if (typeof onBlurValidatorRef.current !== 'function') {
return {};
}
const value = transformers.current.toEvent(overrideValue !== null && overrideValue !== void 0 ? overrideValue : valueRef.current, 'onBlurValidator');
let result = isAsync(onBlurValidatorRef.current) ? await callValidatorFnAsync(onBlurValidatorRef.current, value) : callValidatorFnSync(onBlurValidatorRef.current, value);
if (result instanceof Promise) {
result = await result;
}
return {
result
};
}, [callValidatorFnAsync, callValidatorFnSync, transformers, valueRef]);
const revealOnBlurValidatorResult = useCallback(({
result
}) => {
persistErrorState('gracefully', 'onBlurValidator', result);
if (isAsync(onBlurValidatorRef.current)) {
defineAsyncProcess(undefined);
setFieldState(result instanceof Error ? 'error' : 'complete');
}
revealError();
}, [defineAsyncProcess, persistErrorState, revealError, setFieldState]);
const startOnBlurValidatorProcess = useCallback(async ({
overrideValue = null
} = {}) => {
if (typeof onBlurValidatorRef.current !== 'function') {
return undefined;
}
if ((localErrorInitiatorRef.current === 'required' || localErrorInitiatorRef.current === 'schema') && !asyncBehaviorIsEnabled && !isAsync(onChangeValidatorRef.current)) {
return undefined;
}
if (isAsync(onBlurValidatorRef.current)) {
defineAsyncProcess('onBlurValidator');
setFieldState('validating');
}
const value = transformers.current.toEvent(overrideValue !== null && overrideValue !== void 0 ? overrideValue : valueRef.current, 'onBlurValidator');
let result = isAsync(onBlurValidatorRef.current) ? await callValidatorFnAsync(onBlurValidatorRef.current, value) : callValidatorFnSync(onBlurValidatorRef.current, value);
if (result instanceof Promise) {
result = await result;
}
revealOnBlurValidatorResult({
result
});
return {
result
};
}, [asyncBehaviorIsEnabled, callValidatorFnAsync, callValidatorFnSync, defineAsyncProcess, revealOnBlurValidatorResult, setFieldState, localErrorInitiatorRef, transformers, valueRef]);
const runOnBlurValidator = useCallback(async () => {
if (!onBlurValidatorRef.current) {
return undefined;
}
const {
result
} = await callOnBlurValidator();
if (String(result) !== String(validatorCacheRef.current.onBlurValidator) && revealErrorRef.current) {
if (result) {
revealOnBlurValidatorResult({
result
});
} else {
hideError();
clearErrorState();
}
}
validatorCacheRef.current.onBlurValidator = result || null;
}, [callOnBlurValidator, clearErrorState, hideError, revealOnBlurValidatorResult]);
const prioritizeContextSchema = useMemo(() => {
if (errorPrioritization) {
const contextSchema = dataContextSchema;
if (isZodSchema(contextSchema)) {
return errorPrioritization?.indexOf('contextSchema') === 0;
}
const schemaPath = identifier.split('/').join('/properties/');
const hasContextSchema = pointer.has(contextSchema || {}, schemaPath);
return hasContextSchema && errorPrioritization?.indexOf('contextSchema') === 0;
}
return undefined;
}, [dataContextSchema, errorPrioritization, identifier]);
const prioritizeSectionSchema = useMemo(() => {
return errorPrioritization?.indexOf('sectionSchema') === 0 && hasSectionSchema;
}, [errorPrioritization, hasSectionSchema]);
const validateValue = useCallback(async () => {
const isProcessActive = startProcess();
if (disabled) {
if (isProcessActive()) {
clearErrorState();
}
hideError();
setFieldState(undefined);
return undefined;
}
const value = valueRef.current;
changeEventResultRef.current = null;
validatedValueRef.current = null;
let initiator = null;
try {
const requiredError = transformers.current.validateRequired(value, {
emptyValue,
required,
isChanged: changedRef.current,
error: new FormError('Field.errorRequired')
});
if (requiredError instanceof Error) {
initiator = 'required';
throw requiredError;
}
if (error instanceof Error) {
initiator = 'errorProp';
throw error;
}
const skipLocalSchema = prioritizeContextSchema || prioritizeSectionSchema;
if (value !== undefined && !skipLocalSchema && typeof schemaValidatorRef.current === 'function') {
const validationResult = schemaValidatorRef.current(value);
if (validationResult !== true) {
let error;
if (hasZodSchema) {
const zodError = validationResult;
error = zodErrorsToOneFormError(zodError.issues);
} else {
error = getAjvInstance()?.ajvErrorsToOneFormError(schemaValidatorRef.current.errors, value);
}
initiator = 'schema';
throw error;
}
}
if (onChangeValidatorRef.current && (changedRef.current || validateInitially || validateUnchanged)) {
const {
result
} = await startOnChangeValidatorValidation();
if (result instanceof Error) {
initiator = 'onChangeValidator';
throw result;
}
}
if (onBlurValidatorRef.current && validateInitially && !changedRef.current) {
const {
result
} = await startOnBlurValidatorProcess();
if (result instanceof Error) {
initiator = 'onBlurValidator';
throw result;
}
}
if (isProcessActive()) {
clearErrorState();
}
validatedValueRef.current = value;
} catch (error) {
if (isProcessActive()) {
persistErrorState('weak', initiator, error);
if (validateContinuously && changedRef.current) {
revealError();
}
}
}
}, [clearErrorState, disabled, emptyValue, error, hasZodSchema, hideError, persistErrorState, prioritizeContextSchema, prioritizeSectionSchema, required, revealError, setFieldState, startOnBlurValidatorProcess, startOnChangeValidatorValidation, startProcess, validateInitially, validateContinuously, validateUnchanged, valueRef, changedRef, changeEventResultRef, validatedValueRef, transformers, schemaValidatorRef]);
connectWithPathListenerRef.current = () => {
runOnChangeValidator();
runOnBlurValidator();
};
return {
validateValue,
startOnChangeValidatorValidation,
startOnBlurValidatorProcess,
runOnChangeValidator,
runOnBlurValidator,
callOnBlurValidator,
handleConnectWithPath,
onChangeValidator,
onChangeValidatorRef,
onBlurValidatorRef,
additionalArgs
};
}
//# sourceMappingURL=useFieldValidation.js.map