Skip to content

Commit

Permalink
Improved message value replacements
Browse files Browse the repository at this point in the history
  • Loading branch information
henit committed Nov 15, 2023
1 parent 9582c64 commit f7faba1
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<DataContext.Provider schema={schema} data={{ val: 'abc' }}>
<TestField
path="/val"
errorMessages={{
minLength: 'Minimum {minLength} chars.',
}}
/>
</DataContext.Provider>
)

expect(screen.getByText('Minimum 7 chars.')).toBeInTheDocument()
})
})
39 changes: 23 additions & 16 deletions packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -145,7 +146,9 @@ export default function useDataValue<
// - Local errors are errors based on validation instructions received by
const localErrorRef = useRef<Error | FormError | undefined>()
// - Context errors are from outer contexts, like validation for this field as part of the whole data set
const contextErrorRef = useRef<Error | FormError | undefined>()
const contextErrorRef = useRef<Error | FormError | undefined>(
dataContextError
)

const showErrorRef = useRef<boolean>(Boolean(showErrorInitially))
const errorMessagesRef = useRef(errorMessages)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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 ??
Expand Down
15 changes: 13 additions & 2 deletions packages/dnb-eufemia/src/extensions/forms/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { JSONSchema7 } from 'json-schema'
import { SpacingProps } from '../../components/space/types'

type ValidationRule = string | string[]
type MessageValues = Record<string, string>

interface IFormErrorOptions {
validationRule?: string | string[]
validationRule?: ValidationRule
messageValues?: MessageValues
}

/**
Expand All @@ -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
}
}
}
Expand Down
29 changes: 29 additions & 0 deletions packages/dnb-eufemia/src/extensions/forms/utils/ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit f7faba1

Please sign in to comment.