From 7fdd85da9d142c419618370731fd0a25db1f4a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Wed, 14 Aug 2024 14:53:26 +0200 Subject: [PATCH 1/5] fix(forms): add `Form.Isolation` path support when used inside `Form.Section` --- .../forms/Form/Isolation/Examples.tsx | 64 +++- .../extensions/forms/Form/Isolation/demos.mdx | 4 + .../extensions/forms/Form/Isolation/info.mdx | 1 + .../forms/DataContext/Provider/Provider.tsx | 96 +++--- .../DataContext/Provider/ProviderDocs.ts | 5 + .../extensions/forms/Form/Handler/Handler.tsx | 2 + .../forms/Form/Isolation/Isolation.tsx | 118 +++++-- .../Form/Isolation/IsolationCommitButton.tsx | 1 + .../forms/Form/Isolation/IsolationDocs.ts | 2 +- .../Isolation/__tests__/Isolation.test.tsx | 306 ++++++++++++++++-- .../Isolation/stories/Isolation.stories.tsx | 138 +++++++- .../extensions/forms/Form/Section/Section.tsx | 2 +- .../data-context/__tests__/clearData.test.tsx | 16 + .../extensions/forms/hooks/useFieldProps.ts | 7 + .../dnb-eufemia/src/extensions/forms/types.ts | 3 +- 15 files changed, 632 insertions(+), 133 deletions(-) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/Examples.tsx index fc5b9ae1142..ce1942ab14a 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/Examples.tsx @@ -16,10 +16,7 @@ export const UsingCommitButton = () => { > - - - - + @@ -58,10 +55,14 @@ export const CommitHandleRef = () => { > Ny hovedkontaktperson - + + + + + { @@ -176,9 +177,8 @@ export const TransformCommitData = () => { ], } }} - id="my-isolated-area" - onCommit={() => { - Form.clearData('my-isolated-area') + onCommit={(data, { clearData }) => { + clearData() }} > @@ -201,3 +201,45 @@ export const TransformCommitData = () => { ) } + +export const InsideSection = () => { + return ( + + { + console.log('Outer onChange:', data) + }} + > + + + { + console.log('Isolated onChange:', path, value) + }} + onCommit={(data) => console.log('onCommit:', data)} + > + + + + + + + + + + + + + + + ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/demos.mdx index c6fd726fa91..61c371e2536 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/demos.mdx @@ -18,3 +18,7 @@ import * as Examples from './Examples' ### Using commitHandleRef + +### Inside a section + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/info.mdx index 7f061c42b3b..adb72d831cd 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/info.mdx @@ -16,6 +16,7 @@ It's a provider that lets you provide a `schema` or `data` very similar to what - Input fields are prevented from submitting the form when pressing enter. Pressing enter on input fields will commit the isolated data to the `Form.Handler` context instead. - You can provide a `schema`, `data` or `defaultData` like you would do with the `Form.Handler`. - You can also provide `data` or `defaultData` to the `Form.Handler`, defining the data that will be used for the isolated data. +- Using `Form.Isolation` inside of a `Form.Section` is supported. - `onChange` on the `Form.Handler` will be called when the isolated data gets commited. - `onChange` on the `Form.Isolation` will be called on every change of the isolated data. Use `onCommit` to get the data that gets commited. 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 a8dfca43f70..7e5bf262245 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx @@ -25,8 +25,8 @@ import { OnChange, EventReturnWithStateObject, ValueProps, - OnCommit, } from '../../types' +import type { IsolationProviderProps } from '../../Form/Isolation/Isolation' import { debounce } from '../../../../shared/helpers' import { extendDeep } from '../../../../shared/component-helper' import FieldPropsProvider from '../../Form/FieldProps' @@ -53,7 +53,8 @@ import structuredClone from '@ungap/structured-clone' const useLayoutEffect = typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect -export interface Props { +export interface Props + extends IsolationProviderProps { /** * Unique ID to communicate with the hook Form.useData */ @@ -108,7 +109,7 @@ export interface Props { */ onPathChange?: ( path: Path, - value: any + value: unknown ) => | EventReturnWithStateObject | void @@ -135,11 +136,6 @@ export interface Props { | EventReturnWithStateObject | void | Promise - /** - * Used internally by the Form.Isolation component. - * Will emit on a nested form context commit – if validation has passed. - */ - onCommit?: OnCommit /** * Minimum time to display the submit indicator. */ @@ -168,18 +164,6 @@ export interface Props { * Make all fields required */ required?: boolean - /** - * Used internally by the Form.Isolation component - */ - path?: Path - /** - * Used internally by the Form.Isolation component - */ - isolate?: boolean - /** - * Transform the data before it gets committed to the form. The first parameter is the isolated data object. The second parameter is the outer context data object (Form.Handler). - */ - transformOnCommit?: (data: Data, contextData: Data) => Data /** * The children of the context provider */ @@ -205,6 +189,8 @@ export default function Provider( onSubmitRequest, onSubmitComplete, onCommit, + onClear, + transformOnCommit, scrollTopOnSubmit, minimumAsyncBehaviorTime, asyncSubmitTimeout, @@ -232,8 +218,8 @@ export default function Provider( const { hasContext, - handlePathChange: handlePathChangeNested, - data: dataNested, + handlePathChange: handlePathChangeOuter, + data: dataOuter, } = useContext(Context) || {} if (hasContext && !isolate) { @@ -294,6 +280,7 @@ export default function Provider( return JSON.parse(sessionDataJSON) } } + return data ?? defaultData // eslint-disable-next-line react-hooks/exhaustive-deps -- Avoid triggering code that should only run initially }, []) @@ -591,11 +578,14 @@ export default function Provider( sharedData.data !== internalDataRef.current ) { cacheRef.current.shared = sharedData.data - if (sharedData.data.clearForm) { - const clear = {} as Data - sharedData.set(clear) + + // Reset the shared state, if clearForm is set + if (sharedData.data?.clearForm) { + const clear = (cacheRef.current.shared = clearedData as Data) + setSharedData(clear) return clear } + return { ...internalDataRef.current, ...sharedData.data, @@ -609,13 +599,19 @@ export default function Provider( } return internalDataRef.current - }, [data, id, initialData, sharedData]) + }, [id, initialData, sharedData, data, setSharedData]) internalDataRef.current = props.path && pointer.has(internalData, props.path) ? pointer.get(internalData, props.path) : internalData + useEffect(() => { + if (sharedData.data?.clearForm) { + onClear?.() + } + }, [onClear, sharedData.data?.clearForm]) + useLayoutEffect(() => { // Set the shared state, if initialData was given if (id && initialData && !sharedData.data) { @@ -837,6 +833,16 @@ export default function Provider( } }, []) + const clearData = useCallback(() => { + internalDataRef.current = clearedData as Data + if (id) { + setSharedData?.(internalDataRef.current) + } else { + forceUpdate() + } + onClear?.() + }, [id, onClear, setSharedData]) + /** * Shared logic dedicated to submit the whole form */ @@ -894,24 +900,22 @@ export default function Provider( if (isolate) { const path = props.path ?? '/' const outerData = - props.path && pointer.has(dataNested, path) - ? pointer.get(dataNested, path) - : dataNested + props.path && pointer.has(dataOuter, path) + ? pointer.get(dataOuter, path) + : dataOuter let isolatedData = internalDataRef.current - if (typeof props.transformOnCommit === 'function') { - isolatedData = props.transformOnCommit( - isolatedData, - outerData - ) + if (typeof transformOnCommit === 'function') { + isolatedData = transformOnCommit(isolatedData, outerData) } // Commit the internal data to the nested context data - handlePathChangeNested?.( + handlePathChangeOuter?.( path, extendDeep({}, outerData, isolatedData) ) - result = await onCommit?.(isolatedData) + + result = await onCommit?.(isolatedData, { clearData }) } else { result = await onSubmit() } @@ -964,8 +968,9 @@ export default function Provider( return internalDataRef.current }, [ - dataNested, - handlePathChangeNested, + clearData, + dataOuter, + handlePathChangeOuter, hasErrors, hasFieldState, hasFieldWithAsyncValidator, @@ -976,6 +981,7 @@ export default function Provider( setFormState, setShowAllErrors, setSubmitState, + transformOnCommit, ] ) @@ -1004,14 +1010,7 @@ export default function Provider( forceUpdate() // in order to fill "empty fields" again with their internal states }, - clearData: () => { - internalDataRef.current = {} as Data - if (id) { - setSharedData?.({} as Data) - } else { - forceUpdate() - } - }, + clearData, } let result = undefined @@ -1042,16 +1041,15 @@ export default function Provider( }) }, [ + clearData, filterDataHandler, handleSubmitCall, - id, mutateDataHandler, onSubmit, onSubmitComplete, scrollToTop, scrollTopOnSubmit, sessionStorageId, - setSharedData, transformOut, ] ) @@ -1332,3 +1330,5 @@ function useFormStatusBuffer(props: FormStatusBufferProps) { return { bufferedFormState: stateRef.current } } + +export const clearedData = Object.freeze({}) diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/ProviderDocs.ts b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/ProviderDocs.ts index e1fa802a6eb..e25ee2bce63 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/ProviderDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/ProviderDocs.ts @@ -119,4 +119,9 @@ export const ProviderEvents: PropertiesTableProps = { type: 'function', status: 'optional', }, + onClear: { + doc: 'Will be called when the form is cleared via `Form.clearData` or via the `onSubmit` event (or `onCommit`) argument `{ clearData }`.', + type: 'function', + status: 'optional', + }, } diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Handler/Handler.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Handler/Handler.tsx index 945c59f4127..578307522d6 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Handler/Handler.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Handler/Handler.tsx @@ -36,6 +36,7 @@ export default function FormHandler({ onSubmit, onSubmitRequest, onSubmitComplete, + onClear, minimumAsyncBehaviorTime, asyncSubmitTimeout, scrollTopOnSubmit, @@ -63,6 +64,7 @@ export default function FormHandler({ onSubmit, onSubmitRequest, onSubmitComplete, + onClear, minimumAsyncBehaviorTime, asyncSubmitTimeout, scrollTopOnSubmit, diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx index 719ba807de5..69ea9afda01 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx @@ -1,10 +1,45 @@ -import React, { useCallback, useContext, useMemo, useRef } from 'react' +import React, { + useCallback, + useContext, + useMemo, + useReducer, + useRef, +} from 'react' import pointer, { JsonObject } from 'json-pointer' -import { Context, Provider } from '../../DataContext' -import { Props as ProviderProps } from '../../DataContext/Provider' -import { Path } from '../../types' import { extendDeep } from '../../../../shared/component-helper' +import { Context, Provider } from '../../DataContext' +import SectionContext from '../Section/SectionContext' import IsolationCommitButton from './IsolationCommitButton' +import { + clearedData, + type Props as ProviderProps, +} from '../../DataContext/Provider' +import type { OnCommit, Path } from '../../types' + +export type IsolationProviderProps = { + /** + * Form.Isolation: Will be called when the isolated context is committed. + */ + onCommit?: OnCommit + /** + * Form.Isolation: Will be called when the form is cleared via Form.clearData + */ + onClear?: () => void + /** + * Form.Isolation: A function that will be called when the isolated context is committed. + * It will receive the data from the isolated context and the data from the outer context. + * You can use this to transform the data before it is committed. + */ + transformOnCommit?: (isolatedData: Data, handlerData: Data) => Data + /** + * Used internally by the Form.Isolation component + */ + path?: Path + /** + * Used internally by the Form.Isolation component + */ + isolate?: boolean +} export type IsolationProps = Omit< ProviderProps, @@ -22,18 +57,6 @@ export type IsolationProps = Omit< * A ref (function) that you can call in order to commit the data programmatically to the outer context. */ commitHandleRef?: React.MutableRefObject<() => void> - - /** - * A function that will be called when the isolated context is committed. - * It will receive the data from the isolated context and the data from the outer context. - * You can use this to transform the data before it is committed. - */ - transformOnCommit?: (isolatedData: Data, handlerData: Data) => Data - - /** - * Will be called when the isolated context is committed. - */ - onCommit?: (data: Data) => void } function IsolationProvider( @@ -43,44 +66,73 @@ function IsolationProvider( children, onPathChange, onCommit, + onClear: onClearProp, commitHandleRef, data, defaultData, } = props + const [, forceUpdate] = useReducer(() => ({}), {}) + const internalDataRef = useRef() + const localDataRef = useRef>({}) const outerContext = useContext(Context) - - const dataRef = useRef>({}) - const getData = useCallback(() => { - return extendDeep({}, outerContext?.data, dataRef.current) as Data - }, [outerContext?.data]) + const { path: pathSection } = useContext(SectionContext) || {} const onPathChangeHandler = useCallback( - async (path: Path, value: any) => { - pointer.set(dataRef.current, path, value) + async (path: Path, value: unknown) => { + if (localDataRef.current === clearedData) { + localDataRef.current = {} + } + pointer.set(localDataRef.current, path, value) return await onPathChange?.(path, value) }, [onPathChange] ) + // Update the isolated data with the outside context data + useMemo(() => { + if (localDataRef.current === clearedData) { + return // stop here + } + + let localData = data ?? defaultData + + if ( + localData && + pathSection && + !pointer.has(localDataRef.current, pathSection) + ) { + const obj = {} as Data + pointer.set(obj, pathSection, localData) + localData = obj + } + + internalDataRef.current = extendDeep( + {}, + outerContext?.data, + localData || {}, + localDataRef.current + ) as Data + }, [data, defaultData, outerContext?.data, pathSection]) + + const onClear = useCallback(() => { + localDataRef.current = clearedData + internalDataRef.current = clearedData as Data + forceUpdate() + onClearProp?.() + }, [onClearProp]) + const providerProps: IsolationProps = { ...props, - data, - defaultData, + data: internalDataRef.current, + defaultData: undefined, onPathChange: onPathChangeHandler, onCommit, + onClear, isolate: true, } - // Update the isolated data with the outside context data - providerProps.data = useMemo(() => { - if (!defaultData && !data) { - return getData() - } - return providerProps.data - }, [data, defaultData, getData, providerProps.data]) - return ( diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationCommitButton.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationCommitButton.tsx index 0d2c51dc45c..6e7f46024b1 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationCommitButton.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationCommitButton.tsx @@ -29,6 +29,7 @@ function IsolationCommitButton(props: Props) { variant="secondary" className={classnames('dnb-forms-isolate-button', className)} icon={check} + icon_position="left" onClick={onClickHandler} {...rest} > diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationDocs.ts index 8bbb77b5157..e7c0bf7bfad 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationDocs.ts @@ -31,7 +31,7 @@ export const IsolationProperties: PropertiesTableProps = { export const IsolationEvents: PropertiesTableProps = { onCommit: { - doc: 'Will be called on a nested form context commit – if validation has passed.', + doc: 'Will be called on a nested form context commit – if validation has passed. The first parameter is the committed data object. The second parameter is on object containing a method to clear the internal data `{ clearData }`.', type: 'function', status: 'optional', }, 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 17ba12fe9be..5f940f28d99 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 @@ -4,12 +4,13 @@ import { createEvent, fireEvent, render, + waitFor, } from '@testing-library/react' -import { Field, Form, JSONSchema } from '../../..' import userEvent from '@testing-library/user-event' +import { Field, Form, JSONSchema } from '../../..' +import setData from '../../data-context/setData' import nbNO from '../../../constants/locales/nb-NO' -import setData from '../../data-context/setData' const nb = nbNO['nb-NO'] describe('Form.Isolation', () => { @@ -249,13 +250,13 @@ describe('Form.Isolation', () => { - + @@ -268,9 +269,9 @@ describe('Form.Isolation', () => { ) expect(regular).toHaveValue('Regular') - expect(isolated).toHaveValue('') + expect(isolated).toHaveValue('Isolated') - await userEvent.type(isolated, 'Something') + await userEvent.type(isolated, '{Backspace>8}Something') await userEvent.click(button) expect(regular).toHaveValue('Regular') @@ -522,9 +523,12 @@ describe('Form.Isolation', () => { }) expect(onCommit).toHaveBeenCalledTimes(1) - expect(onCommit).toHaveBeenLastCalledWith({ - isolated: 'Isolated', - }) + expect(onCommit).toHaveBeenLastCalledWith( + { + isolated: 'Isolated', + }, + expect.anything() + ) await userEvent.type(regular, 'Regular') @@ -533,10 +537,13 @@ describe('Form.Isolation', () => { }) expect(onCommit).toHaveBeenCalledTimes(2) - expect(onCommit).toHaveBeenLastCalledWith({ - regular: 'Regular', - isolated: 'Isolated', - }) + expect(onCommit).toHaveBeenLastCalledWith( + { + regular: 'Regular', + isolated: 'Isolated', + }, + expect.anything() + ) }) it('should support nested paths', async () => { @@ -636,7 +643,7 @@ describe('Form.Isolation', () => { ) }) - describe('Isolate.Button', () => { + describe('Isolate.CommitButton', () => { it('should have correct type and text', async () => { render( @@ -659,7 +666,7 @@ describe('Form.Isolation', () => { - + ) @@ -678,9 +685,12 @@ describe('Form.Isolation', () => { isolated: 'Isolated', }) expect(onCommit).toHaveBeenCalledTimes(1) - expect(onCommit).toHaveBeenLastCalledWith({ - isolated: 'Isolated', - }) + expect(onCommit).toHaveBeenLastCalledWith( + { + isolated: 'Isolated', + }, + expect.anything() + ) await userEvent.click(button) await userEvent.type(isolated, '-updated') @@ -690,9 +700,12 @@ describe('Form.Isolation', () => { isolated: 'Isolated', }) expect(onCommit).toHaveBeenCalledTimes(2) - expect(onCommit).toHaveBeenLastCalledWith({ - isolated: 'Isolated', - }) + expect(onCommit).toHaveBeenLastCalledWith( + { + isolated: 'Isolated', + }, + expect.anything() + ) await userEvent.click(button) @@ -701,9 +714,81 @@ describe('Form.Isolation', () => { isolated: 'Isolated-updated', }) expect(onCommit).toHaveBeenCalledTimes(3) - expect(onCommit).toHaveBeenLastCalledWith({ + expect(onCommit).toHaveBeenLastCalledWith( + { + isolated: 'Isolated-updated', + }, + expect.anything() + ) + + expect(onSubmit).toHaveBeenCalledTimes(0) + }) + + it('should commit data on SubmitButton click without committing the form', async () => { + const onChange = jest.fn() + const onSubmit = jest.fn() + const onCommit = jest.fn() + + render( + + + + + + + ) + + const isolated = document.querySelector('input') + const button = document.querySelector('button') + + expect(button).toHaveTextContent('Send') + + await userEvent.type(isolated, 'Isolated') + expect(onChange).toHaveBeenCalledTimes(0) + expect(onCommit).toHaveBeenCalledTimes(0) + + await userEvent.click(button) + + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenLastCalledWith({ + isolated: 'Isolated', + }) + expect(onCommit).toHaveBeenCalledTimes(1) + expect(onCommit).toHaveBeenLastCalledWith( + { + isolated: 'Isolated', + }, + expect.anything() + ) + + await userEvent.click(button) + await userEvent.type(isolated, '-updated') + + expect(onChange).toHaveBeenCalledTimes(2) + expect(onChange).toHaveBeenLastCalledWith({ + isolated: 'Isolated', + }) + expect(onCommit).toHaveBeenCalledTimes(2) + expect(onCommit).toHaveBeenLastCalledWith( + { + isolated: 'Isolated', + }, + expect.anything() + ) + + await userEvent.click(button) + + expect(onChange).toHaveBeenCalledTimes(3) + expect(onChange).toHaveBeenLastCalledWith({ isolated: 'Isolated-updated', }) + expect(onCommit).toHaveBeenCalledTimes(3) + expect(onCommit).toHaveBeenLastCalledWith( + { + isolated: 'Isolated-updated', + }, + expect.anything() + ) expect(onSubmit).toHaveBeenCalledTimes(0) }) @@ -1020,7 +1105,7 @@ describe('Form.Isolation', () => { } }} > - + @@ -1047,4 +1132,179 @@ describe('Form.Isolation', () => { persons: [{ name: 'John' }, { name: 'Oda' }, { name: 'Odd' }], }) }) + + it('should render inside section with correct paths', async () => { + const onPathChange = jest.fn() + const onChange = jest.fn() + + render( + + + + + + + + + + + + ) + + const button = document.querySelector('button') + const inputs = Array.from(document.querySelectorAll('input')) + const [isolated, synced, regular] = inputs + + expect(isolated).toHaveValue('inside') + expect(synced).toHaveValue('outside') + expect(regular).toHaveValue('regular') + + await userEvent.type(isolated, ' changed') + + expect(onPathChange).toHaveBeenLastCalledWith( + '/mySection/isolated', + 'inside changed' + ) + + expect(isolated).toHaveValue('inside changed') + expect(synced).toHaveValue('outside') + expect(regular).toHaveValue('regular') + + await userEvent.type(regular, ' changed') + + expect(isolated).toHaveValue('inside changed') + expect(synced).toHaveValue('outside') + expect(regular).toHaveValue('regular changed') + + await userEvent.type(synced, ' changed') + + expect(isolated).toHaveValue('inside changed') + expect(synced).toHaveValue('outside changed') + expect(regular).toHaveValue('regular changed') + + await userEvent.click(button) + + expect(onChange).toHaveBeenLastCalledWith({ + isolated: 'inside', + mySection: { + isolated: 'inside changed', + regular: 'regular changed', + }, + }) + + expect(isolated).toHaveValue('inside changed') + expect(synced).toHaveValue('inside changed') + expect(regular).toHaveValue('regular changed') + + await userEvent.type(synced, ' 2x') + + expect(isolated).toHaveValue('inside changed') + expect(synced).toHaveValue('inside changed 2x') + expect(regular).toHaveValue('regular changed') + + await userEvent.type(regular, ' 2x') + + expect(isolated).toHaveValue('inside changed') + expect(synced).toHaveValue('inside changed 2x') + expect(regular).toHaveValue('regular changed 2x') + + await userEvent.type(isolated, ' 2x') + + expect(onPathChange).toHaveBeenLastCalledWith( + '/mySection/isolated', + 'inside changed 2x' + ) + + expect(isolated).toHaveValue('inside changed 2x') + expect(synced).toHaveValue('inside changed 2x') + expect(regular).toHaveValue('regular changed 2x') + }) + + it('clears the form data when "clearData" is called inside the "onCommit" event', async () => { + const onCommit = jest.fn((data, { clearData }) => { + clearData() + }) + + render( + + + + + + + + + + + + ) + + const button = document.querySelector('button') + const inputs = Array.from(document.querySelectorAll('input')) + const [isolated, synced, regular] = inputs + + await userEvent.type(isolated, ' changed') + + expect(isolated).toHaveValue('inside changed') + expect(synced).toHaveValue('outside') + expect(regular).toHaveValue('regular') + + await userEvent.click(button) + + expect(onCommit).toHaveBeenCalledTimes(1) + expect(onCommit).toHaveBeenLastCalledWith( + { + mySection: { + isolated: 'inside changed', + regular: 'regular', + }, + }, + { clearData: expect.any(Function) } + ) + + await waitFor(() => { + expect(isolated).toHaveValue('') + expect(synced).toHaveValue('inside changed') + expect(regular).toHaveValue('regular') + + expect(document.querySelector('.dnb-form-status')).toBeNull() + }) + + await userEvent.click(button) + + expect(document.querySelector('.dnb-form-status')).toHaveTextContent( + nb.Field.errorRequired + ) + + await userEvent.type(isolated, 'new value') + + expect(isolated).toHaveValue('new value') + expect(synced).toHaveValue('inside changed') + expect(regular).toHaveValue('regular') + }) }) diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/stories/Isolation.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/stories/Isolation.stories.tsx index d8d22ee63ad..f303a7b1960 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/stories/Isolation.stories.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/stories/Isolation.stories.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Field, Form } from '../../..' -import { Flex } from '../../../../../components' +import { Card, Flex, HeightAnimation } from '../../../../../components' export default { title: 'Eufemia/Extensions/Forms/Isolation', @@ -17,22 +17,20 @@ export function Isolation() { // info: 'Info message', // } }} - defaultData={ - { - // regular: 'Regular', - // isolated: 'Isolated', - } - } + defaultData={{ + regular: 'Regular', + // isolated: 'Isolated', + }} > { - await new Promise((resolve) => setTimeout(resolve, 1000)) - // console.log('Isolated onChange:', data) - // return { - // info: 'Info message', - // } - }} + // onChange={async (data) => { + // await new Promise((resolve) => setTimeout(resolve, 1000)) + // // console.log('Isolated onChange:', data) + // // return { + // // info: 'Info message', + // // } + // }} onCommit={(data) => console.log('onCommit:', data)} // defaultData={{ // isolated: 'Isolated', @@ -53,7 +51,7 @@ export function Isolation() { - + @@ -61,3 +59,113 @@ export function Isolation() { ) } + +export function IsolationInsideSection() { + return ( + { + // console.log('Outer onChange:', data) + // await new Promise((resolve) => setTimeout(resolve, 10)) + // }} + > + + + { + // console.log('Isolated onChange:', path, value) + // await new Promise((resolve) => setTimeout(resolve, 10)) + // }} + onCommit={(data) => console.log('onCommit:', data)} + > + + + + + + + + + + + + + + + + + + ) +} + +export const TransformOnCommit = () => { + return ( + + + Legg til ny hovedkontaktperson + + + + + + + + + + Ny hovedkontaktperson + + { + return { + ...handlerData, + contactPersons: [ + ...handlerData.contactPersons, + { + ...isolatedData.newPerson, + value: isolatedData.newPerson.title.toLowerCase(), + }, + ], + } + }} + onCommit={(data, { clearData }) => { + clearData() + }} + > + + + + + + + + + + + + + ) +} diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx index cb759139329..2a132dd9fd0 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx @@ -71,7 +71,7 @@ function SectionComponent(props: LocalProps) { children, } = props - if (path && !path.startsWith('/')) { + if (path && !path.startsWith?.('/')) { throw new Error(`path="${path}" must start with a slash`) } diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/__tests__/clearData.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/__tests__/clearData.test.tsx index 9f88654e126..bde98346af7 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/data-context/__tests__/clearData.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/data-context/__tests__/clearData.test.tsx @@ -17,6 +17,22 @@ describe('Form.clearData', () => { expect(document.querySelector('input')).toHaveValue('') }) + it('should call onClear', () => { + const onClear = jest.fn() + + render( + + + + ) + + expect(onClear).not.toHaveBeenCalled() + + act(() => Form.clearData('unique-id')) + + expect(onClear).toHaveBeenCalledTimes(1) + }) + describe('can be used in beforeEach', () => { beforeEach(() => { Form.clearData('unique-id') diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts index d4fea88a515..e005bf3c79f 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts @@ -20,6 +20,7 @@ import { EventStateObjectWithSuccess, } from '../types' import { Context as DataContext, ContextState } from '../DataContext' +import { clearedData } from '../DataContext/Provider/Provider' import FieldPropsContext from '../Form/FieldProps/FieldPropsContext' import { combineDescribedBy } from '../../../shared/component-helper' import useId from '../../../shared/helpers/useId' @@ -1107,6 +1108,12 @@ export default function useFieldProps< validateValue() }, [schema, validateValue]) + useEffect(() => { + if (dataContext.data === clearedData) { + hideError() + } + }, [dataContext.data, hideError]) + useUpdateEffect(() => { // Error or removed error for this field from the surrounding data context (by path) if (valueRef.current !== externalValue) { diff --git a/packages/dnb-eufemia/src/extensions/forms/types.ts b/packages/dnb-eufemia/src/extensions/forms/types.ts index b5d780ed9ba..58a79dd4e38 100644 --- a/packages/dnb-eufemia/src/extensions/forms/types.ts +++ b/packages/dnb-eufemia/src/extensions/forms/types.ts @@ -497,7 +497,8 @@ export type OnSubmit = ( | Promise export type OnCommit = ( - data: Data + data: Data, + { clearData }: { clearData: () => void } ) => | EventReturnWithStateObject | void From ea2227aba362a6a568531652c01cf9e09eff03c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Fri, 16 Aug 2024 14:36:12 +0200 Subject: [PATCH 2/5] Update packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationDocs.ts Co-authored-by: Anders --- .../src/extensions/forms/Form/Isolation/IsolationDocs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationDocs.ts index e7c0bf7bfad..dac99bb05ac 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/IsolationDocs.ts @@ -31,7 +31,7 @@ export const IsolationProperties: PropertiesTableProps = { export const IsolationEvents: PropertiesTableProps = { onCommit: { - doc: 'Will be called on a nested form context commit – if validation has passed. The first parameter is the committed data object. The second parameter is on object containing a method to clear the internal data `{ clearData }`.', + doc: 'Will be called on a nested form context commit – if validation has passed. The first parameter is the committed data object. The second parameter is an object containing a method to clear the internal data `{ clearData }`.', type: 'function', status: 'optional', }, From ba41527ed379f901ed6dc08a8846067e8b97c020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Fri, 16 Aug 2024 16:17:46 +0200 Subject: [PATCH 3/5] Correct what data is committed --- .../forms/DataContext/Provider/Provider.tsx | 32 +------ .../forms/Form/Isolation/Isolation.tsx | 45 +++++++++- .../Isolation/__tests__/Isolation.test.tsx | 87 +++++++++++++++++-- 3 files changed, 128 insertions(+), 36 deletions(-) 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 7e5bf262245..f58ba7e2ae2 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx @@ -28,7 +28,6 @@ import { } from '../../types' import type { IsolationProviderProps } from '../../Form/Isolation/Isolation' import { debounce } from '../../../../shared/helpers' -import { extendDeep } from '../../../../shared/component-helper' import FieldPropsProvider from '../../Form/FieldProps' import useMountEffect from '../../../../shared/helpers/useMountEffect' import useUpdateEffect from '../../../../shared/helpers/useUpdateEffect' @@ -216,11 +215,7 @@ export default function Provider( ) } - const { - hasContext, - handlePathChange: handlePathChangeOuter, - data: dataOuter, - } = useContext(Context) || {} + const { hasContext } = useContext(Context) || {} if (hasContext && !isolate) { throw new Error('DataContext (Form.Handler) can not be nested') @@ -898,24 +893,9 @@ export default function Provider( try { if (isolate) { - const path = props.path ?? '/' - const outerData = - props.path && pointer.has(dataOuter, path) - ? pointer.get(dataOuter, path) - : dataOuter - let isolatedData = internalDataRef.current - - if (typeof transformOnCommit === 'function') { - isolatedData = transformOnCommit(isolatedData, outerData) - } - - // Commit the internal data to the nested context data - handlePathChangeOuter?.( - path, - extendDeep({}, outerData, isolatedData) - ) - - result = await onCommit?.(isolatedData, { clearData }) + result = await onCommit?.(internalDataRef.current, { + clearData, + }) } else { result = await onSubmit() } @@ -969,19 +949,15 @@ export default function Provider( }, [ clearData, - dataOuter, - handlePathChangeOuter, hasErrors, hasFieldState, hasFieldWithAsyncValidator, isolate, onCommit, onSubmitRequest, - props.path, setFormState, setShowAllErrors, setSubmitState, - transformOnCommit, ] ) diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx index 69ea9afda01..5c90735e291 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx @@ -16,6 +16,12 @@ import { } from '../../DataContext/Provider' import type { OnCommit, Path } from '../../types' +/** + * Deprecated, as it is supported by all major browsers and Node.js >=v18 + * So its a question of time, when we will remove this polyfill + */ +import structuredClone from '@ungap/structured-clone' + export type IsolationProviderProps = { /** * Form.Isolation: Will be called when the isolated context is committed. @@ -65,8 +71,9 @@ function IsolationProvider( const { children, onPathChange, - onCommit, + onCommit: onCommitProp, onClear: onClearProp, + transformOnCommit: transformOnCommitProp, commitHandleRef, data, defaultData, @@ -77,6 +84,8 @@ function IsolationProvider( const localDataRef = useRef>({}) const outerContext = useContext(Context) const { path: pathSection } = useContext(SectionContext) || {} + const { handlePathChange: handlePathChangeOuter, data: dataOuter } = + outerContext || {} const onPathChangeHandler = useCallback( async (path: Path, value: unknown) => { @@ -110,11 +119,41 @@ function IsolationProvider( internalDataRef.current = extendDeep( {}, - outerContext?.data, + dataOuter, localData || {}, localDataRef.current ) as Data - }, [data, defaultData, outerContext?.data, pathSection]) + }, [data, defaultData, dataOuter, pathSection]) + + const onCommit: IsolationProps['onCommit'] = useCallback( + async (data: Data, additionalArgs) => { + const path = props.path ?? '/' + const outerData = + props.path && pointer.has(dataOuter, path) + ? pointer.get(dataOuter, path) + : dataOuter + let isolatedData = structuredClone(localDataRef.current) as Data + + if (typeof transformOnCommitProp === 'function') { + isolatedData = transformOnCommitProp(isolatedData, outerData) + } + + // Commit the internal data to the nested context data + handlePathChangeOuter?.( + path, + extendDeep({}, outerData, isolatedData) + ) + + return await onCommitProp?.(isolatedData, additionalArgs) + }, + [ + handlePathChangeOuter, + onCommitProp, + dataOuter, + props.path, + transformOnCommitProp, + ] + ) const onClear = useCallback(() => { localDataRef.current = 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 5f940f28d99..0a40c12bba3 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 @@ -44,7 +44,7 @@ describe('Form.Isolation', () => { expect(isolated).toHaveValue('Isolated') }) - it('should use data from isolated context', () => { + it('should use "data" from isolated context', () => { render( { expect(isolated).toHaveValue('Isolated other value') }) - it('should use defaultData from isolated context', () => { + it('should use "defaultData" from isolated context', () => { render( { }) }) + it('onCommit should only return the isolated data', async () => { + const onCommit = jest.fn() + + render( + + + + + + + + + ) + + const button = document.querySelector('button') + const [regular, isolated] = Array.from( + document.querySelectorAll('input') + ) + + expect(regular).toHaveValue('Regular') + expect(isolated).toHaveValue('Isolated') + + await userEvent.type(isolated, '{Backspace>8}Something') + await userEvent.click(button) + + expect(regular).toHaveValue('Regular') + expect(isolated).toHaveValue('Something') + expect(onCommit).toHaveBeenCalledTimes(1) + expect(onCommit).toHaveBeenLastCalledWith( + { + isolated: 'Something', + }, + expect.anything() + ) + + await userEvent.type(regular, ' updated') + await userEvent.click(button) + + expect(regular).toHaveValue('Regular updated') + expect(isolated).toHaveValue('Something') + expect(onCommit).toHaveBeenCalledTimes(2) + expect(onCommit).toHaveBeenLastCalledWith( + { + isolated: 'Something', + }, + expect.anything() + ) + + await userEvent.type(isolated, ' 2') + await userEvent.click(button) + + expect(regular).toHaveValue('Regular updated') + expect(isolated).toHaveValue('Something 2') + expect(onCommit).toHaveBeenCalledTimes(3) + expect(onCommit).toHaveBeenLastCalledWith( + { + isolated: 'Something 2', + }, + expect.anything() + ) + + await userEvent.type(regular, ' 2') + await userEvent.click(button) + + expect(regular).toHaveValue('Regular updated 2') + expect(isolated).toHaveValue('Something 2') + expect(onCommit).toHaveBeenCalledTimes(4) + expect(onCommit).toHaveBeenLastCalledWith( + { + isolated: 'Something 2', + }, + expect.anything() + ) + }) + it('should call onCommit event when commitHandleRef is called', async () => { const onCommit = jest.fn() const commitHandleRef = React.createRef<() => void>() @@ -539,7 +619,6 @@ describe('Form.Isolation', () => { expect(onCommit).toHaveBeenCalledTimes(2) expect(onCommit).toHaveBeenLastCalledWith( { - regular: 'Regular', isolated: 'Isolated', }, expect.anything() @@ -1198,7 +1277,6 @@ describe('Form.Isolation', () => { await userEvent.click(button) expect(onChange).toHaveBeenLastCalledWith({ - isolated: 'inside', mySection: { isolated: 'inside changed', regular: 'regular changed', @@ -1281,7 +1359,6 @@ describe('Form.Isolation', () => { { mySection: { isolated: 'inside changed', - regular: 'regular', }, }, { clearData: expect.any(Function) } From 8b41c22d3e2ac97c5af87ba6c8f301b5891faafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Fri, 16 Aug 2024 16:38:50 +0200 Subject: [PATCH 4/5] Ensure onCommit and onPathChange does not include section path --- .../forms/Form/Isolation/Isolation.tsx | 28 +++++++++++++++---- .../Isolation/__tests__/Isolation.test.tsx | 19 +++++++------ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx index 5c90735e291..75146ec1716 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx @@ -92,11 +92,25 @@ function IsolationProvider( if (localDataRef.current === clearedData) { localDataRef.current = {} } + pointer.set(localDataRef.current, path, value) + if (pathSection) { + path = path.replace(pathSection, '') + } + return await onPathChange?.(path, value) }, - [onPathChange] + [onPathChange, pathSection] + ) + + const removeSectionPath = useCallback( + (data: Data) => { + return pathSection && pointer.has(data, pathSection) + ? pointer.get(data, pathSection) + : data + }, + [pathSection] ) // Update the isolated data with the outside context data @@ -144,14 +158,18 @@ function IsolationProvider( extendDeep({}, outerData, isolatedData) ) - return await onCommitProp?.(isolatedData, additionalArgs) + return await onCommitProp?.( + removeSectionPath(isolatedData), + additionalArgs + ) }, [ - handlePathChangeOuter, - onCommitProp, - dataOuter, props.path, + dataOuter, transformOnCommitProp, + handlePathChangeOuter, + onCommitProp, + removeSectionPath, ] ) 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 0a40c12bba3..22d43d96613 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 @@ -1213,8 +1213,9 @@ describe('Form.Isolation', () => { }) it('should render inside section with correct paths', async () => { - const onPathChange = jest.fn() const onChange = jest.fn() + const onCommit = jest.fn() + const onPathChange = jest.fn() render( { defaultData={{ isolated: 'inside', }} + onCommit={onCommit} onPathChange={onPathChange} > @@ -1254,7 +1256,7 @@ describe('Form.Isolation', () => { await userEvent.type(isolated, ' changed') expect(onPathChange).toHaveBeenLastCalledWith( - '/mySection/isolated', + '/isolated', 'inside changed' ) @@ -1276,6 +1278,11 @@ describe('Form.Isolation', () => { await userEvent.click(button) + expect(onCommit).toHaveBeenCalledTimes(1) + expect(onCommit).toHaveBeenLastCalledWith( + { isolated: 'inside changed' }, + { clearData: expect.any(Function) } + ) expect(onChange).toHaveBeenLastCalledWith({ mySection: { isolated: 'inside changed', @@ -1302,7 +1309,7 @@ describe('Form.Isolation', () => { await userEvent.type(isolated, ' 2x') expect(onPathChange).toHaveBeenLastCalledWith( - '/mySection/isolated', + '/isolated', 'inside changed 2x' ) @@ -1356,11 +1363,7 @@ describe('Form.Isolation', () => { expect(onCommit).toHaveBeenCalledTimes(1) expect(onCommit).toHaveBeenLastCalledWith( - { - mySection: { - isolated: 'inside changed', - }, - }, + { isolated: 'inside changed' }, { clearData: expect.any(Function) } ) From 9732e0b256ea915e1e0ce9bfc1c9c92769a0265c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Fri, 16 Aug 2024 19:52:15 +0200 Subject: [PATCH 5/5] Include mounted field values in onCommit --- .../forms/DataContext/Provider/Provider.tsx | 13 ++++- .../forms/Form/Isolation/Isolation.tsx | 6 +- .../Isolation/__tests__/Isolation.test.tsx | 58 +++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) 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 f58ba7e2ae2..5fec9e83a93 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx @@ -189,7 +189,6 @@ export default function Provider( onSubmitComplete, onCommit, onClear, - transformOnCommit, scrollTopOnSubmit, minimumAsyncBehaviorTime, asyncSubmitTimeout, @@ -893,7 +892,17 @@ export default function Provider( try { if (isolate) { - result = await onCommit?.(internalDataRef.current, { + const mounterData = {} as Data + mountedFieldPathsRef.current.forEach((path) => { + if (pointer.has(internalDataRef.current, path)) { + pointer.set( + mounterData, + path, + pointer.get(internalDataRef.current, path) + ) + } + }) + result = await onCommit?.(mounterData, { clearData, }) } else { diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx index 75146ec1716..6362d86ae95 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Isolation/Isolation.tsx @@ -140,13 +140,15 @@ function IsolationProvider( }, [data, defaultData, dataOuter, pathSection]) const onCommit: IsolationProps['onCommit'] = useCallback( - async (data: Data, additionalArgs) => { + async (mountedData: Data, additionalArgs) => { const path = props.path ?? '/' const outerData = props.path && pointer.has(dataOuter, path) ? pointer.get(dataOuter, path) : dataOuter - let isolatedData = structuredClone(localDataRef.current) as Data + + localDataRef.current = mountedData + let isolatedData = structuredClone(mountedData) as Data if (typeof transformOnCommitProp === 'function') { isolatedData = transformOnCommitProp(isolatedData, outerData) 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 22d43d96613..389adc8e33e 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 @@ -1318,6 +1318,64 @@ describe('Form.Isolation', () => { expect(regular).toHaveValue('regular changed 2x') }) + it('should commit unchanged data when inside a section', async () => { + const onChange = jest.fn() + const onCommit = jest.fn() + + render( + + + + + + + + + + + + ) + + const button = document.querySelector('button') + const inputs = Array.from(document.querySelectorAll('input')) + const [isolated, synced, regular] = inputs + + expect(isolated).toHaveValue('inside') + expect(synced).toHaveValue('outside') + expect(regular).toHaveValue('regular') + + await userEvent.click(button) + + expect(isolated).toHaveValue('inside') + expect(synced).toHaveValue('inside') + expect(regular).toHaveValue('regular') + + expect(onCommit).toHaveBeenCalledTimes(1) + expect(onCommit).toHaveBeenLastCalledWith( + { isolated: 'inside' }, + { clearData: expect.any(Function) } + ) + expect(onChange).toHaveBeenLastCalledWith({ + mySection: { + isolated: 'inside', + regular: 'regular', + }, + }) + }) + it('clears the form data when "clearData" is called inside the "onCommit" event', async () => { const onCommit = jest.fn((data, { clearData }) => { clearData()