Skip to content

Commit

Permalink
fix(Forms): provide connectWithPath in validator and onBlurValidator
Browse files Browse the repository at this point in the history
Update packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx
  • Loading branch information
tujoworker committed Sep 4, 2024
1 parent 169f0d6 commit d5037d8
Show file tree
Hide file tree
Showing 14 changed files with 1,047 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ const validator = (value) => {
render(<Field.PhoneNumber validator={validator} />)
```

## Reuse existing error messages in a validator function

You can reuse existing error messages in a validator function. The types of error messages available depend on the field type.

For example, you can reuse the `required` error message in a validator function:

```tsx
const validator = (value, { errorMessages }) => {
// Your validation logic
return new Error(errorMessages.required)
}
render(<Field.String validator={validator} />)
```

### FormError object

You can use the JavaScript `Error` object to display a custom error message:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export const ValidationFunction = () => {
const fnr = (value: string) =>
value.length >= 11 ? { status: 'valid' } : { status: 'invalid' }

const validator = (value, errorMessages) => {
const validator = (value, { errorMessages }) => {
const result = fnr(value)
return result.status === 'invalid'
? new Error(errorMessages.pattern)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,9 @@ const schema = {
<Field.PhoneNumber schema={schema} />
```

#### validator
#### onBlurValidator and validator

The `validator` (including `onBlurValidator`) property is a function that takes the current value of the field as an argument and returns an error message if the value is invalid:
The `onBlurValidator` and `validator` properties accepts a function that takes the current value of the field as an argument and returns an error message if the value is invalid:

```tsx
const validator = (value) => {
Expand All @@ -366,6 +366,44 @@ render(<Field.PhoneNumber validator={validator} />)

You can find more info about error messages in the [Error messages](/uilib/extensions/forms/Form/error-messages/) docs.

##### 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:

```tsx
import { Form, Field } from '@dnb/eufemia/extensions/forms'

const validator = (value, { connectWithPath }) => {
const { getValue } = connectWithPath('/myReference')
const amount = getValue()
if (amount >= value) {
return new Error(`The amount should be greater than ${amount}`)
}
}

render(
<Form.Handler>
<Field.Number path="/myReference" defaultValue={2} />

<Field.Number
path="/withValidator"
defaultValue={2}
validator={validator} // NB: You may use "onBlurValidator" depending on use case.

//
/>
</Form.Handler>,
)
```

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:

- `validateInitially` Will run the validator initially.
- `continuousValidation` Will run the validator on every change, including when the connected field changes.
- `validateUnchanged` Is a combination of `validateInitially` and `continuousValidation`.

##### Async validation

Async validation is also supported. The validator function can return a promise (async/await) that resolves to an error message.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ type HandleSubmitProps = {

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

export type FilterDataHandler<Data> = (
Expand Down Expand Up @@ -66,6 +66,7 @@ export interface ContextState {
hasContext: boolean
/** The dataset for the form / form wizard */
data: any
internalDataRef?: React.MutableRefObject<any>
/** Should the form validate data before submitting? */
errors?: Record<string, Error>
/** Will set autoComplete="on" on each nested Field.String and Field.Number */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,12 @@ export default function Provider<Data extends JsonObject>(
} else {
onPathChange?.(path, value)
}

for (const itm of fieldEventListenersRef.current) {
if (itm.type === 'onPathChange' && itm.path === path) {
itm.callback({ value })
}
}
},
[onPathChange, updateDataValue]
)
Expand Down Expand Up @@ -1182,6 +1188,7 @@ export default function Provider<Data extends JsonObject>(
/** Additional */
id,
data: internalDataRef.current,
internalDataRef,
props,
...rest,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ export type Props = StringFieldProps & {
}

function NationalIdentityNumber(props: Props) {
const translations = useTranslation().NationalIdentityNumber
const errorMessage = translations.errorRequired

const { validate = true, omitMask } = props

const translations = useTranslation().NationalIdentityNumber
const { label, errorRequired, errorFnr, errorDnr } = translations
const errorMessages = useErrorMessage(props.path, props.errorMessages, {
required: errorMessage,
pattern: errorMessage,
required: errorRequired,
pattern: errorRequired,
errorFnr,
errorDnr,
})

const mask = useMemo(
Expand Down Expand Up @@ -49,11 +50,11 @@ function NationalIdentityNumber(props: Props) {
new RegExp(validationPattern).test(value) &&
fnr(value).status === 'invalid'
) {
return Error(translations.errorFnr)
return Error(errorFnr)
}
return undefined
},
[translations.errorFnr]
[errorFnr]
)

const dnrValidator = useCallback(
Expand All @@ -63,11 +64,11 @@ function NationalIdentityNumber(props: Props) {
new RegExp(validationPattern).test(value) &&
dnr(value).status === 'invalid'
) {
return Error(translations.errorDnr)
return Error(errorDnr)
}
return undefined
},
[translations.errorDnr]
[errorDnr]
)

const dnrAndFnrValidator = useCallback(
Expand All @@ -85,7 +86,7 @@ function NationalIdentityNumber(props: Props) {
: validate && !props.validator
? validationPattern
: undefined,
label: props.label ?? translations.label,
label: props.label ?? label,
errorMessages,
mask,
width: props.width ?? 'medium',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,19 @@ describe('Field.NationalIdentityNumber', () => {
)

expect(validator).toHaveBeenCalledTimes(1)
expect(validator).toHaveBeenCalledWith('123', {
maxLength: expect.stringContaining('{maxLength}'),
minLength: expect.stringContaining('{minLength}'),
pattern: expect.stringContaining('11'),
required: expect.stringContaining('11'),
})
expect(validator).toHaveBeenCalledWith(
'123',
expect.objectContaining({
errorMessages: expect.objectContaining({
maxLength: expect.stringContaining('{maxLength}'),
minLength: expect.stringContaining('{minLength}'),
pattern: expect.stringContaining('11'),
required: expect.stringContaining('11'),
errorDnr: expect.stringContaining('d-nummer'),
errorFnr: expect.stringContaining('fødselsnummer'),
}),
})
)
})

it('should have numeric input mode', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import React from 'react'
import { axeComponent, wait } from '../../../../../core/jest/jestSetup'
import { screen, render, fireEvent, act } from '@testing-library/react'
import {
screen,
render,
fireEvent,
act,
waitFor,
} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Field, FieldBlock, Form, JSONSchema } from '../../..'
import { Provider } from '../../../../../shared'
Expand Down Expand Up @@ -467,8 +473,7 @@ describe('Field.Number', () => {
</Form.Handler>
)

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

expect(onSubmit).toHaveBeenCalledTimes(1)
expect(onSubmit).toHaveBeenCalledWith(
Expand Down Expand Up @@ -519,6 +524,52 @@ describe('Field.Number', () => {
expect(screen.queryByRole('alert')).toBeInTheDocument()
})

it('should call validator with validateInitially', async () => {
const validator = jest.fn(() => {
return new Error('Validator message')
})

render(
<Field.Number
validator={validator}
defaultValue={123}
validateInitially
/>
)

expect(validator).toHaveBeenCalledTimes(1)
expect(validator).toHaveBeenCalledWith(123, expect.anything())

await waitFor(() => {
expect(screen.queryByRole('alert')).toBeInTheDocument()
})
})

it('should call validator on form submit', async () => {
const validator = jest.fn(() => {
return new Error('Validator message')
})

render(
<Form.Handler>
<Field.Number
path="/myNumber"
validator={validator}
defaultValue={123}
/>
</Form.Handler>
)

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

expect(validator).toHaveBeenCalledTimes(1)
expect(validator).toHaveBeenCalledWith(123, expect.anything())

await waitFor(() => {
expect(screen.queryByRole('alert')).toBeInTheDocument()
})
})

describe('validation based on required-prop', () => {
it('should show error for empty value', async () => {
render(<Field.Number value={1} required />)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Field } from '../../..'
import { useCallback } from 'react'
import { Field, Form, UseFieldProps } from '../../..'
import { Flex } from '../../../../../components'

export default {
Expand Down Expand Up @@ -42,3 +43,46 @@ export const Number = () => {
</Flex.Stack>
)
}

export const WithFreshValidator = () => {
const validator: UseFieldProps<number>['validator'] = useCallback(
(num, { connectWithPath }) => {
const { getValue } = connectWithPath('/refValue')
const amount = getValue()
// console.log('amount', amount, amount >= num)
if (amount >= num) {
return new Error(`The amount should be greater than ${amount}`)
}
if (num === undefined) {
return new Error(`No amount was given`)
}
},
[]
)

return (
<Form.Handler
defaultData={{ refValue: 2 }}
onSubmit={() => {
console.log('onSubmit 🍏')
}}
>
<Flex.Stack>
<Field.Number label="Ref" path="/refValue" />

<Field.Number
label="Num"
// onBlurValidator={validator}
validator={validator}
defaultValue={2}
// validateInitially
// continuousValidation
// validateUnchanged
path="/myNumberWithValidator"
/>

<Form.SubmitButton />
</Flex.Stack>
</Form.Handler>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -649,10 +649,15 @@ describe('Field.PhoneNumber', () => {
)

expect(validator).toHaveBeenCalledTimes(1)
expect(validator).toHaveBeenCalledWith('+41 9999', {
pattern: enGB.PhoneNumber.errorRequired,
required: enGB.PhoneNumber.errorRequired,
})
expect(validator).toHaveBeenCalledWith(
'+41 9999',
expect.objectContaining({
errorMessages: expect.objectContaining({
pattern: enGB.PhoneNumber.errorRequired,
required: enGB.PhoneNumber.errorRequired,
}),
})
)

await waitFor(() => {
expect(document.querySelector('[role="alert"]')).toBeInTheDocument()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,6 @@ describe('Form.Handler', () => {
)

const buttonElement = document.querySelector('button')

fireEvent.click(buttonElement)

await waitFor(() => {
Expand All @@ -823,15 +822,20 @@ describe('Form.Handler', () => {
filterData: expect.any(Function),
}
)
})

expect(asyncValidator).toHaveBeenCalledTimes(1)
expect(asyncValidator).toHaveBeenCalledWith('bar', {
maxLength: expect.any(String),
minLength: expect.any(String),
pattern: expect.any(String),
required: expect.any(String),
expect(asyncValidator).toHaveBeenCalledTimes(1)
expect(asyncValidator).toHaveBeenCalledWith(
'bar',
expect.objectContaining({
errorMessages: expect.objectContaining({
maxLength: expect.any(String),
minLength: expect.any(String),
pattern: expect.any(String),
required: expect.any(String),
}),
})
})
)
})

it('should accept custom minimumAsyncBehaviorTimevalue', async () => {
Expand Down
Loading

0 comments on commit d5037d8

Please sign in to comment.