diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/info.mdx
index 946aa6c04de..0cd074a197b 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/info.mdx
@@ -5,13 +5,19 @@ hideInMenu: true
## Description
-`Iterate.PushContainer` enables users to create a new item in the array. It can be used instead of the [PushButton](/uilib/extensions/forms/Iterate/PushButton/).
+`Iterate.PushContainer` enables users to create a new item in the array. It can be used instead of the [PushButton](/uilib/extensions/forms/Iterate/PushButton/), but with fields in the container.
It allows the user to fill in the fields without storing them in the data context.
-Fields inside the container must have an `itemPath` defined.
+Good to know:
-You can place it below the [Array](/uilib/extensions/forms/Iterate/Array/) component like this:
+- Fields inside the container must have an `itemPath` defined, instead of a `path`.
+- You can provide `data` or `defaultData` to prefill the fields.
+- The `path` you define needs to point to an array an existing [Iterate.Array](/uilib/extensions/forms/Iterate/Array/) path.
+
+## Usage
+
+You may place it below the [Iterate.Array](/uilib/extensions/forms/Iterate/Array/) component like this:
```tsx
import { Iterate, Field } from '@dnb/eufemia/extensions/forms'
@@ -27,8 +33,6 @@ render(
)
```
-Technically it uses the [EditContainer](/uilib/extensions/forms/Iterate/EditContainer/) and the [Form.Isolation](/uilib/extensions/forms/Form/Isolation/) under the hood.
-
## Show a button to create a new item
By default, it keeps the form open after a new item has been created. You can change this behavior by using the `openButton` and `showOpenButtonWhen` properties.
@@ -84,3 +88,9 @@ render(
,
)
```
+
+## Technical details
+
+Under the hood, it uses the [Form.Isolation](/uilib/extensions/forms/Form/Isolation/) component to isolate the data from the rest of the form. It also uses the the [EditContainer](/uilib/extensions/forms/Iterate/EditContainer/) inside the [Iterate.Array](/uilib/extensions/forms/Iterate/Array/) component to render the fields.
+
+All fields inside the container will be stored in the data context at this path: `/pushContainerItems/0`.
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/properties.mdx
index f1664abf510..516cfa1f8b9 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/properties.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/properties.mdx
@@ -5,12 +5,19 @@ hideInMenu: true
import TranslationsTable from 'dnb-design-system-portal/src/shared/parts/TranslationsTable'
import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable'
-import { PushContainerProperties } from '@dnb/eufemia/src/extensions/forms/Iterate/PushContainer/PushContainerDocs'
+import {
+ PushContainerProperties,
+ PushContainerEvents,
+} from '@dnb/eufemia/src/extensions/forms/Iterate/PushContainer/PushContainerDocs'
## Properties
+## Events
+
+
+
## Translations
diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts b/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts
index ef8edd97246..7b38bd80961 100644
--- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts
@@ -154,6 +154,7 @@ export interface ContextState {
params?: { remove?: boolean }
) => void
setFieldConnection?: (path: Path, connections: FieldConnections) => void
+ isEmptyDataRef?: React.MutableRefObject
fieldPropsRef?: React.MutableRefObject>
valuePropsRef?: React.MutableRefObject>
fieldConnectionsRef?: React.RefObject>
diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx
index 6ad23d97955..325927e8339 100644
--- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx
@@ -303,6 +303,7 @@ export default function Provider(
// eslint-disable-next-line react-hooks/exhaustive-deps -- Avoid triggering code that should only run initially
}, [])
const internalDataRef = useRef(initialData)
+ const isEmptyDataRef = useRef(false)
// - Validator
const ajvValidatorRef = useRef()
@@ -658,8 +659,10 @@ export default function Provider(
) {
cacheRef.current.shared = sharedData.data
- if (internalDataRef.current === clearedData) {
- return clearedData as Data
+ if (isEmptyDataRef.current) {
+ return (
+ Array.isArray(internalDataRef.current) ? [] : clearedData
+ ) as Data
}
return {
@@ -683,7 +686,11 @@ export default function Provider(
: internalData
const clearData = useCallback(() => {
- internalDataRef.current = (emptyData ?? clearedData) as Data
+ isEmptyDataRef.current = true
+ internalDataRef.current = ((typeof emptyData === 'function'
+ ? emptyData(internalDataRef.current)
+ : emptyData) ??
+ (Array.isArray(internalDataRef.current) ? [] : clearedData)) as Data
if (id) {
setSharedData?.(internalDataRef.current)
@@ -691,6 +698,10 @@ export default function Provider(
forceUpdate()
onClear?.()
+
+ requestAnimationFrame?.(() => {
+ isEmptyDataRef.current = false
+ }) // Delay so the field validation error message are not shown
}, [emptyData, id, onClear, setSharedData])
useEffect(() => {
@@ -1329,6 +1340,7 @@ export default function Provider(
id,
data: internalDataRef.current,
internalDataRef,
+ isEmptyDataRef,
props,
...rest,
}}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainer.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainer.tsx
index 58c75f77421..a9050f987c8 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainer.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainer.tsx
@@ -8,7 +8,7 @@ import EditContainer, { CancelButton, DoneButton } from '../EditContainer'
import IterateArray, { ContainerMode } from '../Array'
import OpenButton from './OpenButton'
import { Flex, HeightAnimation } from '../../../../components'
-import { Path } from '../../types'
+import { OnCommit, Path } from '../../types'
import { SpacingProps } from '../../../../shared/types'
import { useArrayLimit, useSwitchContainerMode } from '../hooks'
import Toolbar from '../Toolbar'
@@ -39,20 +39,30 @@ export type Props = {
showOpenButtonWhen?: (list: unknown[]) => boolean
/**
- * Prefilled data to add to the fields.
+ * Prefilled data to add to the fields. The data will be put into this path: "/pushContainerItems/0".
*/
data?: unknown | Record
/**
- * Prefilled data to add to the fields.
+ * Prefilled data to add to the fields. The data will be put into this path: "/pushContainerItems/0".
*/
defaultData?: unknown | Record
+ /**
+ * Provide additional data that will be put into the root of the isolated data context (parallel to "/pushContainerItems/0").
+ */
+ isolatedData?: Record
+
/**
* A custom toolbar to be shown below the container.
*/
toolbar?: React.ReactNode
+ /**
+ * Will be called when the user clicks on the "Done" button.
+ */
+ onCommit?: OnCommit
+
/**
* The container contents.
*/
@@ -65,11 +75,13 @@ function PushContainer(props: AllProps) {
const {
data: dataProp,
defaultData: defaultDataProp,
+ isolatedData,
path,
title,
children,
openButton,
showOpenButtonWhen,
+ onCommit,
...rest
} = props
@@ -101,23 +113,44 @@ function PushContainer(props: AllProps) {
if (defaultDataProp) {
return // don't return a fallback, because we want to use the defaultData
}
- return { newItems: [dataProp ?? clearedData] }
- }, [dataProp, defaultDataProp])
+ return {
+ ...isolatedData,
+ pushContainerItems: [dataProp ?? clearedData],
+ }
+ }, [dataProp, defaultDataProp, isolatedData])
const defaultData = useMemo(() => {
- return { newItems: [defaultDataProp ?? clearedData] }
- }, [defaultDataProp])
+ return {
+ ...(!dataProp ? isolatedData : null),
+ pushContainerItems: [defaultDataProp ?? clearedData],
+ }
+ }, [dataProp, defaultDataProp, isolatedData])
+
+ const emptyData = useCallback(
+ (data: { pushContainerItems: unknown[] }) => {
+ const firstItem = data.pushContainerItems?.[0]
+ if (firstItem === null || typeof firstItem !== 'object') {
+ return {
+ ...isolatedData,
+ pushContainerItems: [null],
+ }
+ }
+ return defaultData
+ },
+ [defaultData, isolatedData]
+ )
return (
{
- return moveValueToPath(path, [...entries, ...newItems])
+ transformOnCommit={({ pushContainerItems }) => {
+ return moveValueToPath(path, [...entries, ...pushContainerItems])
}}
- onCommit={(data, { clearData, preventCommit }) => {
+ onCommit={(data, options) => {
+ const { clearData, preventCommit } = options
if (hasReachedLimit) {
preventCommit()
setShowStatus(true)
@@ -126,11 +159,12 @@ function PushContainer(props: AllProps) {
switchContainerModeRef.current?.('view')
clearData()
}
+ onCommit?.(data, options)
}}
>
{
@@ -548,7 +549,7 @@ describe('PushContainer', () => {
)
})
- it('should keep the defaultValue after clearing', async () => {
+ it('should not show error message after clearing', async () => {
const onChange = jest.fn()
render(
@@ -643,6 +644,68 @@ describe('PushContainer', () => {
expect(document.querySelector('.dnb-form-status')).toBeNull()
})
})
+
+ it('should keep the defaultValue after clearing', async () => {
+ const onChange = jest.fn()
+ const onCommit = jest.fn()
+
+ let internalContext = null
+ const CollectInternalData = () => {
+ internalContext = useContext(DataContext)
+ return null
+ }
+
+ render(
+
+
+
+
+
+
+ )
+
+ expect(internalContext).toMatchObject({
+ data: {
+ pushContainerItems: ['default value'],
+ },
+ })
+
+ const input = document.querySelector('input')
+
+ await userEvent.type(input, ' changed')
+
+ const button = document.querySelector('button')
+
+ await userEvent.click(button)
+ expect(internalContext.internalDataRef.current).toEqual({
+ pushContainerItems: ['default value'],
+ })
+ expect(onChange).toHaveBeenCalledTimes(1)
+ expect(onChange).toHaveBeenLastCalledWith(
+ ['default value changed'],
+ expect.anything()
+ )
+ expect(onCommit).toHaveBeenCalledTimes(1)
+ expect(onCommit).toHaveBeenLastCalledWith(
+ ['default value changed'],
+ expect.anything()
+ )
+
+ await userEvent.click(button)
+ expect(internalContext.internalDataRef.current).toEqual({
+ pushContainerItems: ['default value'],
+ })
+ expect(onChange).toHaveBeenCalledTimes(2)
+ expect(onChange).toHaveBeenLastCalledWith(
+ ['default value changed', 'default value'],
+ expect.anything()
+ )
+ expect(onCommit).toHaveBeenCalledTimes(2)
+ expect(onCommit).toHaveBeenLastCalledWith(
+ ['default value changed', 'default value'],
+ expect.anything()
+ )
+ })
})
it('should support initial data as a string', async () => {
diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/stories/PushContainer.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/stories/PushContainer.stories.tsx
new file mode 100644
index 00000000000..73636f1c885
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/stories/PushContainer.stories.tsx
@@ -0,0 +1,168 @@
+import React, { useLayoutEffect } from 'react'
+import { Field, Form, Iterate, Value, Wizard } from '../..'
+import { Card, Flex } from '../../../../components'
+
+export default {
+ title: 'Eufemia/Extensions/Forms/Iterate/PushContainer',
+}
+
+const formData = {
+ persons: [
+ {
+ firstName: 'Test',
+ lastName: 'Bruker',
+ },
+ {
+ firstName: 'Some',
+ lastName: 'Person',
+ },
+ {
+ firstName: 'Geir',
+ lastName: 'Service',
+ },
+ ],
+}
+
+export const ComplexPushContainer = () => {
+ return (
+
+
+
+
+ )
+}
+
+function RepresentativesSection() {
+ return (
+
+ Representatives
+
+
+
+
+
+
+
+
+ )
+}
+
+function RepresentativesView() {
+ return (
+
+
+
+
+
+
+ )
+}
+
+function RepresentativesEdit() {
+ return (
+
+
+
+
+ )
+}
+
+function ExistingPersonDetails() {
+ const { data, getValue } = Form.useData()
+ const person = getValue(data.selectedPerson)?.data || {}
+
+ return (
+
+
+
+
+ )
+}
+
+function NewPersonDetails() {
+ return (
+
+
+
+
+ )
+}
+
+function PushContainerContent() {
+ const { data, update } = Form.useData()
+
+ // Clear the PushContainer data when the selected person is "other",
+ // so the fields do not inherit existing data.
+ useLayoutEffect(() => {
+ if (data.selectedPerson === 'other') {
+ update('/pushContainerItems/0', {})
+ }
+ }, [data.selectedPerson, update])
+
+ return (
+
+
+
+
+
+ typeof value === 'string' && value !== 'other',
+ }}
+ >
+
+
+
+ value === 'other',
+ }}
+ >
+
+
+
+ )
+}
+
+function RepresentativesCreateNew() {
+ return (
+ {
+ return {
+ title: [data.firstName, data.lastName].join(' '),
+ value: '/persons/' + i,
+ data,
+ }
+ }),
+ }}
+ openButton={
+
+ }
+ showOpenButtonWhen={(list) => list.length > 0}
+ >
+
+
+ )
+}
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
index a382858069b..973ebea13cd 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
@@ -1785,8 +1785,9 @@ export default function useFieldProps(
const isEmptyData = useCallback(
() => {
return (
+ dataContext.isEmptyDataRef?.current ||
dataContext.internalDataRef?.current ===
- (dataContext.props?.emptyData ?? clearedData)
+ (dataContext.props?.emptyData ?? clearedData)
)
},
@@ -1822,11 +1823,8 @@ export default function useFieldProps(
useEffect(() => {
if (isEmptyData()) {
- // Fill the data context with the default value after it has been cleared
- requestAnimationFrame(() => {
- setContextData()
- validateValue()
- })
+ setContextData()
+ validateValue()
}
}, [isEmptyData, setContextData, validateValue])
diff --git a/packages/dnb-eufemia/src/extensions/forms/types.ts b/packages/dnb-eufemia/src/extensions/forms/types.ts
index 87e9ef9c3c3..6ec8f27a635 100644
--- a/packages/dnb-eufemia/src/extensions/forms/types.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/types.ts
@@ -600,7 +600,10 @@ export type OnCommit = (
{
clearData,
preventCommit,
- }: { clearData: () => void; preventCommit?: () => void }
+ }: {
+ clearData: () => void
+ preventCommit?: () => void
+ }
) =>
| EventReturnWithStateObject
| void