UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

468 lines (467 loc) 16.4 kB
"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