Skip to content

Commit

Permalink
feat(Forms): add isolatedData to Iterate.PushContainer
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Oct 7, 2024
1 parent 59cf6c5 commit 4d83c77
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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.
Expand Down Expand Up @@ -84,3 +88,9 @@ render(
</Form.Handler>,
)
```

## 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`.
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export interface ContextState {
params?: { remove?: boolean }
) => void
setFieldConnection?: (path: Path, connections: FieldConnections) => void
isEmptyDataRef?: React.MutableRefObject<boolean>
fieldPropsRef?: React.MutableRefObject<Record<string, FieldProps>>
valuePropsRef?: React.MutableRefObject<Record<string, ValueProps>>
fieldConnectionsRef?: React.RefObject<Record<Path, FieldConnections>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,9 @@ export default function Provider<Data extends JsonObject>(
? pointer.get(internalData, props.path)
: internalData

const isEmptyDataRef = useRef(false)
const clearData = useCallback(() => {
isEmptyDataRef.current = true
internalDataRef.current = (emptyData ?? clearedData) as Data

if (id) {
Expand All @@ -691,6 +693,10 @@ export default function Provider<Data extends JsonObject>(

forceUpdate()
onClear?.()

requestAnimationFrame?.(() => {
isEmptyDataRef.current = false
}) // Delay so the field validation error message are not shown
}, [emptyData, id, onClear, setSharedData])

useEffect(() => {
Expand Down Expand Up @@ -996,6 +1002,7 @@ export default function Provider<Data extends JsonObject>(
if (isolate) {
submitResult = await onCommit?.(internalDataRef.current, {
clearData,
setData,
})
} else {
submitResult = await onSubmit()
Expand Down Expand Up @@ -1068,6 +1075,7 @@ export default function Provider<Data extends JsonObject>(
setFormState,
setShowAllErrors,
setSubmitState,
setData,
]
)

Expand Down Expand Up @@ -1329,6 +1337,7 @@ export default function Provider<Data extends JsonObject>(
id,
data: internalDataRef.current,
internalDataRef,
isEmptyDataRef,
props,
...rest,
}}
Expand Down Expand Up @@ -1481,3 +1490,9 @@ function useFormStatusBuffer(props: FormStatusBufferProps) {
}

export const clearedData = Object.freeze({})
export const getClearedData = (mergeData?: Record<string, unknown>) => {
if (mergeData) {
return Object.assign(clearedData, mergeData)
}
return clearedData
}
Original file line number Diff line number Diff line change
Expand Up @@ -1466,6 +1466,7 @@ describe('Form.Isolation', () => {
{ isolated: 'inside changed' },
{
clearData: expect.any(Function),
setData: expect.any(Function),
preventCommit: expect.any(Function),
}
)
Expand Down Expand Up @@ -1557,6 +1558,7 @@ describe('Form.Isolation', () => {
{ isolated: 'inside' },
{
clearData: expect.any(Function),
setData: expect.any(Function),
preventCommit: expect.any(Function),
}
)
Expand Down Expand Up @@ -1619,6 +1621,7 @@ describe('Form.Isolation', () => {
{ isolated: 'inside changed' },
{
clearData: expect.any(Function),
setData: expect.any(Function),
preventCommit: expect.any(Function),
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,20 @@ 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<string, unknown>

/**
* 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<string, unknown>

/**
* Provide additional data that will be put into the root of the isolated data context (parallel to "/pushContainerItems/0").
*/
isolatedData?: Record<string, unknown>

/**
* A custom toolbar to be shown below the container.
*/
Expand All @@ -65,6 +70,7 @@ function PushContainer(props: AllProps) {
const {
data: dataProp,
defaultData: defaultDataProp,
isolatedData,
path,
title,
children,
Expand Down Expand Up @@ -101,36 +107,48 @@ 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])

return (
<Isolation
data={data}
defaultData={defaultData}
emptyData={defaultData}
commitHandleRef={commitHandleRef}
transformOnCommit={({ newItems }) => {
return moveValueToPath(path, [...entries, ...newItems])
transformOnCommit={({ pushContainerItems }) => {
return moveValueToPath(path, [...entries, ...pushContainerItems])
}}
onCommit={(data, { clearData, preventCommit }) => {
onCommit={(data, { clearData, setData, preventCommit }) => {
if (hasReachedLimit) {
preventCommit()
setShowStatus(true)
} else {
setNextContainerMode('view')
switchContainerModeRef.current?.('view')
clearData()
if (isolatedData) {
setData({
...isolatedData,
pushContainerItems: [clearedData],
})
}
}
}}
>
<PushContainerContext.Provider value={newItemContextProps}>
<IterateArray
path="/newItems"
path="/pushContainerItems"
containerMode={showOpenButton ? 'view' : 'edit'}
>
<NewContainer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@ export const PushContainerProperties: PropertiesTableProps = {
status: 'optional',
},
data: {
doc: 'Prefilled data to be used by fields. Use `defaultData` when possible.',
doc: 'Prefilled data to be used by fields. The data will be put into this path: `/pushContainerItems/0`. Use `defaultData` when possible.',
type: ['object', 'array'],
status: 'optional',
},
defaultData: {
doc: 'Prefilled data to be used by fields.',
doc: 'Prefilled data to be used by fields. The data will be put into this path: `/pushContainerItems/0`',
type: ['object', 'array'],
status: 'optional',
},
isolatedData: {
doc: 'Provide additional data that will be put into the root of the isolated data context (parallel to `/pushContainerItems/0`).',
type: 'object',
status: 'optional',
},
openButton: {
doc: 'The button to open container.',
type: 'React.Node',
Expand Down
Loading

0 comments on commit 4d83c77

Please sign in to comment.