diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useDataValue.test.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useDataValue.test.tsx index 2f9533ea218..7c0d904f044 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useDataValue.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useDataValue.test.tsx @@ -163,5 +163,54 @@ describe('useDataValue', () => { expect(onFocus).toHaveBeenCalledTimes(2) expect(onBlur).toHaveBeenCalledTimes(2) }) + + it('should update the internal value and run error validation', async () => { + const onFocus = jest.fn() + const onBlur = jest.fn() + const onChange = jest.fn() + + const { result } = renderHook(() => + useDataValue({ + value: 'foo', + emptyValue: '', + onFocus, + onBlur, + onChange, + required: true, + }) + ) + + const { handleFocus, handleBlur, updateValue } = result.current + + act(() => { + handleFocus() + handleBlur() + updateValue('') + }) + + expect(onFocus).toHaveBeenLastCalledWith('foo') + expect(onBlur).toHaveBeenLastCalledWith('foo') + + await waitFor(() => { + expect(result.current.error).toBeInstanceOf(Error) + }) + + act(() => { + handleFocus() + updateValue('a') + handleBlur() + }) + + await waitFor(() => { + expect(result.current.error).toBeUndefined() + }) + + expect(onFocus).toHaveBeenLastCalledWith('') + expect(onBlur).toHaveBeenLastCalledWith('a') + + expect(onChange).toHaveBeenCalledTimes(0) + expect(onFocus).toHaveBeenCalledTimes(2) + expect(onBlur).toHaveBeenCalledTimes(2) + }) }) }) diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts index f2a6d6943a0..fd356efaf04 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts @@ -373,14 +373,6 @@ export default function useDataValue< const handleBlur = useCallback(() => setHasFocus(false), [setHasFocus]) const updateValue = useCallback( - (argFromInput) => { - valueRef.current = fromInput(argFromInput) - forceUpdate() - }, - [fromInput] - ) - - const handleChange = useCallback( (argFromInput) => { const newValue = fromInput(argFromInput) @@ -389,8 +381,8 @@ export default function useDataValue< // calling onChange even if the actual value did not change. return } + valueRef.current = newValue - changedRef.current = true if ( continuousValidation || @@ -407,31 +399,52 @@ export default function useDataValue< // Always validate the value immediately when it is changed validateValue() - onChange?.(newValue) if (path) { dataContextHandlePathChange?.(path, newValue) } + + forceUpdate() + }, + [ + continuousValidation, + dataContextHandlePathChange, + fromInput, + hideError, + path, + showError, + validateValue, + ] + ) + + const handleChange = useCallback( + (argFromInput) => { + const newValue = fromInput(argFromInput) + + if (newValue === valueRef.current) { + // Avoid triggering a change if the value was not actually changed. This may be caused by rendering components + // calling onChange even if the actual value did not change. + return + } + + updateValue(argFromInput) + + changedRef.current = true + onChange?.(newValue) + if (elementPath) { const iterateValuePath = `/${iterateElementIndex}${ elementPath && elementPath !== '/' ? elementPath : '' }` handleIterateElementChange?.(iterateValuePath, newValue) } - forceUpdate() }, [ - path, elementPath, + fromInput, + handleIterateElementChange, iterateElementIndex, - continuousValidation, onChange, - validateValue, - dataContextHandlePathChange, - showError, - hideError, - handleIterateElementChange, - fromInput, - forceUpdate, + updateValue, ] )