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): enhance transformIn and transformOut to support changed array and object instances #4392

Merged
merged 1 commit into from
Dec 13, 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 @@ -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
Loading