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 and langz authored Oct 11, 2024
1 parent f67b3bb commit d451793
Show file tree
Hide file tree
Showing 25 changed files with 1,049 additions and 157 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,117 @@
import React from 'react'
import { Button, Card } from '@dnb/eufemia/src'
import ComponentBox from '../../../../../../shared/tags/ComponentBox'
import {
Field,
Form,
Tools,
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">
<Wizard.Container
onStepChange={(index, mode, args) => {
if (mode === 'previous') {
revertSnapshot(String(args.id), 'my-snapshot-slice')
} else {
createSnapshot(
args.previousStep.id,
'my-snapshot-slice',
)
}
}}
>
<Wizard.Step title="Step A" id="step-a">
<Form.Snapshot name="my-snapshot-slice">
<Field.String path="/foo" label="Will be reverted" />
</Form.Snapshot>
<Field.String path="/bar" label="Will stay" />
<Wizard.Buttons />
</Wizard.Step>

<Wizard.Step title="Step B" id="step-b">
<Field.String path="/foo" label="Will be reverted" />
<Field.String path="/bar" label="Will stay" />
<Wizard.Buttons />
</Wizard.Step>
</Wizard.Container>
</Form.Handler>
)
}

return <MyForm />
}}
</ComponentBox>
)
}

export const UndoRedo = () => {
return (
<ComponentBox scope={{ Tools }}>
{() => {
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 (
<>
<Card stack>
<Form.Snapshot name="my-snapshot-slice">
<Field.String
path="/foo"
label="Will be reverted"
onChange={changeHandler}
/>
</Form.Snapshot>
<Field.String path="/bar" label="Will stay" />
</Card>

<Form.ButtonRow>
<Button variant="secondary" onClick={undoHandler}>
Undo
</Button>
<Button variant="secondary" onClick={redoHandler}>
Redo
</Button>
</Form.ButtonRow>

<Tools.Log top />
</>
)
}

return (
<Form.Handler>
<MyComponent />
</Form.Handler>
)
}}
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
showTabs: true
---

import * as Examples from './Examples'

## Demos

### Undo / Redo

<Examples.UndoRedo />

### Used in a Wizard

This example reverts 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,107 @@
---
showTabs: true
---

## Description

The `Form.useSnapshot` hook lets you store data snapshots of your form data, either inside or outside of the form context.

```tsx
import { Form } from '@dnb/eufemia/extensions/forms'

function MyComponent() {
const { createSnapshot, applySnapshot, revertSnapshot } =
Form.useSnapshot()

return <>MyComponent</>
}

render(
<Form.Handler>
<MyComponent />
</Form.Handler>,
)
```

The hook returns an object with the following properties:

- `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.

## Partial data snapshots

In order to create and revert a snapshot for a specific part of the data context, you can use the `Form.Snapshot` component:

```tsx
import { Form, Field } from '@dnb/eufemia/extensions/forms'

function MyForm() {
return (
<Form.Handler>
<Form.Snapshot name="my-snapshot-slice-name">
<Field.String path="/foo" label="Will be reverted" />
<Field.String path="/bar" label="Me too" />
</Form.Snapshot>

<Field.String path="/baz" label="Will stay as before" />
</Form.Handler>
)
}
```

When calling the `createSnapshot` or `revertSnapshot` functions, you can pass in your snapshot `name` (my-snapshot-slice-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-1', 'my-snapshot-slice-name')
revertSnapshot('my-snapshot-1', 'my-snapshot-slice-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 snapshot:

```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.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ To keep track of the current step, you can provide each step with an `id` proper
```tsx
<Wizard.Container
onStepChange={(index, mode, args) => {
const { id, previousIndex, preventNavigation } = args
const {
id,
preventNavigation,
previousStep: { index },
} = args
}}
>
<Wizard.Step
Expand Down
2 changes: 1 addition & 1 deletion packages/dnb-eufemia/src/core/jest/jestSetupScreenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ export const setupPageScreenshot = (
pageViewport = null,
headers = null,
fullscreen = false,
timeout = null,
timeout = 60e3,
matchConfig = null,
} = { url: undefined }
) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
OnSubmitParams,
} from '../types'
import { Props as ProviderProps } from './Provider'
import { SnapshotName } from '../Form/Snapshot'

export type MountState = {
isPreMounted?: boolean
Expand Down Expand Up @@ -160,6 +161,9 @@ export interface ContextState {
valuePropsRef?: React.MutableRefObject<Record<string, ValueProps>>
fieldConnectionsRef?: React.RefObject<Record<Path, FieldConnections>>
mountedFieldsRef?: React.MutableRefObject<Record<Path, MountState>>
snapshotsRef?: React.MutableRefObject<
Map<SnapshotName, Map<Path, unknown>>
>
formElementRef?: React.MutableRefObject<HTMLFormElement>
showAllErrors: boolean
hasVisibleError: boolean
Expand Down
Loading

0 comments on commit d451793

Please sign in to comment.