diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot.mdx
new file mode 100644
index 00000000000..41f3a26d950
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot.mdx
@@ -0,0 +1,23 @@
+---
+title: 'useSnapshot'
+description: '`Form.useSnapshot` lets you store data snapshots of your form data, either inside or outside of the form context.'
+showTabs: true
+tabs:
+ - title: Info
+ key: '/info'
+ - title: Demos
+ key: '/demos'
+breadcrumb:
+ - text: Forms
+ href: /uilib/extensions/forms/
+ - text: Form
+ href: /uilib/extensions/forms/Form/
+ - text: Form.useSnapshot
+ href: /uilib/extensions/forms/Form/useSnapshot/
+---
+
+import Info from 'Docs/uilib/extensions/forms/Form/useSnapshot/info'
+import Demos from 'Docs/uilib/extensions/forms/Form/useSnapshot/demos'
+
+
+
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
new file mode 100644
index 00000000000..8b35af4b99f
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/Examples.tsx
@@ -0,0 +1,97 @@
+import ComponentBox from '../../../../../../shared/tags/ComponentBox'
+import { Field, Form, Wizard } from '@dnb/eufemia/src/extensions/forms'
+
+export const InWizard = () => {
+ return (
+
+ {() => {
+ const MyForm = () => {
+ const { createSnapshot, revertSnapshot } =
+ Form.useSnapshot('my-form')
+
+ return (
+
+ {
+ if (mode === 'previous') {
+ revertSnapshot(args.id)
+ } else {
+ createSnapshot(args.previousId)
+ }
+ }}
+ >
+
+ Step A
+
+
+
+
+
+ Step B
+
+
+
+
+
+ ['group-1', 'group-2'].includes(value),
+ }}
+ >
+ Step C
+
+
+
+
+
+ Step D
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ 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
new file mode 100644
index 00000000000..fc99aa80054
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/demos.mdx
@@ -0,0 +1,13 @@
+---
+showTabs: true
+---
+
+import * as Examples from './Examples'
+
+## Demos
+
+### Used in a Wizard
+
+This example revers 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
new file mode 100644
index 00000000000..2122a43198d
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx
@@ -0,0 +1,75 @@
+---
+showTabs: true
+---
+
+## Description
+
+The `Form.useSnapshot` hook lets you store data snapshots of your form data, either inside or outside of the form context.
+
+The hook returns an object with the following properties:
+
+```tsx
+import { Form } from '@dnb/eufemia/extensions/forms'
+
+function MyComponent() {
+ const { createSnapshot, revertSnapshot } = Form.useSnapshot()
+
+ return <>MyComponent>
+}
+
+render(
+
+
+ ,
+)
+```
+
+- `createSnapshot` will store the current data as a new snapshot with the given id.
+- `revertSnapshot` will revert the data to the snapshot with the given id (required). A reverted snapshot gets deleted from the memory.
+
+## Usage
+
+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:
+
+```jsx
+import { Form } from '@dnb/eufemia/extensions/forms'
+
+function MyForm() {
+ return (
+
+
+
+ )
+}
+
+function Component() {
+ const { createSnapshot, revertSnapshot } = Form.useSnapshot()
+}
+```
+
+### With an `id` property
+
+While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string) property:
+
+```jsx
+import { Form } from '@dnb/eufemia/extensions/forms'
+
+function MyForm() {
+ return (
+ <>
+ ...
+
+ >
+ )
+}
+
+function Component() {
+ const { createSnapshot, revertSnapshot } = Form.useSnapshot('unique')
+}
+```
+
+This is beneficial when you need to utilize the form data in other places within your application.
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/index.ts b/packages/dnb-eufemia/src/extensions/forms/Form/index.ts
index ae7fd03bd51..302c50df54b 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/index.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/index.ts
@@ -18,6 +18,7 @@ export { default as getData } from './data-context/getData'
export { default as clearData } from './data-context/clearData'
export { default as useValidation } from './data-context/useValidation'
export { default as useTranslation } from '../hooks/useTranslation'
+export { default as useSnapshot } from '../hooks/useSnapshot'
/**
* Can be removed in v11
diff --git a/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx b/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx
index 3ce8066c359..cc00e4d4acc 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx
@@ -17,6 +17,7 @@ import useId from '../../../../shared/helpers/useId'
import Step, { Props as StepProps } from '../Step'
import WizardContext, {
OnStepChange,
+ OnStepChangeOptions,
OnStepsChangeMode,
SetActiveIndexOptions,
StepIndex,
@@ -148,22 +149,25 @@ function WizardContainer(props: Props) {
preventNextStepRef.current = shouldPrevent
}, [])
- const getStepChangeOptions = useCallback(
- (index: StepIndex) => {
- const previousIndex = activeIndexRef.current
- const options = {
- previousIndex,
- preventNavigation,
- }
- const id = stepsRef.current[index]?.id
- if (id) {
- Object.assign(options, { id })
- }
+ const getStepChangeOptions: (index: StepIndex) => OnStepChangeOptions =
+ useCallback(
+ (index) => {
+ const previousIndex = activeIndexRef.current
+ const options = {
+ previousIndex,
+ preventNavigation,
+ }
- return options
- },
- [preventNavigation]
- )
+ const id = stepsRef.current[index]?.id
+ if (id) {
+ const previousId = stepsRef.current[previousIndex]?.id
+ Object.assign(options, { id, previousId })
+ }
+
+ return options
+ },
+ [preventNavigation]
+ )
const callOnStepChange = useCallback(
async (index: StepIndex, mode: OnStepsChangeMode) => {
@@ -213,11 +217,14 @@ function WizardContainer(props: Props) {
)
}
- const result =
- skipStepChangeCall ||
- (skipStepChangeCallBeforeMounted && !isInteractionRef.current)
- ? undefined
- : await callOnStepChange(index, mode)
+ let result = undefined
+
+ if (
+ !skipStepChangeCall &&
+ !(skipStepChangeCallBeforeMounted && !isInteractionRef.current)
+ ) {
+ result = await callOnStepChange(index, mode)
+ }
// Hide async indicator
setFormState('abort')
diff --git a/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainerDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainerDocs.ts
index a3b2998ac7c..8626c03e9df 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainerDocs.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainerDocs.ts
@@ -50,7 +50,7 @@ export const WizardContainerProperties: PropertiesTableProps = {
export const WizardContainerEvents: PropertiesTableProps = {
onStepChange: {
- doc: 'Will be called when the user navigate to a different step, with step `index` (number) as the first argument and a string with `previous` or `next` (or `stepListModified` when a step gets replaced) as the second argument, and an object containing `previousIndex`, `id` (if set on the Wizard.Step), `preventNavigation` function as the third parameter. \n\nWhen an async function is provided, it will show an indicator on the submit button during the form submission. All form elements will be disabled during the submit. The indicator will be shown for minimum 1 second. Related Form.Handler props: `minimumAsyncBehaviorTime` and `asyncSubmitTimeout`.',
+ doc: 'Will be called when the user navigate to a different step, with step `index` as the first argument and `previous` or `next` (string) as the second argument, and an options object containing `preventNavigation` function as the third parameter. When an async function is provided, it will show an indicator on the submit button during the form submission. All form elements will be disabled during the submit. The indicator will be shown for minimum 1 second. Related Form.Handler props: `minimumAsyncBehaviorTime` and `asyncSubmitTimeout`.',
type: 'function',
status: 'optional',
},
diff --git a/packages/dnb-eufemia/src/extensions/forms/Wizard/Context/WizardContext.ts b/packages/dnb-eufemia/src/extensions/forms/Wizard/Context/WizardContext.ts
index 005d32903a6..79e6b87bc75 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Wizard/Context/WizardContext.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/Wizard/Context/WizardContext.ts
@@ -3,14 +3,16 @@ import { EventReturnWithStateObject } from '../../types'
import { VisibleWhen } from '../../Form/Visibility'
export type OnStepsChangeMode = 'previous' | 'next' | 'stepListModified'
+export type OnStepChangeOptions = {
+ previousIndex: StepIndex
+ preventNavigation: (shouldPrevent?: boolean) => void
+ id?: string
+ previousId?: string
+}
export type OnStepChange = (
index: StepIndex,
mode: OnStepsChangeMode,
- options: {
- id?: string
- previousIndex: StepIndex
- preventNavigation: (shouldPrevent?: boolean) => void
- }
+ options: OnStepChangeOptions
) =>
| EventReturnWithStateObject
| void
diff --git a/packages/dnb-eufemia/src/extensions/forms/Wizard/stories/Wizard.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Wizard/stories/Wizard.stories.tsx
index c1572f941cc..82bf34a0ad6 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Wizard/stories/Wizard.stories.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Wizard/stories/Wizard.stories.tsx
@@ -42,8 +42,9 @@ export const Basic = () => {
}
export const WizardDynamicStepsActiveWhen = () => {
+ const { createSnapshot, revertSnapshot } = Form.useSnapshot('my-form')
return (
-
+
{
console.log(
@@ -53,23 +54,31 @@ export const WizardDynamicStepsActiveWhen = () => {
args.id,
args.previousIndex
)
+
+ if (mode === 'previous') {
+ revertSnapshot(args.id)
+ } else {
+ createSnapshot(args.previousId)
+ }
}}
>
Step A
+
Step B
+
@@ -78,20 +87,22 @@ export const WizardDynamicStepsActiveWhen = () => {
activeWhen={{
path: '/activeSteps',
hasValue: (value: string) =>
- ['group-1', 'group-2'].includes(value),
+ ['group-a', 'group-b'].includes(value),
}}
id="step-c"
>
Step C
+
Step D
+
@@ -102,8 +113,8 @@ export const WizardDynamicStepsActiveWhen = () => {
optionsLayout="horizontal"
top
>
-
-
+
+
)
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useSnapshot.test.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useSnapshot.test.ts
new file mode 100644
index 00000000000..103bc1babcc
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useSnapshot.test.ts
@@ -0,0 +1,68 @@
+import { renderHook, act } from '@testing-library/react'
+import useSnapshot from '../useSnapshot'
+
+let mockData: any
+const mockSet = jest.fn()
+
+beforeEach(() => {
+ mockData = { value: 'initial' }
+ mockSet.mockClear()
+})
+
+afterEach(() => {
+ jest.restoreAllMocks()
+})
+
+jest.mock('../../Form/data-context/useData', () => {
+ const useDataMock = jest.fn(() => ({
+ data: mockData,
+ set: mockSet,
+ }))
+ return useDataMock
+})
+
+describe('Form.useSnapshot', () => {
+ it('creates a snapshot and retrieves it correctly', () => {
+ const { result } = renderHook(useSnapshot)
+ let snapshotId: string
+
+ act(() => {
+ snapshotId = result.current.createSnapshot()
+ })
+
+ expect(result.current.createSnapshot).toBeDefined()
+ expect(result.current.revertSnapshot).toBeDefined()
+ expect(snapshotId).toBeTruthy()
+ })
+
+ it('reverts to a snapshot', () => {
+ const { result } = renderHook(useSnapshot)
+ act(() => {
+ const snapshotId = result.current.createSnapshot(undefined, {
+ value: 'modified',
+ })
+ mockSet.mockClear()
+ result.current.revertSnapshot(snapshotId)
+ })
+
+ expect(mockSet).toHaveBeenCalledWith({ value: 'modified' })
+ })
+
+ it('reverts and deletes the snapshot', () => {
+ const { result } = renderHook(useSnapshot)
+ let snapshotId: string
+
+ act(() => {
+ snapshotId = result.current.createSnapshot()
+ result.current.revertSnapshot(snapshotId)
+ })
+
+ expect(mockSet).toHaveBeenCalledWith(mockData)
+
+ mockSet.mockClear()
+ act(() => {
+ result.current.revertSnapshot(snapshotId)
+ })
+ expect(mockSet).not.toHaveBeenCalled()
+ })
+})
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useSnapshot.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/useSnapshot.tsx
new file mode 100644
index 00000000000..231074f6dce
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useSnapshot.tsx
@@ -0,0 +1,43 @@
+import { useCallback, useRef } from 'react'
+import { makeUniqueId } from '../../../shared/component-helper'
+import { SharedStateId } from '../../../shared/helpers/useSharedState'
+import useData from '../Form/data-context/useData'
+
+export default function useSnapshot(id?: SharedStateId) {
+ const snapshotsRef = useRef