Skip to content

Commit

Permalink
fix(Forms): enhance transformIn and transformOut to support changed a…
Browse files Browse the repository at this point in the history
…rray and object instances (#4392)

Fixes #4366
  • Loading branch information
tujoworker authored Dec 13, 2024
1 parent c2c2a7b commit ae4648a
Show file tree
Hide file tree
Showing 17 changed files with 519 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,21 @@ const MyComponent = () => {

### TransformIn and TransformOut

You can use the `transformIn` and `transformOut` to transform the value before it gets displayed in the field and before it gets sent to the form. The second parameter is the country object. You may have a look at the demo below to see how it works.
You can use the `transformIn` and `transformOut` to transform the value before it gets displayed in the field and before it gets sent to the form context. The second parameter is the country object. You may have a look at the demo below to see how it works.

```tsx
const transformOut = (value, country) => {
if (value) {
return `${country.name} (${value})`
import type { CountryType } from '@dnb/eufemia/extensions/forms/Field/SelectCountry'

// From the Field (internal value) to the data context or event parameter
const transformOut = (internal: string, country: CountryType) => {
if (internal) {
return `${country.name} (${internal})`
}
}
const transformIn = (value) => {
return String(value).match(/\((.*)\)/)?.[1]

// To the Field (from e.g. defaultValue)
const transformIn = (external: unknown) => {
return String(external).match(/\((.*)\)/)?.[1] || 'NO'
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -466,10 +466,16 @@ export function TransformInAndOut() {
return (
<ComponentBox scope={{ Tools }}>
{() => {
// From the Field (internal value) to the data context or event parameter
const transformOut = (value) => {
return { value, foo: 'bar' }
}

// To the Field (from e.g. defaultValue)
const transformIn = (data) => {
if (typeof data === 'string') {
return data
}
return data?.value
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,14 @@ export function TransformInAndOut() {
return (
<ComponentBox scope={{ Tools }}>
{() => {
// From the Field (internal value) to the data context or event parameter
const transformOut = (value, country) => {
if (value) {
return country
}
}

// To the Field (from e.g. defaultValue)
const transformIn = (country) => {
return country?.iso
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,71 @@ async function virusCheck(newFiles) {
return await Promise.all(promises)
}
```

### TransformIn and TransformOut

You can use the `transformIn` and `transformOut` properties to transform the data from the internal format to the external format and vice versa.

```tsx
import { Form, Field, Tools } from '@dnb/eufemia/extensions/forms'
import type {
UploadValue,
UploadFileNative,
} from '@dnb/eufemia/extensions/forms/Field/Upload'

// Our external format
type DocumentMetadata = {
id: string
fileName: string
}

const defaultValue = [
{
id: '1234',
fileName: 'myFile.pdf',
},
] satisfies DocumentMetadata[] as unknown as UploadValue

const filesCache = new Map<string, File>()

// To the Field (from e.g. defaultValue)
const transformIn = (external?: DocumentMetadata[]) => {
return (
external?.map(({ id, fileName }) => {
const file: File =
filesCache.get(id) ||
new File([], fileName, { type: 'images/png' })

return { id, file }
}) || []
)
}

// From the Field (internal value) to the data context or event parameter
const transformOut = (internal?: UploadValue) => {
return (
internal?.map(({ id, file }) => {
if (!filesCache.has(id)) {
filesCache.set(id, file)
}

return { id, fileName: file.name }
}) || []
)
}

function MyForm() {
return (
<Form.Handler>
<Field.Upload
path="/documents"
transformIn={transformIn}
transformOut={transformOut}
defaultValue={defaultValue}
/>

<Tools.Log />
</Form.Handler>
)
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -284,20 +284,20 @@ You may check out an [interactive example](/uilib/extensions/forms/Form/Handler/

#### Transforming data

Each [field](/uilib/extensions/forms/all-fields/) and [value](/uilib/extensions/forms/Value/) component supports transformer functions. These functions allow you to transform a value before it is processed into the form data object and vice versa:
Each [Field.\*](/uilib/extensions/forms/all-fields/) and [Value.\*](/uilib/extensions/forms/Value/) component supports transformer functions.

These functions allow you to manipulate the field value to a different format than it uses internally and vice versa.

```tsx
<Field.String
path="/myField"
transformIn={transformIn}
transformOut={transformOut}
transformIn={transformIn}
/>
```

This allows you to show a value in a different format than it is stored in the form data object.

- `transformIn` (in to the field or value) transforms the internal value before it is displayed.
- `transformOut` (out of the field) transforms the internal value before it gets forwarded to the data context or returned as e.g. the `onChange` value parameter.
- `transformOut` (out of the `Field.*` component) transforms the internal value before it gets forwarded to the data context or returned as e.g. the `onChange` value parameter.
- `transformIn` (in to the `Field.*` or `Value.*` component) transforms the external value before it is displayed and used internally.

<Examples.Transformers />

Expand All @@ -311,14 +311,18 @@ You can achieve this by using the `transformIn` and `transformOut` functions:

```tsx
import { Field } from '@dnb/eufemia/extensions/forms'
import type { CountryType } from '@dnb/eufemia/extensions/forms/Field/SelectCountry'

const transformOut = (value, country) => {
if (value) {
// From the Field (internal value) to the data context or event parameter
const transformOut = (internal, country) => {
if (internal) {
return country
}
}
const transformIn = (country) => {
return country?.iso

// To the Field (from e.g. defaultValue)
const transformIn = (external: CountryType) => {
return external?.iso || 'NO'
}

const MyForm = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type CountryFilterSet =
| 'Nordic'
| 'Europe'
| 'Prioritized'
export type { CountryType }

export type Props = FieldPropsWithExtraValue<
string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,11 +391,11 @@ describe('Field.SelectCountry', () => {
return `${country.name} (${value})`
}
})
const transformIn = jest.fn((value) => {
return String(value).match(/\((.*)\)/)?.[1]
const transformIn = jest.fn((external) => {
return String(external).match(/\((.*)\)/)?.[1] || external
})
const valueTransformIn = jest.fn((value) => {
return String(value).match(/\((.*)\)/)?.[1]
const valueTransformIn = jest.fn((internal) => {
return String(internal).match(/\((.*)\)/)?.[1]
})

const onSubmit = jest.fn()
Expand Down Expand Up @@ -434,7 +434,7 @@ describe('Field.SelectCountry', () => {
}

expect(transformOut).toHaveBeenCalledTimes(1)
expect(transformIn).toHaveBeenCalledTimes(4)
expect(transformIn).toHaveBeenCalledTimes(3)
expect(valueTransformIn).toHaveBeenCalledTimes(2)

const firstItemElement = () =>
Expand All @@ -452,7 +452,7 @@ describe('Field.SelectCountry', () => {
)

expect(transformOut).toHaveBeenCalledTimes(1)
expect(transformIn).toHaveBeenCalledTimes(5)
expect(transformIn).toHaveBeenCalledTimes(4)
expect(valueTransformIn).toHaveBeenCalledTimes(3)

expect(input).toHaveValue('Norge')
Expand All @@ -468,7 +468,7 @@ describe('Field.SelectCountry', () => {
expect(value).toHaveTextContent('Sveits')

expect(transformOut).toHaveBeenCalledTimes(4)
expect(transformIn).toHaveBeenCalledTimes(8)
expect(transformIn).toHaveBeenCalledTimes(6)
expect(valueTransformIn).toHaveBeenCalledTimes(4)

fireEvent.submit(form)
Expand All @@ -479,23 +479,21 @@ describe('Field.SelectCountry', () => {
)

expect(transformOut).toHaveBeenCalledTimes(4)
expect(transformIn).toHaveBeenCalledTimes(9)
expect(transformIn).toHaveBeenCalledTimes(7)
expect(valueTransformIn).toHaveBeenCalledTimes(5)

expect(transformOut).toHaveBeenNthCalledWith(1, 'NO', NO)
expect(transformOut).toHaveBeenNthCalledWith(2, 'NO', NO)
expect(transformOut).toHaveBeenNthCalledWith(3, 'CH', CH)
expect(transformOut).toHaveBeenNthCalledWith(4, 'CH', CH)

expect(transformIn).toHaveBeenNthCalledWith(1, undefined)
expect(transformIn).toHaveBeenNthCalledWith(2, undefined)
expect(transformIn).toHaveBeenNthCalledWith(1, 'NO')
expect(transformIn).toHaveBeenNthCalledWith(2, 'NO')
expect(transformIn).toHaveBeenNthCalledWith(3, 'Norge (NO)')
expect(transformIn).toHaveBeenNthCalledWith(4, 'Norge (NO)')
expect(transformIn).toHaveBeenNthCalledWith(5, 'Norge (NO)')
expect(transformIn).toHaveBeenNthCalledWith(6, 'Norge (NO)')
expect(transformIn).toHaveBeenNthCalledWith(6, 'Sveits (CH)')
expect(transformIn).toHaveBeenNthCalledWith(7, 'Sveits (CH)')
expect(transformIn).toHaveBeenNthCalledWith(8, 'Sveits (CH)')
expect(transformIn).toHaveBeenNthCalledWith(9, 'Sveits (CH)')

expect(valueTransformIn).toHaveBeenNthCalledWith(1, undefined)
expect(valueTransformIn).toHaveBeenNthCalledWith(2, 'Norge (NO)')
Expand All @@ -504,6 +502,121 @@ describe('Field.SelectCountry', () => {
expect(valueTransformIn).toHaveBeenNthCalledWith(5, 'Sveits (CH)')
})

it('should support "transformIn" and "transformOut" when value is given by the data context', async () => {
const transformOut = jest.fn((value, country) => {
if (value) {
return `${country.name} (${value})`
}
})
const transformIn = jest.fn((external) => {
return String(external).match(/\((.*)\)/)?.[1]
})
const valueTransformIn = jest.fn((internal) => {
return String(internal).match(/\((.*)\)/)?.[1]
})

const onSubmit = jest.fn()

render(
<Form.Handler
onSubmit={onSubmit}
defaultData={{ country: 'Norge (NO)' }}
>
<Field.SelectCountry
path="/country"
transformIn={transformIn}
transformOut={transformOut}
/>

<Value.SelectCountry
path="/country"
transformIn={valueTransformIn}
/>
</Form.Handler>
)

const NO = {
cdc: '47',
continent: 'Europe',
i18n: { en: 'Norway', nb: 'Norge' },
iso: 'NO',
name: 'Norge',
regions: ['Scandinavia', 'Nordic'],
}

const CH = {
cdc: '41',
continent: 'Europe',
i18n: { en: 'Switzerland', nb: 'Sveits' },
iso: 'CH',
name: 'Sveits',
}

expect(transformOut).toHaveBeenCalledTimes(0)
expect(transformIn).toHaveBeenCalledTimes(1)
expect(valueTransformIn).toHaveBeenCalledTimes(1)

const firstItemElement = () =>
document.querySelectorAll('li.dnb-drawer-list__option')[0]

const form = document.querySelector('form')
const input = document.querySelector('input')
const value = document.querySelector('.dnb-forms-value-block__content')

fireEvent.submit(form)
expect(onSubmit).toHaveBeenCalledTimes(1)
expect(onSubmit).toHaveBeenLastCalledWith(
{ country: 'Norge (NO)' },
expect.anything()
)

expect(transformOut).toHaveBeenCalledTimes(0)
expect(transformIn).toHaveBeenCalledTimes(2)
expect(valueTransformIn).toHaveBeenCalledTimes(2)

expect(input).toHaveValue('Norge')
expect(value).toHaveTextContent('Norge')

await userEvent.type(input, '{Backspace>10}Sveits')
await waitFor(() => {
expect(firstItemElement()).toBeInTheDocument()
})
await userEvent.click(firstItemElement())

expect(input).toHaveValue('Sveits')
expect(value).toHaveTextContent('Sveits')

expect(transformOut).toHaveBeenCalledTimes(3)
expect(transformIn).toHaveBeenCalledTimes(4)
expect(valueTransformIn).toHaveBeenCalledTimes(3)

fireEvent.submit(form)
expect(onSubmit).toHaveBeenCalledTimes(2)
expect(onSubmit).toHaveBeenLastCalledWith(
{ country: 'Sveits (CH)' },
expect.anything()
)

expect(transformOut).toHaveBeenCalledTimes(3)
expect(transformIn).toHaveBeenCalledTimes(5)
expect(valueTransformIn).toHaveBeenCalledTimes(4)

expect(transformOut).toHaveBeenNthCalledWith(1, 'NO', NO)
expect(transformOut).toHaveBeenNthCalledWith(2, 'CH', CH)
expect(transformOut).toHaveBeenNthCalledWith(3, 'CH', CH)

expect(transformIn).toHaveBeenNthCalledWith(1, 'Norge (NO)')
expect(transformIn).toHaveBeenNthCalledWith(2, 'Norge (NO)')
expect(transformIn).toHaveBeenNthCalledWith(3, 'Norge (NO)')
expect(transformIn).toHaveBeenNthCalledWith(4, 'Sveits (CH)')
expect(transformIn).toHaveBeenNthCalledWith(5, 'Sveits (CH)')

expect(valueTransformIn).toHaveBeenNthCalledWith(1, 'Norge (NO)')
expect(valueTransformIn).toHaveBeenNthCalledWith(2, 'Norge (NO)')
expect(valueTransformIn).toHaveBeenNthCalledWith(3, 'Sveits (CH)')
expect(valueTransformIn).toHaveBeenNthCalledWith(4, 'Sveits (CH)')
})

it('should store "displayValue" in data context', async () => {
let dataContext = null

Expand Down
Loading

0 comments on commit ae4648a

Please sign in to comment.