Skip to content

Commit

Permalink
feat(Forms): add Form.useSnapshot hook to handle snapshots of data
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Oct 9, 2024
1 parent e36c224 commit dcd8262
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -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'

<Info />
<Demos />
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import ComponentBox from '../../../../../../shared/tags/ComponentBox'
import { Field, Form, Wizard } from '@dnb/eufemia/src/extensions/forms'

export const InWizard = () => {
return (
<ComponentBox>
{() => {
const MyForm = () => {
const { createSnapshot, revertSnapshot } =
Form.useSnapshot('my-form')

return (
<Form.Handler
id="my-form"
defaultData={{ activeSteps: 'group-1' }}
>
<Wizard.Container
onStepChange={(index, mode, args) => {
if (mode !== 'previous') {
createSnapshot(args.id)
}
}}
onBeforeStepChange={(index, mode, args) => {
if (mode === 'previous') {
revertSnapshot(args.id)
}
}}
>
<Wizard.Step
title="Step A"
id="step-a"
activeWhen={{
path: '/activeSteps',
hasValue: 'group-1',
}}
>
<Form.MainHeading>Step A</Form.MainHeading>
<Field.String path="/foo" label="Content" />
<Wizard.Buttons />
</Wizard.Step>

<Wizard.Step
title="Step B"
id="step-b"
activeWhen={{
path: '/activeSteps',
hasValue: 'group-1',
}}
>
<Form.MainHeading>Step B</Form.MainHeading>
<Field.String path="/foo" label="Content" />
<Wizard.Buttons />
</Wizard.Step>

<Wizard.Step
title="Step C"
id="step-c"
activeWhen={{
path: '/activeSteps',
hasValue: (value: string) =>
['group-1', 'group-2'].includes(value),
}}
>
<Form.MainHeading>Step C</Form.MainHeading>
<Field.String path="/foo" label="Content" />
<Wizard.Buttons />
</Wizard.Step>

<Wizard.Step
title="Step D"
id="step-d"
activeWhen={{
path: '/activeSteps',
hasValue: 'group-2',
}}
>
<Form.MainHeading>Step D</Form.MainHeading>
<Field.String path="/foo" label="Content" />
<Wizard.Buttons />
</Wizard.Step>
</Wizard.Container>

<Field.Selection
path="/activeSteps"
variant="button"
optionsLayout="horizontal"
top
>
<Field.Option value="group-1" title="Group 1" />
<Field.Option value="group-2" title="Group 2" />
</Field.Selection>
</Form.Handler>
)
}

return <MyForm></MyForm>
}}
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
@@ -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.

<Examples.InWizard />
Original file line number Diff line number Diff line change
@@ -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(
<Form.Handler>
<MyComponent />
</Form.Handler>,
)
```

- `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 (
<Form.Handler>
<Component />
</Form.Handler>
)
}

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 (
<>
<Form.Handler id="unique">...</Form.Handler>
<Component />
</>
)
}

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.
1 change: 1 addition & 0 deletions packages/dnb-eufemia/src/extensions/forms/Form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ export const Basic = () => {
}

export const WizardDynamicStepsActiveWhen = () => {
const { createSnapshot, revertSnapshot } = Form.useSnapshot('my-form')
return (
<Form.Handler defaultData={{ activeSteps: 'group-1' }}>
<Form.Handler id="my-form" defaultData={{ activeSteps: 'group-a' }}>
<Wizard.Container
onStepChange={(index, mode, args) => {
console.log(
Expand All @@ -53,23 +54,40 @@ export const WizardDynamicStepsActiveWhen = () => {
args.id,
args.previousIndex
)
if (mode !== 'previous') {
createSnapshot(args.id)
}
}}
onBeforeStepChange={(index, mode, args) => {
console.log(
'onBeforeStepChange',
index,
mode,
args.id,
args.previousIndex
)
if (mode === 'previous') {
revertSnapshot(args.id)
}
}}
>
<Wizard.Step
title="Step A"
activeWhen={{ path: '/activeSteps', hasValue: 'group-1' }}
activeWhen={{ path: '/activeSteps', hasValue: 'group-a' }}
id="step-a"
>
<Form.MainHeading>Step A</Form.MainHeading>
<Field.String path="/foo" label="Content" />
<Wizard.Buttons />
</Wizard.Step>

<Wizard.Step
title="Step B"
activeWhen={{ path: '/activeSteps', hasValue: 'group-1' }}
activeWhen={{ path: '/activeSteps', hasValue: 'group-a' }}
id="step-b"
>
<Form.MainHeading>Step B</Form.MainHeading>
<Field.String path="/foo" label="Content" />
<Wizard.Buttons />
</Wizard.Step>

Expand All @@ -78,20 +96,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"
>
<Form.MainHeading>Step C</Form.MainHeading>
<Field.String path="/foo" label="Content" />
<Wizard.Buttons />
</Wizard.Step>

<Wizard.Step
title="Step D"
activeWhen={{ path: '/activeSteps', hasValue: 'group-2' }}
activeWhen={{ path: '/activeSteps', hasValue: 'group-b' }}
id="step-d"
>
<Form.MainHeading>Step D</Form.MainHeading>
<Field.String path="/foo" label="Content" />
<Wizard.Buttons />
</Wizard.Step>
</Wizard.Container>
Expand All @@ -102,8 +122,8 @@ export const WizardDynamicStepsActiveWhen = () => {
optionsLayout="horizontal"
top
>
<Field.Option value="group-1" title="Group 1" />
<Field.Option value="group-2" title="Group 2" />
<Field.Option value="group-a" title="Group A" />
<Field.Option value="group-b" title="Group B" />
</Field.Selection>
</Form.Handler>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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', () => {
return jest.fn(() => ({
data: mockData,
set: mockSet,
}))
})

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()
})
})
Loading

0 comments on commit dcd8262

Please sign in to comment.