Skip to content

Commit

Permalink
fix(Forms): always revalidate on submit when data context changes
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Feb 4, 2025
1 parent 2dfd44a commit 7b4b5a4
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export interface ContextState {
existingFieldsRef?: React.MutableRefObject<Map<Path, boolean>>
formElementRef?: React.MutableRefObject<HTMLFormElement>
fieldErrorRef?: React.MutableRefObject<Record<Path, Error>>
showAllErrors: boolean
showAllErrors: boolean | number
hasVisibleError: boolean
formState: SubmitState
ajvInstance: Ajv
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,9 @@ export default function Provider<Data extends JsonObject>(
const addSetShowAllErrorsRef = useRef<
Array<(showAllErrors: boolean) => void>
>([])
const showAllErrorsRef = useRef<boolean>(false)
const showAllErrorsRef = useRef<number | boolean>(false)
const setShowAllErrors = useCallback((showAllErrors: boolean) => {
showAllErrorsRef.current = showAllErrors
showAllErrorsRef.current = showAllErrors ? Date.now() : showAllErrors
forceUpdate()
addSetShowAllErrorsRef.current.forEach((fn) => fn?.(showAllErrors))
}, [])
Expand Down Expand Up @@ -1420,7 +1420,7 @@ export default function Provider<Data extends JsonObject>(
globalStatus: {
id: globalStatusId,
title: translation.errorSummaryTitle,
show: showAllErrorsRef.current,
show: Boolean(showAllErrorsRef.current),
},
}
: undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import userEvent from '@testing-library/user-event'
import { spyOnEufemiaWarn, wait } from '../../../../../core/jest/jestSetup'
import { simulateAnimationEnd } from '../../../../../components/height-animation/__tests__/HeightAnimationUtils'
import { GlobalStatus } from '../../../../../components'
import { Button, GlobalStatus } from '../../../../../components'
import SharedProvider from '../../../../../shared/Provider'
import { makeUniqueId } from '../../../../../shared/component-helper'
import { debounceAsync } from '../../../../../shared/helpers/debounce'
Expand Down Expand Up @@ -3412,6 +3412,72 @@ describe('DataContext.Provider', () => {

expect(screen.queryByRole('alert')).toHaveTextContent(errorRequired)
})

it('should hide the error state when the field is set to empty', async () => {
let dataContext = null

const MockComponent = () => {
const { setShowAllErrors } = useContext(DataContext.Context)

return (
<>
<Button
id="show"
onClick={() => {
setShowAllErrors(true)
}}
/>
<Button
id="hide"
onClick={() => {
setShowAllErrors(false)
}}
/>

<DataContext.Consumer>
{(context) => {
dataContext = context
return null
}}
</DataContext.Consumer>
</>
)
}

render(
<Form.Handler>
<MockComponent />
</Form.Handler>
)

expect(dataContext).toMatchObject({ showAllErrors: false })

fireEvent.submit(document.querySelector('form'))
expect(dataContext).toMatchObject({
showAllErrors: false,
})

await userEvent.click(document.querySelector('button#show'))

fireEvent.submit(document.querySelector('form'))
expect(dataContext).toMatchObject({
showAllErrors: expect.any(Number),
})

await userEvent.click(document.querySelector('button#hide'))

fireEvent.submit(document.querySelector('form'))
expect(dataContext).toMatchObject({
showAllErrors: false,
})

await userEvent.click(document.querySelector('button#show'))

fireEvent.submit(document.querySelector('form'))
expect(dataContext).toMatchObject({
showAllErrors: expect.any(Number),
})
})
})

it('should run filterData with correct data in onSubmit', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import {
renderHook,
waitFor,
} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { makeUniqueId } from '../../../../../shared/component-helper'
import { Field, Form } from '../../..'
import { Button } from '../../../../../components'
import useValidation from '../useValidation'
import userEvent from '@testing-library/user-event'

describe('useValidation', () => {
let identifier: string
Expand Down Expand Up @@ -261,6 +262,43 @@ describe('useValidation', () => {
})

describe('with an identifier', () => {
it('should handle the setFormError method outside of the form context', async () => {
const myId = () => null
const onSubmit = jest.fn()

const MockComponent = () => {
const { setFieldStatus } = useValidation(myId)

return (
<Form.Handler id={myId} onSubmit={onSubmit}>
<Field.String
label="My field"
path="/myField"
onChange={(value) => {
if (value === 'error') {
setFieldStatus('/myField', {
error: new Error('Error message'),
})
}
}}
/>
</Form.Handler>
)
}

render(<MockComponent />)

await userEvent.type(document.querySelector('input'), 'error')

fireEvent.submit(document.querySelector('form'))

expect(onSubmit).toHaveBeenCalledTimes(0)

expect(
document.querySelector('.dnb-form-status')
).toBeInTheDocument()
})

it('should set and remove a field error', async () => {
const onSubmit = jest.fn()

Expand Down Expand Up @@ -475,41 +513,53 @@ describe('useValidation', () => {
})
})

it('should handle the setFormError method outside of the form context', async () => {
const myId = () => null
const onSubmit = jest.fn()

it('should hide the error message when the field is set to empty', async () => {
const MockComponent = () => {
const { setFieldStatus } = useValidation(myId)
const { setFieldStatus } = useValidation()

return (
<Form.Handler id={myId} onSubmit={onSubmit}>
<Field.String
label="My field"
path="/myField"
onChange={(value) => {
if (value === 'error') {
setFieldStatus('/myField', {
error: new Error('Error message'),
})
}
<>
<Field.String path="/foo" required />
<Button
onClick={() => {
setFieldStatus('/foo', {
error: undefined,
})
}}
/>
</Form.Handler>
</>
)
}

render(<MockComponent />)
render(
<Form.Handler>
<MockComponent />
</Form.Handler>
)

await userEvent.type(document.querySelector('input'), 'error')
expect(
document.querySelector('.dnb-form-status')
).not.toBeInTheDocument()

fireEvent.submit(document.querySelector('form'))
expect(
document.querySelector('.dnb-form-status')
).toBeInTheDocument()

expect(onSubmit).toHaveBeenCalledTimes(0)
await userEvent.click(document.querySelector('button'))
expect(
document.querySelector('.dnb-form-status')
).not.toBeInTheDocument()

fireEvent.submit(document.querySelector('form'))
expect(
document.querySelector('.dnb-form-status')
).toBeInTheDocument()

await userEvent.click(document.querySelector('button'))
expect(
document.querySelector('.dnb-form-status')
).not.toBeInTheDocument()
})
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function WizardContainer(props: Props) {
const [, forceUpdate] = useReducer(() => ({}), {})
const activeIndexRef = useRef<StepIndex>(initialActiveIndex)
const totalStepsRef = useRef<number>(NaN)
const errorOnStepRef = useRef<Record<StepIndex, boolean>>({})
const errorOnStepRef = useRef<Record<StepIndex, boolean | number>>({})
const elementRef = useRef<HTMLElement>()
const stepElementRef = useRef<HTMLElement>()
const preventNextStepRef = useRef(false)
Expand Down Expand Up @@ -248,7 +248,7 @@ function WizardContainer(props: Props) {

if (!skipErrorCheck) {
// Set the showAllErrors to the step we got to
setShowAllErrors(errorOnStepRef.current[index])
setShowAllErrors(Boolean(errorOnStepRef.current[index]))
}

if (!preventNextStepRef.current && !(result instanceof Error)) {
Expand Down
34 changes: 22 additions & 12 deletions packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -955,12 +955,6 @@ export default function useFieldProps<Value, EmptyValue, Props>(
changedRef.current = state
}, [])

const removeError = useCallback(() => {
setChanged(false)
hideError()
clearErrorState()
}, [clearErrorState, hideError, setChanged])

const validatorCacheRef = useRef({
onChangeValidator: null,
onBlurValidator: null,
Expand Down Expand Up @@ -1328,6 +1322,21 @@ export default function useFieldProps<Value, EmptyValue, Props>(
validateUnchanged,
])

const removeError = useCallback(() => {
// Mark as not changed,
// so the field is considered "fresh" when the user starts typing again.
setChanged(false)

// Hide the error message.
hideError()

// Remove the local error states.
clearErrorState()

// To ensure this field will report back to the context if there are any errors.
validateValue()
}, [clearErrorState, hideError, setChanged, validateValue])

const handleError = useCallback(() => {
if (
validateContinuously ||
Expand Down Expand Up @@ -1483,20 +1492,20 @@ export default function useFieldProps<Value, EmptyValue, Props>(

const handleChangeEventResult = useCallback(async () => {
const result: EventStateObjectWithSuccess =
changeEventResultRef.current
changeEventResultRef.current || ({} as EventStateObjectWithSuccess)

if (typeof result?.error !== 'undefined') {
if (result?.error === null) {
if ('error' in result) {
if (!result.error) {
removeError()
} else {
persistErrorState('gracefully', 'onChangeValidator', result.error)
revealError()
}
}
if (typeof result?.warning !== 'undefined') {
if ('warning' in result) {
warningRef.current = result.warning
}
if (typeof result?.info !== 'undefined') {
if ('info' in result) {
infoRef.current = result.info
}

Expand Down Expand Up @@ -2312,8 +2321,9 @@ export default function useFieldProps<Value, EmptyValue, Props>(
const connections = useMemo(() => {
return {
setEventResult,
emptyValue,
}
}, [setEventResult])
}, [emptyValue, setEventResult])
setFieldConnectionDataContext?.(identifier, connections)

// - Handle htmlAttributes
Expand Down

0 comments on commit 7b4b5a4

Please sign in to comment.