Skip to content

Commit

Permalink
feat(Form.useData): add data handler to get forms data outside of the…
Browse files Browse the repository at this point in the history
… context
  • Loading branch information
tujoworker committed Jan 12, 2024
1 parent 17fa111 commit ed2f726
Show file tree
Hide file tree
Showing 23 changed files with 376 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,6 @@ Tertiary button with long text and text `wrap` enabled.

<ButtonDisabled />

### Error state

Buttons can have an error state

<ButtonErrorState />

<VisibilityByTheme hidden="sbanken">

### Signal button
Expand All @@ -90,3 +84,9 @@ Large Signal button with medium sized icon. To import custom icons, use: `import
This is, as all of the demos, only an example of how to achieve various needs, and not that you should do it.

<ButtonCustomContent />

<VisibleWhenVisualTest>

<ButtonErrorState />

</VisibleWhenVisualTest>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
title: 'VisuallyHidden'
description: 'VisuallyHidden has all the styles necessary to hide it from visual clients, but keep it for screen readers.'
showTabs: true
hideTabs:
- title: Events
theme: 'sbanken'
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,27 @@ render(
)
```

Or together with the `Form.getData` hook:

```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
function Component() {
const data = Form.useData('unique', existingData)

return (
<Form.Handler
id="unique"
onChange={...}
onSubmit={...}
>
...
</Form.Handler>
)
}
```

You decide where you want to provide the initial `data`. It can be done via the `Form.Handler` component, or via the `Form.useData` Hook – or even in each Field, with the value property.

## Philosophy

Eufemia Forms is:
Expand All @@ -89,6 +110,7 @@ In summary:
- Ready to use data driven form components.
- All functionality in all components can be controlled and overridden via props.
- State management using the declarative [JSON Pointer](https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-03) directive (i.e `path="/firstName"`).
- State can be handled outside of the provider context [Form.Handler](/uilib/extensions/forms/extended-features/Form/Handler).
- Simple validation (like `minLength` on text fields) as well as [Ajv JSON schema validator](https://ajv.js.org/) support on both single fields and the whole data set.
- Building blocks for [creating custom field components](/uilib/extensions/forms/create-component).
- Static [value components](/uilib/extensions/forms/extended-features/Value/) for displaying data with proper formatting.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export const DropdownLabel = () => (
<Field.Selection
label="Label text"
onChange={(value) => console.log('onChange', value)}
/>
>
<Field.Option value="foo" title="Foo!" />
<Field.Option value="bar" title="Baar!" />
</Field.Selection>
</ComponentBox>
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,34 @@ render(
)
```

## Form.getData

State can be handled outside of the form. This is useful if you want to use the form data in other components:

```jsx
import { Form } from '@dnb/eufemia/extensions/forms'
function Component() {
const data = Form.useData('unique')

return <Form.Handler id="unique">...</Form.Handler>
}
```

You decide where you want to provide the initial `data`. It can be done via the `Form.Handler` component, or via the `Form.useData` Hook – or even in each Field, with the value property:

```jsx
import { Form, Field } from '@dnb/eufemia/extensions/forms'
function Component() {
const data = Form.useData('unique', existingDataFoo)

return (
<Form.Handler id="unique" data={existingDataBar}>
<Field.String path="/foo" value={existingValue} />
</Form.Handler>
)
}
```

## Browser autofill

You can set `autoComplete` on the `Form.Handler` – each [Field.String](/uilib/extensions/forms/base-fields/String/)-field will then get `autoComplete="on"`:
Expand Down
7 changes: 7 additions & 0 deletions packages/dnb-eufemia/src/components/flex/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ function FlexContainer(props: Props) {
spacing
}

if (
React.isValidElement(previousChild) &&
previousChild?.type?.['_supportsSpacingProps'] === false
) {
startSpacing = 0
}

const space =
direction === 'horizontal'
? { [start]: endSpacing, [end]: startSpacing }
Expand Down
21 changes: 16 additions & 5 deletions packages/dnb-eufemia/src/components/flex/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,18 @@ export const isEufemiaElement = (element): boolean => {
export const isSpacePropsComponent = (
element: React.ReactNode
): boolean => {
return (
(React.isValidElement(element) &&
element?.type?.['_supportsSpacingProps'] === true) ||
isEufemiaElement(element)
)
if (
React.isValidElement(element) &&
typeof element?.type?.['_supportsSpacingProps'] === 'boolean'
) {
return element?.type?.['_supportsSpacingProps']
}

if (isEufemiaElement(element)) {
return true
}

return undefined
}

export const renderWithSpacing = (
Expand All @@ -69,6 +76,10 @@ export const renderWithSpacing = (
) => {
const takesSpaceProps = isSpacePropsComponent(element)

if (takesSpaceProps === false) {
return element
}

return takesSpaceProps ? (
React.cloneElement(element as React.ReactElement<unknown>, props)
) : (
Expand Down
2 changes: 1 addition & 1 deletion packages/dnb-eufemia/src/components/upload/useUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type useUploadReturn = {
* Use together with Upload with the same id to manage the files from outside the component.
*/
function useUpload(id: string): useUploadReturn {
const { data, update } = useEventEmitter(id)
const { data, update } = useEventEmitter<useUploadReturn>(id) || {}

const setFiles = (files: UploadFile[]) => {
update({ files })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@ const VisuallyHidden = (localProps: VisuallyHiddenAllProps) => {
)
}

VisuallyHidden._supportsSpacingProps = true
VisuallyHidden._supportsSpacingProps = false

export default VisuallyHidden
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { JSONSchema7 } from 'json-schema'
import { ValidateFunction } from 'ajv'
import ajv, { ajvErrorsToFormErrors } from '../../utils/ajv'
import { FormError } from '../../types'
import { useEventEmitter } from '../../../../shared/helpers/useEventEmitter'
import useMountEffect from '../../hooks/useMountEffect'
import useUpdateEffect from '../../hooks/useUpdateEffect'
import Context, { ContextState } from '../Context'
Expand All @@ -21,6 +22,8 @@ import Context, { ContextState } from '../Context'
import structuredClone from '@ungap/structured-clone'

export interface Props<Data extends JsonObject> {
/** Unique ID to communicate with the hook useData */
id?: string
/** Default source data, only used if no other source is available, and not leading to updates if changed after mount */
defaultData?: Partial<Data>
/** Dynamic source data used as both initial data, and updates internal data if changed after mount */
Expand Down Expand Up @@ -65,6 +68,7 @@ function removeListPath(paths: PathList, path: string): PathList {
const isArrayJsonPointer = /^\/\d+(\/|$)/

export default function Provider<Data extends JsonObject>({
id,
defaultData,
data,
schema,
Expand Down Expand Up @@ -111,6 +115,14 @@ export default function Provider<Data extends JsonObject>({
const dataCacheRef = useRef<Partial<Data>>(data)
// - Validator
const ajvSchemaValidatorRef = useRef<ValidateFunction>()
// - Emitter data
const { data: emitterData, update: updateEmitter } = useEventEmitter(
id,
data
)
if (emitterData && !(data ?? defaultData)) {
internalDataRef.current = emitterData as Data
}

const validateData = useCallback(() => {
if (!ajvSchemaValidatorRef.current) {
Expand Down Expand Up @@ -193,11 +205,15 @@ export default function Provider<Data extends JsonObject>({

internalDataRef.current = newData

if (id) {
updateEmitter?.(newData)
}

forceUpdate()

return newData
},
[sessionStorageId]
[id, sessionStorageId, updateEmitter]
)

const handlePathChange = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
act,
fireEvent,
render,
renderHook,
screen,
waitFor,
} from '@testing-library/react'
Expand Down Expand Up @@ -820,4 +821,75 @@ describe('DataContext.Provider', () => {
expect(screen.queryByRole('alert')).toBeInTheDocument()
})
})

describe('useData', () => {
it('should set Provider data', () => {
renderHook(() => Form.useData('unique', { foo: 'bar' }))

render(
<DataContext.Provider id="unique">
<Field.String path="/foo" />
</DataContext.Provider>
)

const inputElement = document.querySelector('input')
expect(inputElement).toHaveValue('bar')
})

it('should update Provider data on hook rerender', () => {
const { rerender } = renderHook((props = { foo: 'bar' }) => {
return Form.useData('unique-a', props)
})

render(
<DataContext.Provider id="unique-a">
<Field.String path="/foo" />
</DataContext.Provider>
)

const inputElement = document.querySelector('input')

expect(inputElement).toHaveValue('bar')

rerender({ foo: 'bar-changed' })

expect(inputElement).toHaveValue('bar-changed')
})

it('should only set data when Provider has no data given', () => {
renderHook(() => Form.useData('unique-b', { foo: 'bar' }))

render(
<DataContext.Provider id="unique-b" data={{ foo: 'changed' }}>
<Field.String path="/foo" />
</DataContext.Provider>
)

const inputElement = document.querySelector('input')

expect(inputElement).toHaveValue('changed')
})

it('should initially set data when Provider has no data', () => {
renderHook(() => Form.useData('unique-c', { foo: 'bar' }))

const { rerender } = render(
<DataContext.Provider id="unique-c">
<Field.String path="/foo" />
</DataContext.Provider>
)

const inputElement = document.querySelector('input')

expect(inputElement).toHaveValue('bar')

rerender(
<DataContext.Provider id="unique-c" data={{ foo: 'changed' }}>
<Field.String path="/foo" />
</DataContext.Provider>
)

expect(inputElement).toHaveValue('changed')
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function DateComponent(props: Props) {
show_input={true}
show_cancel_button={true}
show_reset_button={true}
status={error || hasError ? 'error' : undefined}
status={hasError ? 'error' : undefined}
suffix={
help ? (
<HelpButton title={help.title}>{help.contents}</HelpButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface IOption {
}

export type Props = FieldHelpProps &
FieldProps<string | number> & {
FieldProps<IOption['value']> & {
children?: React.ReactNode
variant?: 'dropdown' | 'radio' | 'button'
clear?: boolean
Expand Down Expand Up @@ -252,7 +252,6 @@ function Selection(props: Props) {
on_change={handleDropdownChange}
on_show={handleShow}
on_hide={handleHide}
{...pickSpacingProps(props)}
stretch
/>
</FieldBlock>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ function StringComponent(props: Props) {
disabled: disabled,
stretch: width !== undefined,
inner_ref: innerRef,
status: error || hasError ? 'error' : undefined,
status: hasError ? 'error' : undefined,
value: transformInstantly(value?.toString() ?? ''),
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default function FormHandler<Data extends JsonObject>({
...rest
}: ProviderProps<Data> & Omit<Props, keyof ProviderProps<Data>>) {
const providerProps = {
id: rest.id,
defaultData,
data,
schema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ describe('Form.Handler', () => {
const onSubmit = jest.fn()

render(
<Form.Handler defaultData={{ foo: 'bar' }} onSubmit={onSubmit}>
<Form.Handler data={{ foo: 'bar' }} onSubmit={onSubmit}>
<Field.String path="/other" value="include this" />
<Form.SubmitButton />
</Form.Handler>,
Expand Down
Loading

0 comments on commit ed2f726

Please sign in to comment.