Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Forms): show error on every value change when using exported validators #4068

Merged
merged 1 commit into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3377,7 +3377,7 @@ describe('useFieldProps', () => {
expect(screen.queryByRole('alert')).toHaveTextContent('bar')
})
expect(myValidator).toHaveBeenCalledTimes(5)
expect(fooValidator).toHaveBeenCalledTimes(4)
expect(fooValidator).toHaveBeenCalledTimes(5)
expect(barValidator).toHaveBeenCalledTimes(4)
})

Expand Down Expand Up @@ -3427,7 +3427,7 @@ describe('useFieldProps', () => {
expect(screen.queryByRole('alert')).toHaveTextContent('bar')
})
expect(myValidator).toHaveBeenCalledTimes(5)
expect(fooValidator).toHaveBeenCalledTimes(4)
expect(fooValidator).toHaveBeenCalledTimes(5)
expect(barValidator).toHaveBeenCalledTimes(4)
})

Expand Down Expand Up @@ -3525,7 +3525,7 @@ describe('useFieldProps', () => {
})
expect(publicValidator).toHaveBeenCalledTimes(9)
expect(fooValidator).toHaveBeenCalledTimes(1)
expect(barValidator).toHaveBeenCalledTimes(7)
expect(barValidator).toHaveBeenCalledTimes(8)
expect(bazValidator).toHaveBeenCalledTimes(7)
expect(internalValidators).toHaveBeenCalledTimes(0)
})
Expand Down Expand Up @@ -3563,6 +3563,116 @@ describe('useFieldProps', () => {
})
})

it('should show error on every value change', async () => {
const exportedValidator = jest.fn((value) => {
if (value === '1234') {
return Error('Error message')
}
})

const myValidator = jest.fn((value, { validators }) => {
const { exportedValidator } = validators

return [exportedValidator]
})

const MockComponent = (props) => {
return (
<Field.String
label="Label"
onBlurValidator={props.onBlurValidator}
exportValidators={{ exportedValidator }}
/>
)
}

render(<MockComponent onBlurValidator={myValidator} />)

const input = document.querySelector('input')

await userEvent.type(input, '123')
fireEvent.blur(input)

expect(document.querySelector('.dnb-form-status')).toBeNull()

await userEvent.type(input, '4')
fireEvent.blur(input)

expect(exportedValidator).toHaveBeenCalledTimes(2)
expect(myValidator).toHaveBeenCalledTimes(2)
await waitFor(() => {
expect(
document.querySelector('.dnb-form-status')
).toHaveTextContent('Error message')
})

await userEvent.type(input, '{Backspace}4')
fireEvent.blur(input)

expect(exportedValidator).toHaveBeenCalledTimes(3)
expect(myValidator).toHaveBeenCalledTimes(3)
await waitFor(() => {
expect(
document.querySelector('.dnb-form-status')
).toHaveTextContent('Error message')
})
})

it('should support mixed sync and async validators', async () => {
const exportedValidator = jest.fn(async (value) => {
if (value === '1234') {
return Error('Error message')
}
})

const myValidator = jest.fn((value, { validators }) => {
const { exportedValidator } = validators

return [exportedValidator]
})

const MockComponent = (props) => {
return (
<Field.String
label="Label"
onBlurValidator={props.onBlurValidator}
exportValidators={{ exportedValidator }}
/>
)
}

render(<MockComponent onBlurValidator={myValidator} />)

const input = document.querySelector('input')

await userEvent.type(input, '123')
fireEvent.blur(input)

expect(document.querySelector('.dnb-form-status')).toBeNull()

await userEvent.type(input, '4')
fireEvent.blur(input)

expect(exportedValidator).toHaveBeenCalledTimes(1)
expect(myValidator).toHaveBeenCalledTimes(4)
await waitFor(() => {
expect(
document.querySelector('.dnb-form-status')
).toHaveTextContent('Error message')
})

await userEvent.type(input, '{Backspace}4')
fireEvent.blur(input)

expect(exportedValidator).toHaveBeenCalledTimes(2)
expect(myValidator).toHaveBeenCalledTimes(6)
await waitFor(() => {
expect(
document.querySelector('.dnb-form-status')
).toHaveTextContent('Error message')
})
})

it('should only call returned validators (barValidator should not be called)', async () => {
let internalValidators, fooValidator, barValidator, bazValidator

Expand Down Expand Up @@ -3748,7 +3858,7 @@ describe('useFieldProps', () => {
expect(screen.queryByRole('alert')).toHaveTextContent('baz')
})
expect(publicValidator).toHaveBeenCalledTimes(9)
expect(barValidator).toHaveBeenCalledTimes(7)
expect(barValidator).toHaveBeenCalledTimes(8)
expect(bazValidator).toHaveBeenCalledTimes(7)
expect(internalValidators).toHaveBeenCalledTimes(0)
})
Expand Down
44 changes: 31 additions & 13 deletions packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,25 +559,26 @@ export default function useFieldProps<Value, EmptyValue, Props>(
return result
}, [])

const callValidatorFnSync = useCallback(
(
const callValidatorFnAsync = useCallback(
async (
validator: Validator<Value>,
value: Value = valueRef.current
): ReturnType<Validator<Value>> => {
): Promise<ReturnType<Validator<Value>>> => {
if (typeof validator !== 'function') {
return // stop here
return
}

const result = extendWithExportedValidators(
validator,
validator(value, additionalArgs)
await validator(value, additionalArgs)
)

if (Array.isArray(result)) {
for (const validator of result) {
if (!hasBeenCalledRef(validator)) {
const result = callValidatorFnSync(validator, value)
const result = await callValidatorFnAsync(validator, value)
if (result instanceof Error) {
callStackRef.current = []
return result
}
}
Expand All @@ -591,25 +592,37 @@ export default function useFieldProps<Value, EmptyValue, Props>(
[additionalArgs, extendWithExportedValidators, hasBeenCalledRef]
)

const callValidatorFnAsync = useCallback(
async (
const callValidatorFnSync = useCallback(
(
validator: Validator<Value>,
value: Value = valueRef.current
): Promise<ReturnType<Validator<Value>>> => {
): ReturnType<Validator<Value>> => {
if (typeof validator !== 'function') {
return
return // stop here
}

const result = extendWithExportedValidators(
validator,
await validator(value, additionalArgs)
validator(value, additionalArgs)
)

if (Array.isArray(result)) {
const hasAsyncValidator = result.some((validator) =>
isAsync(validator)
)
if (hasAsyncValidator) {
return new Promise((resolve) => {
callValidatorFnAsync(validator, value).then((result) => {
resolve(result)
})
})
}

for (const validator of result) {
if (!hasBeenCalledRef(validator)) {
const result = await callValidatorFnAsync(validator, value)
const result = callValidatorFnSync(validator, value)
if (result instanceof Error) {
callStackRef.current = []
return result
}
}
Expand All @@ -620,7 +633,12 @@ export default function useFieldProps<Value, EmptyValue, Props>(
return result
}
},
[additionalArgs, extendWithExportedValidators, hasBeenCalledRef]
[
additionalArgs,
callValidatorFnAsync,
extendWithExportedValidators,
hasBeenCalledRef,
]
)

/**
Expand Down
Loading