diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/Examples.tsx
index 11814ed6c06..5ccf8c67833 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/Examples.tsx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/Examples.tsx
@@ -1,5 +1,12 @@
+import { Button, Card } from '@dnb/eufemia/src'
import ComponentBox from '../../../../../../shared/tags/ComponentBox'
-import { Field, Form, Wizard } from '@dnb/eufemia/src/extensions/forms'
+import {
+ Field,
+ Form,
+ Tools,
+ Wizard,
+} from '@dnb/eufemia/src/extensions/forms'
+import React from 'react'
export const InWizard = () => {
return (
@@ -10,87 +17,100 @@ export const InWizard = () => {
Form.useSnapshot('my-form')
return (
-
+
{
if (mode === 'previous') {
- revertSnapshot(args.id)
+ revertSnapshot(args.id, 'my-snapshot-slice')
} else {
- createSnapshot(args.previousStep.id)
+ createSnapshot(
+ args.previousStep.id,
+ 'my-snapshot-slice',
+ )
}
}}
>
-
- Step A
-
+
+
+
+
+
-
- Step B
-
+
+
+
+
+
+ )
+ }
-
- ['group-1', 'group-2'].includes(value),
- }}
- >
- Step C
-
-
-
+ return
+ }}
+
+ )
+}
-
- Step D
-
-
-
-
+export const UndoRedo = () => {
+ return (
+
+ {() => {
+ const MyComponent = () => {
+ const { createSnapshot, applySnapshot } = Form.useSnapshot()
+ const pointerRef = React.useRef(0)
-
-
-
-
-
+ React.useEffect(() => {
+ createSnapshot(pointerRef.current, 'my-snapshot-slice')
+ }, [createSnapshot])
+
+ const changeHandler = React.useCallback(() => {
+ pointerRef.current += 1
+ createSnapshot(pointerRef.current, 'my-snapshot-slice')
+ }, [createSnapshot])
+ const undoHandler = React.useCallback(() => {
+ pointerRef.current -= 1
+ applySnapshot(pointerRef.current, 'my-snapshot-slice')
+ }, [applySnapshot])
+ const redoHandler = React.useCallback(() => {
+ pointerRef.current += 1
+ applySnapshot(pointerRef.current, 'my-snapshot-slice')
+ }, [applySnapshot])
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
)
}
- return
+ return (
+
+
+
+ )
}}
)
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/demos.mdx
index fc99aa80054..365dcc720c1 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/demos.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/demos.mdx
@@ -6,8 +6,12 @@ import * as Examples from './Examples'
## Demos
+### Undo / Redo
+
+
+
### Used in a Wizard
-This example revers the form data to its previous state when the user navigates back to a previous step.
+This example reverts the form data to its previous state when the user navigates back to a previous step.
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx
index 2122a43198d..49a77f1a529 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx
@@ -12,7 +12,8 @@ The hook returns an object with the following properties:
import { Form } from '@dnb/eufemia/extensions/forms'
function MyComponent() {
- const { createSnapshot, revertSnapshot } = Form.useSnapshot()
+ const { createSnapshot, applySnapshot, revertSnapshot } =
+ Form.useSnapshot()
return <>MyComponent>
}
@@ -25,15 +26,46 @@ render(
```
- `createSnapshot` will store the current data as a new snapshot with the given id.
+- `applySnapshot` will revert the data to the snapshot with the given id (required).
- `revertSnapshot` will revert the data to the snapshot with the given id (required). A reverted snapshot gets deleted from the memory.
-## Usage
+## Partial data snapshot
+
+In order to create and revert a snapshot for a specific part of the form data, you can use the `Form.Snapshot` component.
+
+```tsx
+import { Form, Field } from '@dnb/eufemia/extensions/forms'
+
+function MyForm() {
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+```
+
+When calling the `createSnapshot` or `revertSnapshot` functions, you can pass inn your snapshot name as the second parameter. This will make sure that the snapshot is only applied to the given part of the form data.
+
+```tsx
+createSnapshot('my-snapshot-id', 'my-snapshot-name')
+revertSnapshot('my-snapshot-id', 'my-snapshot-name')
+```
+
+You can check out examples in the demo section.
+
+## Usage of the `Form.useSnapshot` hook
You can use the `Form.useSnapshot` hook with or without an `id` (string) property, which is optional and can be used to link the data to a specific [Form.Handler](/uilib/extensions/forms/Form/Handler/) component.
### Without an `id` property
-Here "Component" is rendered inside the `Form.Handler` component and does not need an `id` property to access the form data:
+Here "Component" is rendered inside the `Form.Handler` component and does not need an `id` property to access the snapshot:
```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts b/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts
index 72fb14144c7..34cb6ecfd5a 100644
--- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Context.ts
@@ -15,6 +15,7 @@ import {
OnSubmitParams,
} from '../types'
import { Props as ProviderProps } from './Provider'
+import { SnapshotName } from '../Form/Snapshot'
export type MountState = {
isPreMounted?: boolean
@@ -155,11 +156,15 @@ export interface ContextState {
params?: { remove?: boolean }
) => void
setFieldConnection?: (path: Path, connections: FieldConnections) => void
+ forceUpdate?: () => void
isEmptyDataRef?: React.MutableRefObject
fieldPropsRef?: React.MutableRefObject>
valuePropsRef?: React.MutableRefObject>
fieldConnectionsRef?: React.RefObject>
mountedFieldsRef?: React.MutableRefObject>
+ snapshotsRef?: React.MutableRefObject<
+ Map>
+ >
formElementRef?: React.MutableRefObject
showAllErrors: boolean
hasVisibleError: boolean
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 325927e8339..f4fdd0e9ce0 100644
--- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx
@@ -57,7 +57,7 @@ import structuredClone from '@ungap/structured-clone'
const useLayoutEffect =
typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect
-export type SharedAttachments = {
+export type SharedAttachments = {
visibleDataHandler?: VisibleDataHandler
filterDataHandler?: FilterDataHandler
hasErrors?: ContextState['hasErrors']
@@ -65,8 +65,8 @@ export type SharedAttachments = {
setShowAllErrors?: ContextState['setShowAllErrors']
setSubmitState?: ContextState['setSubmitState']
rerenderUseDataHook?: () => void
- fieldConnectionsRef?: ContextState['fieldConnectionsRef']
clearData?: () => void
+ fieldConnectionsRef?: ContextState['fieldConnectionsRef']
}
export interface Props
@@ -249,6 +249,9 @@ export default function Provider(
// - Paths
const mountedFieldsRef: ContextState['mountedFieldsRef'] = useRef({})
+ // - Snapshots
+ const snapshotsRef: ContextState['snapshotsRef'] = useRef(new Map())
+
// - Errors from provider validation (the whole data set)
const hasVisibleErrorRef = useRef>({})
const errorsRef = useRef | undefined>()
@@ -598,10 +601,13 @@ export default function Provider(
}, [])
// - Shared state
- const sharedData = useSharedState void }>(id)
+ const sharedData = useSharedState(id)
const sharedAttachments = useSharedState>(
id + '-attachments'
)
+ const sharedDataContext = useSharedState(
+ id + '-data-context'
+ )
const setSharedData = sharedData.set
const extendSharedData = sharedData.extend
@@ -634,7 +640,7 @@ export default function Provider(
) {
return {
...internalDataRef.current,
- ...sharedData.data,
+ ...(sharedData.data || {}),
}
}
@@ -667,7 +673,7 @@ export default function Provider(
return {
...internalDataRef.current,
- ...sharedData.data,
+ ...(sharedData.data || {}),
}
}
@@ -704,12 +710,6 @@ export default function Provider(
}) // Delay so the field validation error message are not shown
}, [emptyData, id, onClear, setSharedData])
- useEffect(() => {
- if (sharedData.data?.clearForm) {
- onClear?.()
- }
- }, [onClear, sharedData.data?.clearForm])
-
useLayoutEffect(() => {
// Set the shared state, if initialData was given
if (id && initialData && !sharedData.data) {
@@ -732,8 +732,8 @@ export default function Provider(
hasFieldError,
setShowAllErrors,
setSubmitState,
- fieldConnectionsRef,
clearData,
+ fieldConnectionsRef,
})
if (filterSubmitData) {
rerenderUseDataHook?.()
@@ -1283,68 +1283,73 @@ export default function Provider(
? true
: undefined
+ const contextValue: ContextState = {
+ /** Method */
+ handlePathChange,
+ handlePathChangeUnvalidated,
+ handleSubmit,
+ setMountedFieldState,
+ handleSubmitCall,
+ setFormState,
+ setSubmitState,
+ setShowAllErrors,
+ setVisibleError,
+ setFieldEventListener,
+ setFieldState,
+ setFieldError,
+ setFieldConnection,
+ setFieldProps,
+ setValueProps,
+ hasErrors,
+ hasFieldError,
+ hasFieldState,
+ validateData,
+ updateDataValue,
+ setData,
+ clearData,
+ visibleDataHandler,
+ filterDataHandler,
+ getSubmitData,
+ getSubmitOptions,
+ addOnChangeHandler,
+ setHandleSubmit,
+ scrollToTop,
+ forceUpdate,
+
+ /** State handling */
+ schema,
+ disabled,
+ required,
+ formState,
+ submitState,
+ contextErrorMessages,
+ hasContext: true,
+ errors: errorsRef.current,
+ showAllErrors: showAllErrorsRef.current,
+ hasVisibleError: Object.keys(hasVisibleErrorRef.current).length > 0,
+ fieldConnectionsRef,
+ fieldPropsRef,
+ valuePropsRef,
+ mountedFieldsRef,
+ snapshotsRef,
+ formElementRef,
+ ajvInstance: ajvRef.current,
+
+ /** Additional */
+ id,
+ data: internalDataRef.current,
+ internalDataRef,
+ isEmptyDataRef,
+ props,
+ ...rest,
+ }
+
+ if (id) {
+ sharedDataContext.set(contextValue)
+ }
+
return (
- 0,
- fieldConnectionsRef,
- fieldPropsRef,
- valuePropsRef,
- mountedFieldsRef,
- formElementRef,
- ajvInstance: ajvRef.current,
-
- /** Additional */
- id,
- data: internalDataRef.current,
- internalDataRef,
- isEmptyDataRef,
- props,
- ...rest,
- }}
- >
+
new Map())
+ const mountedFieldsRef: SnapshotMap = useRef(map)
+ const { snapshotsRef } = useContext(DataContext) || {}
+
+ const setMountedField: SnapshotContextState['setMountedField'] =
+ useCallback((path, state) => {
+ mountedFieldsRef.current.set(path, state)
+ }, [])
+
+ useEffect(() => {
+ if (snapshotsRef) {
+ snapshotsRef.current.set(name, mountedFieldsRef.current)
+ }
+ }, [snapshotsRef, name])
+
+ const contextValue = { name, setMountedField }
+
+ return (
+
+ {children}
+
+ )
+}
+
+SnapshotProvider._supportsSpacingProps = undefined
+
+export default SnapshotProvider
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Snapshot/SnapshotContext.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Snapshot/SnapshotContext.tsx
new file mode 100644
index 00000000000..0103f40c091
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/Snapshot/SnapshotContext.tsx
@@ -0,0 +1,17 @@
+import { createContext } from 'react'
+import { Path } from '../../types'
+import { SnapshotName } from './Snapshot'
+
+export type SnapshotMap = React.MutableRefObject