Skip to content

Commit

Permalink
Add support for root path to rerun on ever React re-render
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Sep 5, 2024
1 parent 47741a0 commit 36a11c1
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ You can find more info about error messages in the [Error messages](/uilib/exten

##### Connect with another field

You can also use the `connectWithPath` function to connect the validator to another field. This allows you to rerender the validator function once the value of the connected field changes:
You can also use the `connectWithPath` function to connect the validator to another field. This allows you to rerun the validator function once the value of the connected field changes:

```tsx
import { Form, Field } from '@dnb/eufemia/extensions/forms'
Expand Down Expand Up @@ -396,6 +396,8 @@ render(
)
```

When providing a single slash (`/`) as the path, the validator will rerun on every React rerender cycle.

By default, the validator function will only run when the "/withValidator" field is changed (and blurred). When the error message is shown, it will update the error message with the new value of the "/myReference" field.

You can also change this behaviour by using the following props:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type HandleSubmitProps = {

export type EventListenerCall = {
path?: Path
type?: 'onSubmit' | 'onPathChange'
type?: 'onSubmit' | 'onPathChange' | 'onRerender'
callback: (params?: { value: unknown }) => void | Promise<void | Error>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -876,11 +876,8 @@ export default function Provider<Data extends JsonObject>(

// Just call the submit listeners "once", and not on the retry/recall
if (!skipFieldValidation) {
for (const {
path,
type,
callback,
} of fieldEventListenersRef.current) {
for (const item of fieldEventListenersRef.current) {
const { path, type, callback } = item
if (
type === 'onSubmit' &&
mountedFieldPathsRef.current.includes(path)
Expand Down Expand Up @@ -1072,17 +1069,30 @@ export default function Provider<Data extends JsonObject>(

// Collect listeners to be called during form submit
const fieldEventListenersRef = useRef<Array<EventListenerCall>>([])
const rerenderEventListenersRef = useRef<Array<EventListenerCall>>([])
for (const { type, callback } of rerenderEventListenersRef.current) {
if (type === 'onRerender') {
callback()
}
}
const setFieldEventListener = useCallback(
(
path: EventListenerCall['path'],
type: EventListenerCall['type'],
callback: EventListenerCall['callback']
) => {
fieldEventListenersRef.current =
fieldEventListenersRef.current.filter(({ path: p, type: t }) => {
return !(p === path && t === type)
})
fieldEventListenersRef.current.push({ path, type, callback })
const list =
type === 'onRerender'
? rerenderEventListenersRef.current
: fieldEventListenersRef.current
const index = list.findIndex(({ path: p, type: t, callback: c }) => {
return p === path && t === type && c === callback
})
if (index !== -1) {
list.splice(index, 1, { path, type, callback })
} else {
list.push({ path, type, callback })
}
},
[]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2745,6 +2745,78 @@ describe('useFieldProps', () => {
}

describe('validator', () => {
it('when using the root path it should rerun on every change', () => {
const validator = jest.fn((value, { connectWithPath }) => {
if (value === 12) {
connectWithPath('/')
}
})

const MockComponent = () => {
const [count, increment] = React.useReducer((c) => c + 1, 0)

return (
<Form.Handler>
<Field.Number path="/foo" defaultValue={1} />

<Field.Number
path="/bar"
defaultValue={10}
validator={validator}
/>

<button type="button" onClick={increment}>
{count}
</button>
</Form.Handler>
)
}

render(<MockComponent />)

const [input, inputWithValidator] = Array.from(
document.querySelectorAll('input')
)
const form = document.querySelector('form')
const increment = document.querySelector('button')

fireEvent.change(input, {
target: { value: '2' },
})

expect(validator).toHaveBeenCalledTimes(0)
fireEvent.submit(form)
expect(validator).toHaveBeenCalledTimes(1)

expect(increment).toHaveTextContent('0')
fireEvent.click(increment)
expect(increment).toHaveTextContent('1')
expect(validator).toHaveBeenCalledTimes(1)

fireEvent.change(inputWithValidator, {
target: { value: '11' },
})

expect(validator).toHaveBeenCalledTimes(2)

fireEvent.change(inputWithValidator, {
target: { value: '12' },
})

expect(validator).toHaveBeenCalledTimes(4)

fireEvent.change(input, {
target: { value: '3' },
})

expect(validator).toHaveBeenCalledTimes(5)

fireEvent.click(increment)
expect(increment).toHaveTextContent('2')

expect(validator).toHaveBeenCalledTimes(6)
})

it('should show validator error on form submit', async () => {
const validator = jest.fn(validatorFn)

Expand Down
53 changes: 29 additions & 24 deletions packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ export default function useFieldProps<Value, EmptyValue, Props>(
setFieldError: setFieldErrorDataContext,
setFieldProps: setPropsDataContext,
setHasVisibleError: setHasVisibleErrorDataContext,
setFieldEventListener: setFieldEventListenerDataContext,
handleMountField,
handleUnMountField,
setFieldEventListener,
Expand Down Expand Up @@ -431,33 +430,39 @@ export default function useFieldProps<Value, EmptyValue, Props>(
}, [errorProp])

const { getValueByPath } = useDataValue()
const connectWithPathRef = useRef<
ValidatorAdditionalArgs<Value>['connectWithPath']
>((path) => {
setFieldEventListenerDataContext(path, 'onPathChange', async () => {
let result = undefined
const connectWithPathListenerRef = useRef(async () => {
let result = undefined

if (
localErrorRef.current ||
changedRef.current ||
validateUnchanged ||
continuousValidation
) {
if (validatorRef.current) {
result = await callValidator()
}
if (
path === '/' ||
localErrorRef.current ||
changedRef.current ||
validateUnchanged ||
continuousValidation
) {
if (validatorRef.current) {
result = await callValidator()
}

if (onBlurValidatorRef.current) {
result = await callOnBlurValidator()
}
if (onBlurValidatorRef.current) {
result = await callOnBlurValidator()
}

if (!result) {
changedRef.current = false
hideError()
clearErrorState()
}
if (!result) {
changedRef.current = false
hideError()
clearErrorState()
}
})
}
})
const connectWithPathRef = useRef<
ValidatorAdditionalArgs<Value>['connectWithPath']
>((path) => {
setFieldEventListener?.(
path,
path === '/' ? 'onRerender' : 'onPathChange',
connectWithPathListenerRef.current
)

return {
getValue: () => getValueByPath(path),
Expand Down

0 comments on commit 36a11c1

Please sign in to comment.