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 10, 2024
1 parent e932059 commit 51a36c4
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 33 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,97 @@
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') {
revertSnapshot(args.id)
} else {
createSnapshot(args.previousId)
}
}}
>
<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 @@ -17,6 +17,7 @@ import useId from '../../../../shared/helpers/useId'
import Step, { Props as StepProps } from '../Step'
import WizardContext, {
OnStepChange,
OnStepChangeOptions,
OnStepsChangeMode,
SetActiveIndexOptions,
StepIndex,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 51a36c4

Please sign in to comment.