From 4d83c77b77bf9a7a7b32fcd5e18bafbaa2b9144c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Tue, 1 Oct 2024 20:29:53 +0200 Subject: [PATCH] feat(Forms): add `isolatedData` to Iterate.PushContainer --- .../forms/Iterate/PushContainer/info.mdx | 20 ++- .../extensions/forms/DataContext/Context.ts | 1 + .../forms/DataContext/Provider/Provider.tsx | 15 ++ .../Isolation/__tests__/Isolation.test.tsx | 3 + .../Iterate/PushContainer/PushContainer.tsx | 38 ++-- .../PushContainer/PushContainerDocs.ts | 9 +- .../Iterate/stories/PushContainer.stories.tsx | 168 ++++++++++++++++++ .../extensions/forms/hooks/useFieldProps.ts | 3 +- .../dnb-eufemia/src/extensions/forms/types.ts | 6 +- 9 files changed, 244 insertions(+), 19 deletions(-) create mode 100644 packages/dnb-eufemia/src/extensions/forms/Iterate/stories/PushContainer.stories.tsx 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-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..4eef3be8333 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx @@ -682,7 +682,9 @@ export default function Provider( ? pointer.get(internalData, props.path) : internalData + const isEmptyDataRef = useRef(false) const clearData = useCallback(() => { + isEmptyDataRef.current = true internalDataRef.current = (emptyData ?? clearedData) as Data if (id) { @@ -691,6 +693,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(() => { @@ -996,6 +1002,7 @@ export default function Provider( if (isolate) { submitResult = await onCommit?.(internalDataRef.current, { clearData, + setData, }) } else { submitResult = await onSubmit() @@ -1068,6 +1075,7 @@ export default function Provider( setFormState, setShowAllErrors, setSubmitState, + setData, ] ) @@ -1329,6 +1337,7 @@ export default function Provider( id, data: internalDataRef.current, internalDataRef, + isEmptyDataRef, props, ...rest, }} @@ -1481,3 +1490,9 @@ function useFormStatusBuffer(props: FormStatusBufferProps) { } export const clearedData = Object.freeze({}) +export const getClearedData = (mergeData?: Record) => { + if (mergeData) { + return Object.assign(clearedData, mergeData) + } + return clearedData +} diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/__tests__/Isolation.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/__tests__/Isolation.test.tsx index d3fc9d6cd3f..8099eb94cf5 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/__tests__/Isolation.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/__tests__/Isolation.test.tsx @@ -1466,6 +1466,7 @@ describe('Form.Isolation', () => { { isolated: 'inside changed' }, { clearData: expect.any(Function), + setData: expect.any(Function), preventCommit: expect.any(Function), } ) @@ -1557,6 +1558,7 @@ describe('Form.Isolation', () => { { isolated: 'inside' }, { clearData: expect.any(Function), + setData: expect.any(Function), preventCommit: expect.any(Function), } ) @@ -1619,6 +1621,7 @@ describe('Form.Isolation', () => { { isolated: 'inside changed' }, { clearData: expect.any(Function), + setData: expect.any(Function), preventCommit: expect.any(Function), } ) 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..b11ea93dde0 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainer.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/PushContainer/PushContainer.tsx @@ -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 /** - * 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. */ @@ -65,6 +70,7 @@ function PushContainer(props: AllProps) { const { data: dataProp, defaultData: defaultDataProp, + isolatedData, path, title, children, @@ -101,12 +107,18 @@ 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 ( { - 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) @@ -125,12 +137,18 @@ function PushContainer(props: AllProps) { setNextContainerMode('view') switchContainerModeRef.current?.('view') clearData() + if (isolatedData) { + setData({ + ...isolatedData, + pushContainerItems: [clearedData], + }) + } } }} > { + 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..fb02d0138ef 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) ) }, diff --git a/packages/dnb-eufemia/src/extensions/forms/types.ts b/packages/dnb-eufemia/src/extensions/forms/types.ts index 87e9ef9c3c3..c84c6a1606d 100644 --- a/packages/dnb-eufemia/src/extensions/forms/types.ts +++ b/packages/dnb-eufemia/src/extensions/forms/types.ts @@ -600,7 +600,11 @@ export type OnCommit = ( { clearData, preventCommit, - }: { clearData: () => void; preventCommit?: () => void } + }: { + clearData: () => void + setData?: (data: Data) => void + preventCommit?: () => void + } ) => | EventReturnWithStateObject | void