diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/__tests__/Provider.test.tsx b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/__tests__/Provider.test.tsx index 6c2f3dac4ab..96ffacbcac0 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/__tests__/Provider.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/__tests__/Provider.test.tsx @@ -617,4 +617,29 @@ describe('DataContext.Provider', () => { expect(screen.queryByRole('alert')).not.toBeInTheDocument() }) }) + + it('should show provided errorMessages based on outer schema validation with injected value', () => { + const schema: JSONSchema7 = { + type: 'object', + properties: { + val: { + type: 'string', + minLength: 7, + }, + }, + } + + render( + + + + ) + + expect(screen.getByText('Minimum 7 chars.')).toBeInTheDocument() + }) }) diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts index 4565a4c73c4..9a3e886987b 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts @@ -64,6 +64,7 @@ export default function useDataValue< setPathWithError: dataContextSetPathWithError, errors: dataContextErrors, } = dataContext ?? {} + const dataContextError = path ? dataContextErrors?.[path] : undefined const inFieldBlock = Boolean(fieldBlockContext) const { setError: setFieldBlockError, @@ -145,7 +146,9 @@ export default function useDataValue< // - Local errors are errors based on validation instructions received by const localErrorRef = useRef() // - Context errors are from outer contexts, like validation for this field as part of the whole data set - const contextErrorRef = useRef() + const contextErrorRef = useRef( + dataContextError + ) const showErrorRef = useRef(Boolean(showErrorInitially)) const errorMessagesRef = useRef(errorMessages) @@ -184,18 +187,19 @@ export default function useDataValue< return } - if ( - error instanceof FormError && - typeof error.validationRule === 'string' && - errorMessagesRef.current?.[error.validationRule] !== undefined - ) { - const message = errorMessagesRef.current[ - error.validationRule - ].replace( - `{${error.validationRule}}`, - props?.[error.validationRule] - ) - return new FormError(message) + if (error instanceof FormError) { + const message = + (typeof error.validationRule === 'string' && + errorMessagesRef.current?.[error.validationRule]) || + error.message + + const messageWithValues = Object.entries( + error.messageValues ?? {} + ).reduce((message, [key, value]) => { + return message.replace(`{${key}}`, value) + }, message) + + return new FormError(messageWithValues) } return error @@ -306,9 +310,12 @@ export default function useDataValue< forceUpdate() }, [externalValue, validateValue]) - const dataContextError = path ? dataContextErrors?.[path] : undefined useEffect(() => { - contextErrorRef.current = prepareError(dataContextError) + const error = prepareError(dataContextError) + if (errorChanged(error, contextErrorRef.current)) { + contextErrorRef.current = error + forceUpdate() + } }, [dataContextError, prepareError]) useEffect(() => { @@ -444,7 +451,7 @@ export default function useDataValue< value: toInput(valueRef.current), error: !inFieldBlock && showErrorRef.current - ? errorProp ?? localErrorRef.current ?? dataContextError + ? errorProp ?? localErrorRef.current ?? contextErrorRef.current : undefined, autoComplete: props.autoComplete ?? diff --git a/packages/dnb-eufemia/src/extensions/forms/types.ts b/packages/dnb-eufemia/src/extensions/forms/types.ts index c04192d3307..bd50c59243e 100644 --- a/packages/dnb-eufemia/src/extensions/forms/types.ts +++ b/packages/dnb-eufemia/src/extensions/forms/types.ts @@ -1,8 +1,12 @@ import { JSONSchema7 } from 'json-schema' import { SpacingProps } from '../../components/space/types' +type ValidationRule = string | string[] +type MessageValues = Record + interface IFormErrorOptions { - validationRule?: string | string[] + validationRule?: ValidationRule + messageValues?: MessageValues } /** @@ -12,13 +16,20 @@ export class FormError extends Error { /** * What validation rule did the error occur based on? (i.e: minLength, required or maximum) */ - validationRule?: string | string[] + validationRule?: ValidationRule + + /** + * Replacement values relevant for this error. + * @example { minLength: 3 } to be able to replace values in a message like "Minimum {minLength} charactes" + */ + messageValues?: MessageValues constructor(message: string, options?: IFormErrorOptions) { super(message) if (options) { this.validationRule = options.validationRule + this.messageValues = options.messageValues } } } diff --git a/packages/dnb-eufemia/src/extensions/forms/utils/ajv.ts b/packages/dnb-eufemia/src/extensions/forms/utils/ajv.ts index fbfebeec134..b06413dbb90 100644 --- a/packages/dnb-eufemia/src/extensions/forms/utils/ajv.ts +++ b/packages/dnb-eufemia/src/extensions/forms/utils/ajv.ts @@ -36,9 +36,38 @@ function getValidationRule(ajvError: ErrorObject): string { return ajvError.keyword } +function getMessageValues( + ajvError: ErrorObject +): FormError['messageValues'] { + const validationRule = getValidationRule(ajvError) + + switch (validationRule) { + case 'minLength': + case 'maxLength': + case 'minimum': + case 'maximum': + case 'exclusiveMinimum': + case 'exclusiveMaximum': + return { + [validationRule]: ajvError.params?.limit, + } + case 'multipleOf': + return { + [validationRule]: ajvError.params?.multipleOf, + } + case 'pattern': + return { + [validationRule]: ajvError.params?.pattern, + } + } +} + function ajvErrorToFormError(ajvError: ErrorObject): FormError { const error = new FormError(ajvError.message ?? 'Unknown error', { validationRule: getValidationRule(ajvError), + // Keep the message values in the error object instead of injecting them into the message + // at once, since an error might be validated one place, and then get a new message before it is displayed. + messageValues: getMessageValues(ajvError), }) return error }