UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

1,196 lines (1,195 loc) 39.6 kB
"use client"; import React, { useRef, useMemo, useCallback, useReducer, useEffect, useContext } from 'react'; import pointer from "../../utils/json-pointer/index.js"; import { makeAjvInstance, ajvErrorsToFormErrors, isZodSchema, createZodValidator, zodErrorsToFormErrors } from "../../utils/index.js"; import { debounce, warn } from "../../../../shared/helpers.js"; import FieldPropsProvider from "../../Field/Provider/index.js"; import useUpdateEffect from "../../../../shared/helpers/useUpdateEffect.js"; import GlobalStatusProvider from "../../../../components/global-status/GlobalStatusProvider.js"; import { isAsync } from "../../../../shared/helpers/isAsync.js"; import { createReferenceKey, useSharedState } from "../../../../shared/helpers/useSharedState.js"; import SharedContext from "../../../../shared/Context.js"; import useTranslation from "../../hooks/useTranslation.js"; import { appendPath } from "../../hooks/usePath.js"; import DataContext from "../Context.js"; import { structuredClone } from "../../../../shared/helpers/structuredClone.js"; import { useIsomorphicLayoutEffect as useLayoutEffect } from "../../../../shared/helpers/useIsomorphicLayoutEffect.js"; const isArrayJsonPointer = /^\/\d+(\/|$)/; export default function Provider(props) { const [, forceUpdate] = useReducer(() => ({}), {}); const { id, globalStatusId = 'main', defaultData, emptyData, data, schema, onChange, onPathChange, onSubmit, onSubmitRequest, onSubmitComplete, onCommit, onClear, onUpdateDataValue, scrollTopOnSubmit, minimumAsyncBehaviorTime, asyncSubmitTimeout, sessionStorageId, ajvInstance, transformIn, transformOut, filterSubmitData, countryCode, locale, translations, required, errorMessages, isolate, children, ...rest } = props; if (data !== undefined && sessionStorageId !== undefined) { throw new Error('Use "defaultData" instead of "data" when using sessionStorageId'); } const { hasContext } = useContext(DataContext) || {}; if (hasContext && !isolate) { throw new Error('DataContext (Form.Handler) cannot be nested'); } const formElementRef = useRef(null); const { locale: sharedLocale } = useContext(SharedContext) || {}; const translation = useTranslation().Field; const ajvRef = useRef(); const getAjvInstance = useCallback((instance = ajvInstance) => { if (!ajvRef.current) { ajvRef.current = makeAjvInstance(instance); } return ajvRef.current; }, [ajvInstance]); useEffect(() => { if (schema && !isZodSchema(schema) && !ajvInstance) { warn('Form.Handler received a JSON Schema but no ajvInstance. Provide ajvInstance={makeAjvInstance()} to enable schema validation.'); } }, [schema, ajvInstance]); const mountedFieldsRef = useRef(new Map()); const snapshotsRef = useRef(new Map()); const existingFieldsRef = useRef(new Map()); const hasVisibleErrorRef = useRef(new Map()); const errorsRef = useRef(); const addSetShowAllErrorsRef = useRef([]); const showAllErrorsRef = useRef(false); const setShowAllErrors = useCallback(showAllErrors => { showAllErrorsRef.current = showAllErrors ? Date.now() : showAllErrors; forceUpdate(); addSetShowAllErrorsRef.current.forEach(fn => fn?.(showAllErrors)); }, []); const executeSectionValidators = useCallback(contextErrors => { if (!sectionSchemasRef.current.size) { const hasContextErrors = contextErrors && Object.keys(contextErrors).length > 0; errorsRef.current = hasContextErrors ? contextErrors : undefined; return errorsRef.current; } const sectionErrors = {}; sectionSchemasRef.current.forEach(({ path, schema, validator }) => { if (!validator) { return; } const normalizedPath = path || '/'; const sectionData = normalizedPath === '/' ? internalDataRef.current : pointer.has(internalDataRef.current, normalizedPath) ? pointer.get(internalDataRef.current, normalizedPath) : undefined; const validationResult = validator(sectionData); if (validationResult === true) { return; } let errors = {}; if (isZodSchema(schema)) { const issues = validator.errors; if (issues?.length) { errors = zodErrorsToFormErrors(issues); } } else { const ajvValidator = validator; const ajvErrors = ajvValidator.errors; if (ajvErrors && ajvErrors.length) { errors = ajvErrorsToFormErrors(ajvErrors, sectionData); } } Object.entries(errors).forEach(([errorPath, error]) => { const combinedPath = appendPath(normalizedPath, errorPath); sectionErrors[combinedPath] = error; }); }); const hasSectionErrors = Object.keys(sectionErrors).length > 0; const hasContextErrors = contextErrors && Object.keys(contextErrors).length > 0; if (!hasSectionErrors && !hasContextErrors) { errorsRef.current = undefined; return undefined; } errorsRef.current = { ...(contextErrors !== null && contextErrors !== void 0 ? contextErrors : {}), ...(hasSectionErrors ? sectionErrors : {}) }; return errorsRef.current; }, []); const revealError = useCallback((path, hasError) => { if (hasError) { hasVisibleErrorRef.current.set(path, hasError); } else { hasVisibleErrorRef.current.delete(path); } forceUpdate(); }, []); const submitStateRef = useRef({}); const setSubmitState = useCallback(state => { submitStateRef.current = { ...submitStateRef.current, ...state }; forceUpdate(); }, []); const formStateRef = useRef(); const activeSubmitButtonIdRef = useRef(); const keepPending = useRef(false); const setFormState = useCallback((formState, options = {}) => { if (typeof options?.keepPending === 'boolean') { keepPending.current = options?.keepPending; } formStateRef.current = formState; forceUpdate(); }, []); const setActiveSubmitButtonId = useCallback(id => { activeSubmitButtonIdRef.current = id; forceUpdate(); }, []); const fieldErrorRef = useRef({}); const fieldStateRef = useRef({}); const validationVersionRef = useRef(0); const bumpValidationVersionRef = useRef(() => null); const initialData = useMemo(() => { if (sessionStorageId && typeof window !== 'undefined') { const sessionDataJSON = window.sessionStorage?.getItem(sessionStorageId); if (sessionDataJSON) { try { return JSON.parse(sessionDataJSON); } catch (e) { window.sessionStorage?.removeItem(sessionStorageId); } } } return data !== null && data !== void 0 ? data : defaultData; }, []); const internalDataRef = useRef(initialData); const isEmptyDataRef = useRef(false); const ajvValidatorRef = useRef(); const sectionSchemasRef = useRef(new Map()); const sectionSchemaPathsRef = useRef(new Set()); const createUnifiedValidator = useCallback(schema => { if (isZodSchema(schema)) { const zodValidator = createZodValidator(schema); const unifiedValidator = value => { const result = zodValidator(value); if (result === true) { return true; } else { unifiedValidator.errors = result.issues; return false; } }; return unifiedValidator; } else { return getAjvInstance()?.compile(schema); } }, []); useMemo(() => { if (schema) { ajvValidatorRef.current = createUnifiedValidator(schema); } }, [schema, createUnifiedValidator]); const executeAjvValidator = useCallback(() => { if (!ajvValidatorRef.current) { errorsRef.current = undefined; return undefined; } const validationResult = ajvValidatorRef.current(internalDataRef.current); if (!validationResult) { const errors = ajvValidatorRef.current.errors; if (errors && Array.isArray(errors)) { if (errors.length > 0 && errors[0] && typeof errors[0] === 'object' && 'code' in errors[0]) { errorsRef.current = zodErrorsToFormErrors(errors); } else { errorsRef.current = ajvErrorsToFormErrors(errors, internalDataRef.current); } return errorsRef.current; } errorsRef.current = undefined; return undefined; } errorsRef.current = undefined; return undefined; }, []); const validateData = useCallback(() => { const contextErrors = executeAjvValidator(); executeSectionValidators(contextErrors); forceUpdate(); }, [executeAjvValidator, executeSectionValidators, forceUpdate]); const checkFieldStateFor = useCallback((path, state) => { return Boolean(state === 'error' ? errorsRef.current?.[path] instanceof Error || fieldErrorRef.current[path] instanceof Error : fieldStateRef.current[path] === state); }, []); const hasFieldState = useCallback(state => { return Array.from(mountedFieldsRef.current.entries()).some(([path, item]) => { return item.isMounted && checkFieldStateFor(path, state); }); }, [checkFieldStateFor]); const hasFieldError = useCallback(path => { return Array.from(mountedFieldsRef.current.entries()).some(([p, item]) => { return item.isMounted && p === path && checkFieldStateFor(path, 'error'); }); }, [checkFieldStateFor]); const hasErrors = useCallback(() => { return hasFieldState('error'); }, [hasFieldState]); const setFieldError = useCallback((path, error) => { if (error) { fieldErrorRef.current[path] = error; } else { delete fieldErrorRef.current[path]; } bumpValidationVersionRef.current(); for (const item of fieldEventListenersRef.current) { const { type, callback } = item; if (type === 'onSetFieldError') { callback(); } } }, []); const setFieldState = useCallback((path, fieldState) => { if (fieldState !== fieldStateRef.current[path]) { fieldStateRef.current[path] = fieldState; forceUpdate(); bumpValidationVersionRef.current(); } }, []); const getDataPathHandlerParameters = useCallback((path, data = internalDataRef.current) => { const value = pointer.has(data, path) ? pointer.get(data, path) : undefined; const { value: displayValue } = fieldDisplayValueRef.current[path] || {}; const props = fieldInternalsRef.current[path]?.props || {}; const label = props?.['label']; const error = fieldErrorRef.current[path]; return { path, value, displayValue, label, props, data: internalDataRef.current, error, internal: { error } }; }, []); const mutateDataHandler = useCallback((data, handler, { remove = false, mutate = true, fireHandlerWhen = null } = {}) => { const freshData = {}; const mutateEntry = (path, result) => { if (remove) { if (result === false) { data = structuredClone(data); pointer.remove(data, path); } } else { if (typeof result !== 'undefined') { if (mutate) { data = structuredClone(data); pointer.set(data, path, result); } else { pointer.set(freshData, path, result); } } } }; if (typeof handler === 'function') { const run = path => { const { type } = fieldDisplayValueRef.current[path] || {}; if (fireHandlerWhen?.({ type }) !== false) { const result = handler(getDataPathHandlerParameters(path, data)); mutateEntry(path, result); } }; for (const path in fieldInternalsRef.current) { const exists = pointer.has(data, path); if (exists) { run(path); } } pointer.walk(internalDataRef.current, (value, path) => { if (fieldInternalsRef.current[path] === undefined) { run(path); } }); if (!mutate) { return freshData; } return data; } else if (handler) { const runFilter = ({ path, condition }) => { const exists = pointer.has(data, path); if (exists) { const result = typeof condition === 'function' ? condition(getDataPathHandlerParameters(path, data)) : condition; mutateEntry(path, result); } }; const wildcardPaths = []; Object.entries(handler).forEach(([path, condition]) => { if (path.includes('*')) { const parts = path.split(/\/\*/g); const exists = pointer.has(data, parts[0]); if (exists) { const traverse = (subData, subPath, idx) => { if (idx === parts.length - 1) { wildcardPaths.push({ path: subPath, condition }); return; } const list = pointer.get(subData, subPath); if (Array.isArray(list)) { list.forEach((_, i) => { traverse(list[i], `${subPath}/${i}${parts[idx + 1]}`, idx + 1); }); } }; traverse(data, parts[0], 0); } } else { runFilter({ path, condition }); } }); wildcardPaths.forEach(runFilter); return data; } return data; }, [getDataPathHandlerParameters]); const visibleDataHandler = useCallback((data = internalDataRef.current, { keepPaths, removePaths } = {}) => { const visibleData = {}; mountedFieldsRef.current.forEach((item, path) => { if (item && item.isVisible !== false && (item.isPreMounted !== false || item.wasStepChange === true) && (removePaths ? !removePaths.includes(path) : true) && pointer.has(data, path)) { pointer.set(visibleData, path, pointer.get(data, path)); } }); if (keepPaths) { keepPaths.forEach(path => { if (pointer.has(data, path)) { pointer.set(visibleData, path, pointer.get(data, path)); } }); } return visibleData; }, []); const filterDataHandler = useCallback((data, filter) => { if (filter) { return mutateDataHandler(data, filter, { remove: true }); } return data; }, [mutateDataHandler]); const filterData = useCallback((filter, data = internalDataRef.current) => { return filterDataHandler(data, filter); }, [filterDataHandler]); const fieldDisplayValueRef = useRef({}); const fieldConnectionsRef = useRef({}); const fieldStatusRef = useRef({}); const setFieldConnection = useCallback((path, connections) => { fieldConnectionsRef.current[path] = connections; }, []); const fieldInternalsRef = useRef({}); const setFieldInternals = useCallback((path, internals) => { fieldInternalsRef.current[path] = Object.assign(fieldInternalsRef.current[path] || {}, internals); }, []); const valueInternalsRef = useRef({}); const setValueInternals = useCallback((path, props) => { valueInternalsRef.current[path] = Object.assign(valueInternalsRef.current[path] || {}, { props }); }, []); const hasFieldWithAsyncValidator = useCallback(() => { for (const path in fieldInternalsRef.current) { const fieldInternals = fieldInternalsRef.current[path] || {}; const { enableAsyncMode, props } = fieldInternals; if (enableAsyncMode || isAsync(props?.onChangeValidator) || isAsync(props?.onBlurValidator)) { return true; } } return false; }, []); const sharedData = useSharedState(id); const sharedAttachments = useSharedState(id ? createReferenceKey(id, 'attachments') : undefined); const sharedDataContext = useSharedState(id ? createReferenceKey(id, 'data-context') : undefined); const setSharedData = sharedData.set; const extendSharedData = sharedData.extend; const extendAttachment = sharedAttachments.extend; const rerenderUseDataHook = sharedAttachments.data?.rerenderUseDataHook; bumpValidationVersionRef.current = () => { if (id) { validationVersionRef.current += 1; extendAttachment({ validationVersion: validationVersionRef.current }, { preventSyncOfSameInstance: true }); } }; const hasHydratedFieldErrorRef = useRef(false); if (!hasHydratedFieldErrorRef.current) { const sharedFieldErrorRef = sharedAttachments.data?.fieldErrorRef; if (sharedFieldErrorRef?.current) { fieldErrorRef.current = sharedFieldErrorRef.current; hasHydratedFieldErrorRef.current = true; } } const cacheRef = useRef({ data, schema, shared: sharedData.data, hasUsedInitialData: false }); const internalData = useMemo(() => { if (id && initialData && !sharedData.data) { sharedData.update(initialData); } if (id && initialData && sharedData.data && cacheRef.current.shared === sharedData.data && internalDataRef.current === initialData && typeof internalDataRef.current === 'object') { return { ...internalDataRef.current, ...(sharedData.data || {}) }; } if (id && !initialData && !internalDataRef.current && sharedData.data && cacheRef.current.shared === sharedData.data) { return sharedData.data; } if (id && sharedData.data && cacheRef.current.shared !== sharedData.data && sharedData.data !== internalDataRef.current && typeof internalDataRef.current === 'object') { cacheRef.current.shared = sharedData.data; if (isEmptyDataRef.current) { return Array.isArray(internalDataRef.current) ? [] : clearedData; } return { ...internalDataRef.current, ...(sharedData.data || {}) }; } if (data !== cacheRef.current.data) { cacheRef.current.data = data; return data; } return internalDataRef.current; }, [id, initialData, sharedData, data]); internalDataRef.current = props.path && pointer.has(internalData, props.path) ? pointer.get(internalData, props.path) : internalData; const clearData = useCallback(() => { var _ref; isEmptyDataRef.current = true; internalDataRef.current = (_ref = typeof emptyData === 'function' ? emptyData(internalDataRef.current) : emptyData) !== null && _ref !== void 0 ? _ref : Array.isArray(internalDataRef.current) ? [] : clearedData; if (id) { setSharedData(internalDataRef.current); } forceUpdate(); onClear?.(); requestAnimationFrame?.(() => { isEmptyDataRef.current = false; }); }, [emptyData, id, onClear, setSharedData]); useLayoutEffect(() => { const hasNoErrors = errorsRef.current === undefined; const contextErrors = executeAjvValidator(); executeSectionValidators(contextErrors); if (hasNoErrors && errorsRef.current !== undefined) { forceUpdate(); } }, [internalDataRef.current, executeAjvValidator, executeSectionValidators]); const registerSectionSchema = useCallback(registration => { const normalizedPath = registration.path && registration.path !== '/' ? registration.path : '/'; const validator = createUnifiedValidator(registration.schema); sectionSchemasRef.current.set(registration.id, { path: normalizedPath, schema: registration.schema, validator }); sectionSchemaPathsRef.current.add(normalizedPath); validateData(); return () => { const entry = sectionSchemasRef.current.get(registration.id); if (!entry) { return; } sectionSchemasRef.current.delete(registration.id); const stillUsesPath = Array.from(sectionSchemasRef.current.values()).some(item => item.path === entry.path); if (!stillUsesPath) { sectionSchemaPathsRef.current.delete(entry.path); } validateData(); }; }, [createUnifiedValidator, validateData]); const storeInSession = useMemo(() => { return debounce(() => { window.sessionStorage?.setItem(sessionStorageId, JSON.stringify(internalDataRef.current)); }, process.env.NODE_ENV === 'test' ? 1 : 800); }, [sessionStorageId]); const setData = useCallback((newData, { preventUpdate = false } = {}) => { if (transformIn) { newData = mutateDataHandler(newData, transformIn); } internalDataRef.current = newData; if (id) { extendSharedData(newData, { preventSyncOfSameInstance: true }); if (filterSubmitData) { rerenderUseDataHook?.(); } } if (sessionStorageId && typeof window !== 'undefined') { storeInSession(); } if (!preventUpdate) { forceUpdate(); } }, [extendSharedData, filterSubmitData, id, mutateDataHandler, rerenderUseDataHook, sessionStorageId, storeInSession, transformIn]); const updateDataValue = useCallback((path, value, { preventUpdate } = {}) => { var _internalDataRef$curr; if (!path) { return; } const givenData = path === '/' ? value : (_internalDataRef$curr = internalDataRef.current) !== null && _internalDataRef$curr !== void 0 ? _internalDataRef$curr : path.match(isArrayJsonPointer) ? [] : {}; let newData = null; try { newData = structuredClone(givenData); } catch (e) { newData = givenData; } if (path !== '/') { pointer.set(newData, path, value); } setData(newData, { preventUpdate }); onUpdateDataValue?.(path, value, { preventUpdate }); }, [onUpdateDataValue, setData]); const handlePathChangeUnvalidated = useCallback(async (path, value) => { if (!path) { return null; } updateDataValue(path, value); if (isAsync(onPathChange)) { await onPathChange?.(path, value); } else { onPathChange?.(path, value); } for (const itm of fieldEventListenersRef.current) { if (itm.type === 'onPathChange' && itm.path === path) { const { callback } = itm; if (isAsync(callback)) { await callback({ value }); } else { callback({ value }); } } } }, [onPathChange, updateDataValue]); const handlePathChange = useCallback(async (path, value = '_undefined_') => { if (!path) { return null; } if (value !== '_undefined_') { handlePathChangeUnvalidated(path, value); } showAllErrorsRef.current = false; validateData(); const data = internalDataRef.current; const options = { filterData }; const transformedData = transformOut ? mutateDataHandler(data, transformOut) : data; for (const cb of changeHandlerStackRef.current) { if (isAsync(onChange)) { await cb(transformedData, options); } else { cb(transformedData, options); } } if (isAsync(onChange)) { return await onChange(transformedData, options); } return onChange?.(transformedData, options); }, [filterData, handlePathChangeUnvalidated, mutateDataHandler, onChange, transformOut, validateData]); const changeHandlerStackRef = useRef([]); const addOnChangeHandler = useCallback(callback => { const exists = changeHandlerStackRef.current.some(cb => { return callback === cb; }); if (!exists) { changeHandlerStackRef.current.push(callback); } }, []); const setMountedFieldState = useCallback((path, state) => { const currentState = mountedFieldsRef.current.get(path) || {}; mountedFieldsRef.current.set(path, { ...currentState, ...state }); const hasChanges = Object.keys(state).some(key => { return currentState[key] !== state[key]; }); if (hasChanges) { Promise.resolve().then(() => { bumpValidationVersionRef.current(); }); } for (const itm of fieldEventListenersRef.current) { if (itm.type === 'onMount' && itm.path === path) { const { callback } = itm; callback(); } } }, []); const scrollToTop = useCallback(() => { if (typeof window !== 'undefined') { window?.scrollTo?.({ top: 0, behavior: 'smooth' }); } }, []); const resolveStateResult = useCallback(async fn => { try { const result = await fn(); if (result instanceof Error) { throw result; } return result; } catch (error) { return { error: error }; } }, []); const applySubmitState = useCallback(result => { if (result?.error || result?.warning || result?.info || result?.customStatus) { setSubmitState(result); } }, [setSubmitState]); const handleSubmitCall = useCallback(async args => { const { onSubmit, enableAsyncBehavior, skipFieldValidation, skipErrorCheck } = args; setSubmitState({ error: undefined, warning: undefined, info: undefined, customStatus: undefined }); const asyncBehaviorIsEnabled = (skipErrorCheck ? enableAsyncBehavior : !hasErrors() || hasFieldState('pending')) && (enableAsyncBehavior || hasFieldWithAsyncValidator()); if (asyncBehaviorIsEnabled) { setFormState('pending'); } if (!skipFieldValidation) { for (const item of fieldEventListenersRef.current) { const { path, type, callback } = item; if (type === 'onSubmitCall' && mountedFieldsRef.current.get(path)?.isMounted) { if (asyncBehaviorIsEnabled) { await callback(); } else { callback(); } } } } let result; if (!(skipErrorCheck ? false : hasErrors()) && !hasFieldState('pending') && (skipFieldValidation ? true : !hasFieldState('error'))) { result = await resolveStateResult(async () => { if (isolate) { for (const item of fieldEventListenersRef.current) { const { type, callback } = item; if (type === 'onBeforeCommit') { callback(); } } const commitResult = await onCommit?.(internalDataRef.current, { clearData }); for (const item of fieldEventListenersRef.current) { const { type, callback } = item; if (type === 'onAfterCommit') { callback(); } } return commitResult; } else { return await onSubmit(); } }); if (asyncBehaviorIsEnabled) { if (result?.error) { setFormState('abort'); } else if (keepPending.current !== true) { setFormState('complete'); } } if (result?.['status']) { setFormState(result?.['status']); } applySubmitState(result); } else { if (asyncBehaviorIsEnabled) { await new Promise(resolve => window.requestAnimationFrame(resolve)); setFormState(undefined); if (!skipFieldValidation) { onSubmitContinueRef.current = () => { window.requestAnimationFrame(() => { handleSubmitCall({ ...args, skipFieldValidation: true }); }); }; } } setShowAllErrors(true); const submitRequestResult = await resolveStateResult(() => onSubmitRequest?.({ getErrors: () => Object.keys(fieldErrorRef.current).map(path => { return getDataPathHandlerParameters(path); }).filter(({ error }) => error) })); applySubmitState(submitRequestResult); if (submitRequestResult) { result = submitRequestResult; } for (const itm of fieldEventListenersRef.current) { if (itm.type === 'onSubmitRequest') { itm.callback(); } } } return result; }, [applySubmitState, clearData, getDataPathHandlerParameters, hasErrors, hasFieldState, hasFieldWithAsyncValidator, isolate, onCommit, onSubmitRequest, resolveStateResult, setFormState, setShowAllErrors]); const getSubmitData = useCallback(() => { const data = internalDataRef.current; const mutatedData = transformOut ? mutateDataHandler(data, transformOut) : data; const filteredData = filterSubmitData ? filterDataHandler(mutatedData, filterSubmitData) : mutatedData; return filteredData; }, [filterDataHandler, filterSubmitData, mutateDataHandler, transformOut]); const getSubmitParams = useCallback(() => { const reduceToVisibleFields = (data, options) => { return visibleDataHandler(transformOut ? mutateDataHandler(data, transformOut) : data, options); }; const transformData = (data, handler) => { return mutateDataHandler(data, handler, { mutate: false, fireHandlerWhen: ({ type }) => type === 'field' }); }; const formElement = formElementRef.current; const params = { filterData, reduceToVisibleFields, transformData, resetForm: () => { formElement?.reset?.(); if (typeof window !== 'undefined') { if (sessionStorageId) { window.sessionStorage.removeItem(sessionStorageId); } } forceUpdate(); }, clearData }; return params; }, [clearData, filterData, mutateDataHandler, sessionStorageId, transformOut, visibleDataHandler]); const handleSubmit = useCallback(async () => { for (const item of fieldEventListenersRef.current) { const { type, callback } = item; if (type === 'onBeforeSubmit') { callback(); } } return await handleSubmitCall({ enableAsyncBehavior: isAsync(onSubmit), onSubmit: async () => { let stop = false; const preventSubmit = () => stop = true; for (const item of fieldEventListenersRef.current) { const { type, callback } = item; if (type === 'onSubmit') { if (isAsync(callback)) { await callback({ preventSubmit }); } else { callback({ preventSubmit }); } } } if (stop) { return; } const data = getSubmitData(); const options = getSubmitParams(); let result = undefined; if (isAsync(onSubmit)) { result = await onSubmit(data, options); } else { result = onSubmit?.(data, options); } const completeResult = await onSubmitComplete?.(data, result); if (completeResult) { result = Object.keys(result).length > 0 ? { ...result, ...completeResult } : completeResult; } if (scrollTopOnSubmit) { scrollToTop(); } return result; } }); }, [getSubmitData, getSubmitParams, handleSubmitCall, onSubmit, onSubmitComplete, scrollToTop, scrollTopOnSubmit]); const fieldEventListenersRef = useRef([]); const setFieldEventListener = useCallback((path, type, callback, { remove = false } = {}) => { fieldEventListenersRef.current = fieldEventListenersRef.current.filter(({ path: p, type: t, callback: c }) => { return !(p === path && t === type && c === callback); }); if (!remove) { fieldEventListenersRef.current.push({ path, type, callback }); } }, []); const onSubmitContinueRef = useRef(null); if (!hasFieldState('pending')) { onSubmitContinueRef.current?.(); onSubmitContinueRef.current = null; } useUpdateEffect(() => { if (schema && schema !== cacheRef.current.schema) { cacheRef.current.schema = schema; ajvValidatorRef.current = createUnifiedValidator(schema); validateData(); forceUpdate(); } }, [schema, validateData, forceUpdate, createUnifiedValidator]); const onTimeout = useCallback(() => { setFormState(undefined); setSubmitState({ info: undefined, warning: undefined, error: undefined, customStatus: undefined }); }, [setFormState, setSubmitState]); useLayoutEffect(() => { if (id) { if (initialData && !sharedData.data) { extendSharedData(initialData, { preventSyncOfSameInstance: true }); } } }, [id, initialData, extendSharedData, sharedData.data]); useLayoutEffect(() => { if (id) { extendAttachment({ visibleDataHandler, filterDataHandler, validationVersion: validationVersionRef.current, hasErrors, hasFieldError, setShowAllErrors, setSubmitState, clearData, setData, updateDataValue, fieldConnectionsRef, fieldStatusRef, fieldErrorRef, internalDataRef }, { preventSyncOfSameInstance: true }); if (filterSubmitData) { rerenderUseDataHook?.(); } } }, [clearData, extendAttachment, filterDataHandler, filterSubmitData, hasErrors, hasFieldError, id, rerenderUseDataHook, setData, setShowAllErrors, setSubmitState, updateDataValue, visibleDataHandler]); const { bufferedFormState: formState } = useFormStatusBuffer({ formState: formStateRef.current, waitFor: hasFieldState('pending'), minimumAsyncBehaviorTime, asyncSubmitTimeout, onTimeout }); const submitState = submitStateRef.current; const disabled = typeof rest?.['disabled'] === 'boolean' ? rest?.['disabled'] : formState === 'pending' ? true : undefined; const contextErrorMessages = errorMessages?.[locale !== null && locale !== void 0 ? locale : sharedLocale] || errorMessages; const getSourceValue = useCallback(value => { const data = internalDataRef.current; if (String(value).startsWith('/') && pointer.has(data, String(value))) { return pointer.get(data, String(value)); } return value; }, []); const contextValue = { handlePathChange, handlePathChangeUnvalidated, handleSubmit, setMountedFieldState, handleSubmitCall, setFormState, setSubmitState, setShowAllErrors, revealError, setFieldEventListener, setFieldState, setFieldError, setFieldConnection, setFieldInternals, setValueInternals, hasErrors, hasFieldError, hasFieldState, hasFieldWithAsyncValidator, validateData, updateDataValue, setData, clearData, visibleDataHandler, filterDataHandler, getSubmitData, getSubmitParams, addOnChangeHandler, scrollToTop, registerSectionSchema, schema, disabled, required, formState, activeSubmitButtonId: activeSubmitButtonIdRef.current, submitState, setActiveSubmitButtonId, contextErrorMessages, hasContext: true, errors: errorsRef.current, showAllErrors: showAllErrorsRef.current, hasVisibleError: hasVisibleErrorRef.current.size > 0, addSetShowAllErrorsRef, fieldConnectionsRef, fieldDisplayValueRef, fieldInternalsRef, valueInternalsRef, mountedFieldsRef, snapshotsRef, existingFieldsRef, formElementRef, isEmptyDataRef, fieldErrorRef, errorsRef, sectionSchemaPathsRef, getAjvInstance, countryCode: countryCode ? getSourceValue(countryCode) : undefined, id, data: internalDataRef.current, internalDataRef, props, ...rest }; if (id) { sharedDataContext.set(contextValue); } const show = Boolean(showAllErrorsRef.current); const resolvedLocale = locale || sharedLocale; const customErrorSummaryTitle = translations?.[resolvedLocale]?.Field?.errorSummaryTitle; const formStatusConfig = useMemo(() => { var _ref2, _status$stack$0$title; const status = show ? GlobalStatusProvider.get(globalStatusId) : null; return { globalStatus: { show, id: globalStatusId, title: (_ref2 = (_status$stack$0$title = status?.stack[0]?.title) !== null && _status$stack$0$title !== void 0 ? _status$stack$0$title : customErrorSummaryTitle) !== null && _ref2 !== void 0 ? _ref2 : translation.errorSummaryTitle } }; }, [globalStatusId, show, customErrorSummaryTitle, translation.errorSummaryTitle]); return React.createElement(DataContext.Provider, { value: contextValue }, React.createElement(FieldPropsProvider, { FormStatus: formStatusConfig, formElement: disabled ? { disabled: true } : undefined, locale: locale ? locale : undefined, translations: translations ? translations : undefined }, children)); } function useFormStatusBuffer(props) { const { formState, waitFor, minimumAsyncBehaviorTime, asyncSubmitTimeout, onTimeout } = props || {}; const [, forceUpdate] = useReducer(() => ({}), {}); const stateRef = useRef(); const nowRef = useRef(null); const timeoutRef = useRef({}); const setState = useCallback(state => { stateRef.current = state; forceUpdate(); }, []); const clear = useCallback(() => { for (const key in timeoutRef.current) { clearTimeout(timeoutRef.current[key]); } }, []); const hadCompleteRef = useRef(false); const activeElementRef = useRef(null); useEffect(() => { const isTest = process.env.NODE_ENV === 'test'; const minimum = minimumAsyncBehaviorTime !== null && minimumAsyncBehaviorTime !== void 0 ? minimumAsyncBehaviorTime : isTest ? 1 : 1000; if (stateRef.current && formState === 'error') { clear(); setState(undefined); return; } if (formState === 'abort') { clear(); setState('abort'); timeoutRef.current.reset = setTimeout(() => { nowRef.current = 0; setState(undefined); }, minimum); return; } if (formState === 'complete') { hadCompleteRef.current = true; } if (formState === 'pending' && stateRef.current !== 'pending') { activeElementRef.current = document.activeElement; clear(); nowRef.current = Date.now(); hadCompleteRef.current = false; setState('pending'); } else if (stateRef.current === 'pending') { const offset = Math.max(Date.now() - nowRef.current); const delay = isTest ? minimum : Math.max(minimum - offset, 0); if (!waitFor) { timeoutRef.current.complete = setTimeout(() => { if (hadCompleteRef.current) { setState('complete'); } window.requestAnimationFrame(() => { activeElementRef.current?.focus?.(); }); }, delay); timeoutRef.current.reset = setTimeout(() => { nowRef.current = 0; setState(undefined); clear(); }, delay + minimum); } } if (stateRef.current === 'pending') { timeoutRef.current.timeout = setTimeout(() => { clear(); setState(undefined); onTimeout?.(); }, asyncSubmitTimeout !== null && asyncSubmitTimeout !== void 0 ? asyncSubmitTimeout : 30000); } return clear; }, [clear, minimumAsyncBehaviorTime, formState, setState, waitFor, asyncSubmitTimeout, onTimeout]); return { bufferedFormState: stateRef.current }; } export const clearedData = Object.freeze({}); //# sourceMappingURL=Provider.js.map