diff --git a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/FieldBlock.test.tsx b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/FieldBlock.test.tsx index 5f881d0fb44..219fb94619a 100644 --- a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/FieldBlock.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/FieldBlock.test.tsx @@ -10,7 +10,7 @@ import { runAnimation, simulateAnimationEnd, } from '../../../../components/height-animation/__tests__/HeightAnimationUtils' -import { Field, Form } from '../..' +import { Field, Form, Validator } from '../..' import nbNO from '../../constants/locales/nb-NO' import enGB from '../../constants/locales/en-GB' @@ -775,6 +775,191 @@ describe('FieldBlock', () => { }) }) + describe('summarize errors', () => { + it('should summarize errors in one FormStatus component', () => { + const MockComponent = () => { + useFieldProps({ + required: true, + validateInitially: true, + }) + + return null + } + + render( + + + + ) + + expect(document.querySelectorAll('.dnb-form-status')).toHaveLength( + 1 + ) + expect( + document.querySelector('.dnb-form-status').textContent + ).toBe( + nb.Field.errorSummary + 'Error message' + nb.Field.errorRequired + ) + }) + + it('should summarize errors for nested FieldBlocks', () => { + const nested = new Error('Nested') + const outer = new Error('Outer') + + const MockComponent = () => { + useFieldProps({ + id: 'unique', + error: nested, + }) + + return content + } + + render( + + + + ) + + expect(document.querySelectorAll('.dnb-form-status')).toHaveLength( + 1 + ) + expect( + document.querySelector('.dnb-form-status').textContent + ).toBe(nb.Field.errorSummary + 'Outer' + 'Nested') + }) + + it('should not summarize errors when "disableStatusSummary" is true', () => { + const nested = new Error('Nested') + const outer = new Error('Outer') + + const MockComponent = () => { + useFieldProps({ + id: 'unique', + error: nested, + }) + + return content + } + + render( + + + + ) + + expect(document.querySelectorAll('.dnb-form-status')).toHaveLength( + 2 + ) + expect( + document.querySelectorAll('.dnb-form-status')[0].textContent + ).toBe('Outer') + expect( + document.querySelectorAll('.dnb-form-status')[1].textContent + ).toBe('Nested') + }) + + it('should summarize errors when returned in onChangeValidator', () => { + const onChangeValidator: Validator = jest.fn(() => { + return [ + new Error('Error message one'), + new Error('Error message two'), + ] + }) + + render( + + ) + + expect( + document.querySelector('.dnb-form-status').textContent + ).toBe( + nb.Field.errorSummary + 'Error message one' + 'Error message two' + ) + }) + + it('should summarize errors when returned in onBlurValidator', () => { + const onBlurValidator: Validator = jest.fn(() => { + return [ + new Error('Error message one'), + new Error('Error message two'), + ] + }) + + render( + + ) + + expect( + document.querySelector('.dnb-form-status').textContent + ).toBe( + nb.Field.errorSummary + 'Error message one' + 'Error message two' + ) + }) + + it('should summarize errors when returned in async onChangeValidator', async () => { + const onChangeValidator: Validator = jest.fn(async () => { + return [ + new Error('Error message one'), + new Error('Error message two'), + ] + }) + + render( + + ) + + await waitFor(() => { + expect( + document.querySelector('.dnb-form-status').textContent + ).toBe( + nb.Field.errorSummary + + 'Error message one' + + 'Error message two' + ) + }) + }) + + it('should summarize errors when returned in async onBlurValidator', async () => { + const onBlurValidator: Validator = jest.fn(async () => { + return [ + new Error('Error message one'), + new Error('Error message two'), + ] + }) + + render( + + ) + + await waitFor(() => { + expect( + document.querySelector('.dnb-form-status').textContent + ).toBe( + nb.Field.errorSummary + + 'Error message one' + + 'Error message two' + ) + }) + }) + }) + describe('FormStatus with animation', () => { initializeTestSetup() @@ -883,81 +1068,6 @@ describe('FieldBlock', () => { }) }) - it('should summarize errors in one FormStatus components', () => { - const MockComponent = () => { - useFieldProps({ - required: true, - validateInitially: true, - }) - - return null - } - - render( - - - - ) - - expect(document.querySelectorAll('.dnb-form-status')).toHaveLength(1) - expect(document.querySelector('.dnb-form-status').textContent).toBe( - nb.Field.errorSummary + 'Error message' + nb.Field.errorRequired - ) - }) - - it('should summarize errors for nested FieldBlocks', () => { - const nested = new Error('Nested') - const outer = new Error('Outer') - - const MockComponent = () => { - useFieldProps({ - id: 'unique', - error: nested, - }) - - return content - } - - render( - - - - ) - - expect(document.querySelectorAll('.dnb-form-status')).toHaveLength(1) - expect(document.querySelector('.dnb-form-status').textContent).toBe( - nb.Field.errorSummary + 'Outer' + 'Nested' - ) - }) - - it('should not summarize errors when "disableStatusSummary" is true', () => { - const nested = new Error('Nested') - const outer = new Error('Outer') - - const MockComponent = () => { - useFieldProps({ - id: 'unique', - error: nested, - }) - - return content - } - - render( - - - - ) - - expect(document.querySelectorAll('.dnb-form-status')).toHaveLength(2) - expect( - document.querySelectorAll('.dnb-form-status')[0].textContent - ).toBe('Outer') - expect( - document.querySelectorAll('.dnb-form-status')[1].textContent - ).toBe('Nested') - }) - describe('fieldState', () => { it('should show indicator when fieldState is set to pending', async () => { render( diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/DataValueWritePropsDocs.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/DataValueWritePropsDocs.ts index 5e43b45ec6f..0f764bb5e4b 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/DataValueWritePropsDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/DataValueWritePropsDocs.ts @@ -77,12 +77,12 @@ export const DataValueWritePropsProperties: PropertiesTableProps = { status: 'optional', }, onChangeValidator: { - doc: 'Custom validator function that is triggered on every change done by the user. The function can be either asynchronous or synchronous. The first parameter is the value, and the second parameter returns an object containing { errorMessages, connectWithPath, validators }.', + doc: 'Custom validator function where you can return `undefined`, `Error`, `FormError` or an Array with either several other validators or several `Error` or `FormError`. It is triggered on every change done by the user. The function can be either asynchronous or synchronous. The first parameter is the value, and the second parameter returns an object containing { errorMessages, connectWithPath, validators }.', type: 'function', status: 'optional', }, onBlurValidator: { - doc: 'Custom validator function that is triggered when the user leaves a field (e.g., blurring a text input or closing a dropdown). The function can be either asynchronous or synchronous. The first parameter is the value, and the second parameter returns an object containing { errorMessages, connectWithPath, validators }.', + doc: 'Custom validator function where you can return `undefined`, `Error`, `FormError` or an Array with either several other validators or several `Error` or `FormError`. It is triggered when the user leaves a field (e.g., blurring a text input or closing a dropdown). The function can be either asynchronous or synchronous. The first parameter is the value, and the second parameter returns an object containing { errorMessages, connectWithPath, validators }.', type: 'function', status: 'optional', }, diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts index 3a3a7beac5c..a505b6a1037 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts @@ -789,9 +789,16 @@ export default function useFieldProps( const result = await validator(value, additionalArgs) if (Array.isArray(result)) { - for (const validator of result) { - if (!hasBeenCalledRef(validator)) { - const result = await callValidatorFnAsync(validator, value) + 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 @@ -799,6 +806,12 @@ export default function useFieldProps( } } + if (errors.length > 0) { + return new FormError('Error', { + errors, + }) + } + callStackRef.current = [] } else { return result @@ -830,9 +843,13 @@ export default function useFieldProps( }) } - for (const validator of result) { - if (!hasBeenCalledRef(validator)) { - const result = callValidatorFnSync(validator, value) + 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 @@ -840,6 +857,12 @@ export default function useFieldProps( } } + if (errors.length > 0) { + return new FormError('Error', { + errors, + }) + } + callStackRef.current = [] } else { return result @@ -856,7 +879,11 @@ export default function useFieldProps( ( method: PersistErrorStateMethod, initiator: ErrorInitiator, - errorArg: Error | FormError | undefined = undefined + errorArg: + | Error + | FormError + | Array + | undefined = undefined ) => { const error = prepareError(errorArg) @@ -945,11 +972,7 @@ export default function useFieldProps( // Don't show the error if the value has changed in the meantime if (unchangedValue) { - persistErrorState( - 'gracefully', - 'onChangeValidator', - result as Error - ) + persistErrorState('gracefully', 'onChangeValidator', result) if ( (validateInitially && !changedRef.current) || @@ -1096,7 +1119,7 @@ export default function useFieldProps( const revealOnBlurValidatorResult = useCallback( ({ result }) => { - persistErrorState('gracefully', 'onBlurValidator', result as Error) + persistErrorState('gracefully', 'onBlurValidator', result) if (isAsync(onBlurValidatorRef.current)) { defineAsyncProcess(undefined) diff --git a/packages/dnb-eufemia/src/extensions/forms/types.ts b/packages/dnb-eufemia/src/extensions/forms/types.ts index 4c941d98000..cfcfcbdb336 100644 --- a/packages/dnb-eufemia/src/extensions/forms/types.ts +++ b/packages/dnb-eufemia/src/extensions/forms/types.ts @@ -33,9 +33,11 @@ export { JSONSchemaType } export type ValidatorReturnSync = | Error + | FormError | undefined | void | Array> + | Array export type ValidatorReturnAsync = | ValidatorReturnSync