Skip to content

Commit

Permalink
feat(Forms): add support for using a function instance as a reference…
Browse files Browse the repository at this point in the history
… instead of a string based id
  • Loading branch information
tujoworker committed Nov 22, 2024
1 parent 69dc60a commit afe1278
Show file tree
Hide file tree
Showing 14 changed files with 228 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const { getValue, data, filterData, reduceToVisibleFields } =
- `filterData` will filter the data based on your own logic.
- `reduceToVisibleFields` will reduce the given data set to only contain the visible fields (mounted fields).

You link them together via the `id` (string) property.
You link them together via the `id` (string, function or React Context reference) property.

TypeScript support:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function Component() {
}
```

You link them together via the `id` (string) property.
You link them together via the `id` (string, function or React Context reference) property.

Related helpers:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ render(

## Usage

You can use the `Form.useData` 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.
You can use the `Form.useData` hook with or without an `id` (string, function or React Context reference) 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

Expand All @@ -66,7 +66,7 @@ function Component() {

### With an `id` property

While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string) property:
While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string, function or React Context reference) property:

```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ 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.
You can use the `Form.useSnapshot` hook with or without an `id` (string, function or React Context reference) 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

Expand All @@ -85,7 +85,7 @@ function Component() {

### With an `id` property

While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string) property:
While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string, function or React Context reference) property:

```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function Component() {
}
```

Or by linking the hook together with the form by using the `id` (string) property:
Or by linking the hook together with the form by using the `id` (string, function or React Context reference) property:

```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const MyForm = () => {
}
```

When using the `useStep` hook outside of the `Wizard.Container` context, you need to provide an unique `id` (string):
When using the `useStep` hook outside of the `Wizard.Container` context, you need to provide an unique `id` (string, function or React Context reference):

```tsx
import { Form, Wizard } from '@dnb/eufemia/extensions/forms'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function MyForm() {

## Without a router

You connect the hook with the `Wizard.Container` component via an unique `id` (string). The `id` will be used in the URL query string: `url?unique-id-step=1`.
You connect the hook with the `Wizard.Container` component via an unique `id` (string, function or React Context reference). The `id` will be used in the URL query string: `url?unique-id-step=1`.

```jsx
import { Form, Wizard } from '@dnb/eufemia/extensions/forms'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function MyForm() {
}
```

You can also connect the hook with the `Wizard.Container` via an `id` (string). This lets you render the hook outside of the context:
You can also connect the hook with the `Wizard.Container` via an `id` (string, function or React Context reference). This lets you render the hook outside of the context:

```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import {
Path,
EventStateObject,
EventReturnWithStateObject,
Identifier,
FieldProps,
ValueProps,
OnChange,
OnSubmitParams,
} from '../types'
import { Props as ProviderProps } from './Provider'
import { SnapshotName } from '../Form/Snapshot'
import { SharedStateId } from '../../../shared/helpers/useSharedState'

export type MountState = {
isPreMounted?: boolean
Expand Down Expand Up @@ -85,7 +85,7 @@ export type FieldConnections = {
}

export interface ContextState {
id?: Identifier
id?: SharedStateId
hasContext: boolean
/** The dataset for the form / form wizard */
data: any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ import { debounce } from '../../../../shared/helpers'
import FieldPropsProvider from '../../Field/Provider'
import useUpdateEffect from '../../../../shared/helpers/useUpdateEffect'
import { isAsync } from '../../../../shared/helpers/isAsync'
import { useSharedState } from '../../../../shared/helpers/useSharedState'
import {
SharedStateId,
useSharedState,
} from '../../../../shared/helpers/useSharedState'
import SharedContext, { ContextProps } from '../../../../shared/Context'
import useTranslation from '../../hooks/useTranslation'
import DataContext, {
Expand Down Expand Up @@ -74,7 +77,7 @@ export interface Props<Data extends JsonObject>
/**
* Unique ID to communicate with the hook Form.useData
*/
id?: string
id?: SharedStateId
/**
* Unique ID to connect with a GlobalStatus
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { createContext } from 'react'
import { renderHook, act, render, fireEvent } from '@testing-library/react'
import { makeUniqueId } from '../../../../../shared/component-helper'
import { Field, Form, Wizard } from '../../..'
Expand Down Expand Up @@ -124,6 +124,68 @@ describe('Form.useData', () => {
expect(result.current.data).toEqual({ key: 'changed value' })
})

it('should get data with handler id', () => {
const { result } = renderHook(() => useData(identifier), {
wrapper: ({ children }) => (
<>
<Provider id={identifier}>
<Field.String path="/foo" defaultValue="foo" />
<Field.String path="/bar" defaultValue="bar" />
</Provider>

{children}
</>
),
})

expect(result.current.data).toEqual({
foo: 'foo',
bar: 'bar',
})
})

it('should get data with a function instance as the id', () => {
const myId = () => null
const { result } = renderHook(() => useData(myId), {
wrapper: ({ children }) => (
<>
<Provider id={myId}>
<Field.String path="/foo" defaultValue="foo" />
<Field.String path="/bar" defaultValue="bar" />
</Provider>

{children}
</>
),
})

expect(result.current.data).toEqual({
foo: 'foo',
bar: 'bar',
})
})

it('should get data with a React Context as the id', () => {
const myId = createContext(null)
const { result } = renderHook(() => useData(myId), {
wrapper: ({ children }) => (
<>
<Provider id={myId}>
<Field.String path="/foo" defaultValue="foo" />
<Field.String path="/bar" defaultValue="bar" />
</Provider>

{children}
</>
),
})

expect(result.current.data).toEqual({
foo: 'foo',
bar: 'bar',
})
})

describe('remove', () => {
it('should remove the data', () => {
const { result } = renderHook(() => useData(), {
Expand Down Expand Up @@ -177,6 +239,35 @@ describe('Form.useData', () => {
})
expect(result.current.data).not.toHaveProperty('foo')
})

it('should remove data with handler id', () => {
const { result } = renderHook(() => useData(identifier), {
wrapper: ({ children }) => (
<>
<Provider id={identifier}>
<Field.String path="/foo" defaultValue="foo" />
<Field.String path="/bar" defaultValue="bar" />
</Provider>

{children}
</>
),
})

expect(result.current.data).toEqual({
foo: 'foo',
bar: 'bar',
})

act(() => {
result.current.remove('/foo')
})

expect(result.current.data).toEqual({
bar: 'bar',
})
expect(result.current.data).not.toHaveProperty('foo')
})
})

it('"update" should only re-render when value has changed', () => {
Expand Down Expand Up @@ -225,40 +316,71 @@ describe('Form.useData', () => {
expect(result.current.data).toEqual({ key: 'changed value' })
})

it('should sync two hooks by using "update"', () => {
const props = { key: 'value' }
describe('update', () => {
it('should sync two hooks by using "update"', () => {
const props = { key: 'value' }

const { result: A } = renderHook(() => useData(identifier))
const { result: B } = renderHook(() => useData(identifier, props))
const { result: A } = renderHook(() => useData(identifier))
const { result: B } = renderHook(() => useData(identifier, props))

expect(A.current.data).toEqual({ key: 'value' })
expect(B.current.data).toEqual({ key: 'value' })
expect(A.current.data).toEqual({ key: 'value' })
expect(B.current.data).toEqual({ key: 'value' })

act(() => {
B.current.update('/key', (value) => {
return 'changed ' + value
act(() => {
B.current.update('/key', (value) => {
return 'changed ' + value
})
})

expect(A.current.data).toEqual({ key: 'changed value' })
expect(B.current.data).toEqual({ key: 'changed value' })
})

expect(A.current.data).toEqual({ key: 'changed value' })
expect(B.current.data).toEqual({ key: 'changed value' })
})
it('should support update without a function', () => {
const props = { key: 'value' }

it('should support update without a function', () => {
const props = { key: 'value' }
const { result: A } = renderHook(() => useData(identifier))
const { result: B } = renderHook(() => useData(identifier, props))

const { result: A } = renderHook(() => useData(identifier))
const { result: B } = renderHook(() => useData(identifier, props))
expect(A.current.data).toEqual({ key: 'value' })
expect(B.current.data).toEqual({ key: 'value' })

expect(A.current.data).toEqual({ key: 'value' })
expect(B.current.data).toEqual({ key: 'value' })
act(() => {
B.current.update('/key', 'new value')
})

act(() => {
B.current.update('/key', 'new value')
expect(A.current.data).toEqual({ key: 'new value' })
expect(B.current.data).toEqual({ key: 'new value' })
})

expect(A.current.data).toEqual({ key: 'new value' })
expect(B.current.data).toEqual({ key: 'new value' })
it('should update data with handler id', () => {
const { result } = renderHook(() => useData(identifier), {
wrapper: ({ children }) => (
<>
<Provider id={identifier}>
<Field.String path="/foo" defaultValue="foo" />
<Field.String path="/bar" defaultValue="bar" />
</Provider>

{children}
</>
),
})

expect(result.current.data).toEqual({
foo: 'foo',
bar: 'bar',
})

act(() => {
result.current.update('/foo', 'updated')
})

expect(result.current.data).toEqual({
foo: 'updated',
bar: 'bar',
})
})
})

it('should rerender when shared state calls "set"', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createSharedState } from '../../../../shared/helpers/useSharedState'
import {
SharedStateId,
createSharedState,
} from '../../../../shared/helpers/useSharedState'
import { SharedAttachments } from '../../DataContext/Provider'

export default function clearData(id: string) {
export default function clearData(id: SharedStateId) {
const sharedAttachments = createSharedState<SharedAttachments<unknown>>(
id + '-attachments'
)
Expand Down
Loading

0 comments on commit afe1278

Please sign in to comment.