Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Forms): auto-open iterate container items when validation errors and make Iterate.Toolbar fully customizable #3877

Merged
merged 23 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6f64cf6
feat(Forms): add `containerMode`, `minimumRequiredItems` and `hideToo…
tujoworker Aug 28, 2024
ed2d26d
Rename props to `minimumContainerItems` and `hideContainerToolbarWhen`
tujoworker Sep 9, 2024
153c66b
Add docs about the new props and make it more clear what containers t…
tujoworker Sep 9, 2024
a189ba6
Fix focus management when empty and invalid item goes from view to ed…
tujoworker Sep 9, 2024
b23627b
Revert visual snapshot change
tujoworker Sep 9, 2024
66e8b46
Add Tools.Log
tujoworker Sep 10, 2024
a67cadd
Array: disable flex support to avoid re-render, which could result in…
tujoworker Sep 10, 2024
2dd7536
Refractor example to remove flickering
tujoworker Sep 10, 2024
c3e9a95
chore(Forms): ensure error boundary re-render when reported
tujoworker Sep 10, 2024
4adaa3e
Enhance focus management to avoid double focus which could result in …
tujoworker Sep 10, 2024
331d249
Enhance and sync docs description
tujoworker Sep 10, 2024
e2eeb93
Remove props in favor of customizing the toolbar
tujoworker Sep 10, 2024
acd4514
Refactor naming of Array
tujoworker Sep 10, 2024
41f8f65
Add change log
tujoworker Sep 10, 2024
4dd96bb
Ensure onBlurValidator can be called in sync during submit
tujoworker Sep 10, 2024
5577cb9
Refactor the buttons to be exported in Iterate.EditContainer and Iter…
tujoworker Sep 11, 2024
89779ea
Refactor Toolbar to be included as JSX
tujoworker Sep 11, 2024
bb0dda9
Add back the error notification
tujoworker Sep 11, 2024
c66e459
Refactor of ArrayElement and its context
tujoworker Sep 11, 2024
ebb2565
Refactor button tests
tujoworker Sep 12, 2024
d131df9
Fix potential wrong focus setting
tujoworker Sep 12, 2024
9ea99e8
Update packages/dnb-eufemia/src/extensions/forms/Iterate/PushContaine…
tujoworker Sep 12, 2024
a116b01
Update packages/dnb-eufemia/src/extensions/forms/Iterate/PushContaine…
tujoworker Sep 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Field,
Value,
Form,
Tools,
} from '@dnb/eufemia/src/extensions/forms'
export { Default as AnimatedContainer } from '../AnimatedContainer/Examples'

Expand Down Expand Up @@ -172,7 +173,7 @@ export const ArrayFromFormHandler = () => {
}}
onChange={(data) => console.log('DataContext/onChange', data)}
>
<Flex.Vertical>
<Flex.Stack>
<Form.MainHeading>Avengers</Form.MainHeading>

<Card stack>
Expand Down Expand Up @@ -212,7 +213,7 @@ export const ArrayFromFormHandler = () => {
pushValue={{}}
/>
</Card>
</Flex.Vertical>
</Flex.Stack>
</Form.Handler>
</ComponentBox>
)
Expand Down Expand Up @@ -282,7 +283,7 @@ export const ViewAndEditContainer = () => {
accounts: [
{
firstName: 'Tony',
lastName: undefined, // initiate error
lastName: 'Rogers',
},
],
}}
Expand All @@ -291,7 +292,7 @@ export const ViewAndEditContainer = () => {
}
onSubmit={async (data) => console.log('onSubmit', data)}
>
<Flex.Vertical>
<Flex.Stack>
<Form.MainHeading>Accounts</Form.MainHeading>

<Card stack>
Expand All @@ -304,7 +305,7 @@ export const ViewAndEditContainer = () => {
</Card>

<Form.SubmitButton variant="send" />
</Flex.Vertical>
</Flex.Stack>
</Form.Handler>
)
}
Expand Down Expand Up @@ -364,3 +365,69 @@ export const WithVisibility = () => {
</ComponentBox>
)
}

export const InitialOpen = () => {
return (
<ComponentBox scope={{ Iterate, Tools }}>
<Form.Handler
onSubmit={async (data) => console.log('onSubmit', data)}
onSubmitRequest={() => console.log('onSubmitRequest')}
>
<Flex.Stack>
<Form.MainHeading>Statsborgerskap</Form.MainHeading>

<Card align="stretch">
<Iterate.Array path="/countries" defaultValue={[null]}>
<Iterate.ViewContainer>
<Value.SelectCountry
label="Land du er statsborger i"
itemPath="/"
/>
<Iterate.Toolbar>
{({ items }) =>
items.length === 1 ? (
<Iterate.ViewContainer.EditButton />
) : (
<>
<Iterate.ViewContainer.EditButton />
<Iterate.ViewContainer.RemoveButton />
</>
)
}
</Iterate.Toolbar>
</Iterate.ViewContainer>

<Iterate.EditContainer>
<Field.SelectCountry
label="Land du er statsborger i"
itemPath="/"
required
/>
<Iterate.Toolbar>
{({ items }) =>
items.length === 1 ? null : (
<>
<Iterate.EditContainer.DoneButton />
<Iterate.EditContainer.CancelButton />
</>
)
}
</Iterate.Toolbar>
</Iterate.EditContainer>
</Iterate.Array>

<Iterate.PushButton
path="/countries"
pushValue={null}
text="Legg til flere statsborgerskap"
/>
</Card>

<Form.SubmitButton variant="send" />

<Tools.Log />
</Flex.Stack>
</Form.Handler>
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import * as Examples from './Examples'

### Render props with primitive items

You can provide the child as a function that receives the value of the item as the first argument, and the index of which item you are on as the second.
tujoworker marked this conversation as resolved.
Show resolved Hide resolved

<Examples.RenderPropsPrimitiveItems />

### Render props with object items
Expand All @@ -47,6 +49,14 @@ With an optional `title` and [Iterate.Toolbar](/uilib/extensions/forms/Iterate/T

<Examples.ViewAndEditContainer />

### Initially open

This example uses a customized [Iterate.Toolbar](/uilib/extensions/forms/Iterate/Toolbar/).

It hides the toolbar in the `EditContainer` when there is only one item in the array. And it hides the remove button in the `ViewContainer` when there is only one item in the array.

<Examples.InitialOpen />

### With DataContext and add/remove buttons

<Examples.ArrayFromFormHandler />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ render(
)
```

### Initial container mode

This section describes the behavior of the `EditContainer` and the `ViewContainer` components.

By default, the container mode is set to `auto`. This means that the container will open (switch to `edit` mode) when there is an error in the container or the value is falsy (empty string, null, undefined, etc.).

When a new item is added via the [Iterate.PushButton](/uilib/extensions/forms/Iterate/PushButton/) component, the item before it will change to `view` mode, if it had no validation errors.

## Filter data

You can filter data by paths specific or all paths.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ render(
)
```

## Customize the Toolbar

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

render(
<Iterate.Array>
<Iterate.EditContainer>
<Field.Name.Last itemPath="/name" />
<Iterate.Toolbar>
<Iterate.EditContainer.DoneButton />
<Iterate.EditContainer.CancelButton />
</Iterate.Toolbar>
</Iterate.EditContainer>
</Iterate.Array>,
)
```

## Get the internal item object

You can get the internal item object by using the `Iterate.useItem` hook.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const InitiallyOpen = () => {
}
onSubmit={async (data) => console.log('onSubmit', data)}
>
<Flex.Vertical>
<Flex.Stack>
<Form.MainHeading>Accounts</Form.MainHeading>

<Card stack>
Expand All @@ -87,7 +87,7 @@ export const InitiallyOpen = () => {
</Card>

<Form.SubmitButton variant="send" />
</Flex.Vertical>
</Flex.Stack>
</Form.Handler>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ hideInMenu: true

Use `Iterate.Toolbar` to enhance each item in the array with additional functionality. It's particularly useful within components like [Iterate.AnimatedContainer](/uilib/extensions/forms/Iterate/AnimatedContainer) to incorporate a toolbar with extra tools.

The Toolbar is integrated into the containers [Iterate.ViewContainer](/uilib/extensions/forms/Iterate/ViewContainer/) and [Iterate.EditContainer](/uilib/extensions/forms/Iterate/EditContainer/).

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

Expand All @@ -22,4 +24,50 @@ render(
)
```

The Toolbar is integrated into the containers [Iterate.ViewContainer](/uilib/extensions/forms/Iterate/ViewContainer/) and [Iterate.EditContainer](/uilib/extensions/forms/Iterate/EditContainer/).
## Customize the Toolbar

You can customize the toolbar by either passing a function as a child or as a JSX element:

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

render(
<Iterate.Array>
<Iterate.ViewContainer>
Item Content
<Iterate.Toolbar>
<Iterate.ViewContainer.EditButton />
<Iterate.ViewContainer.RemoveButton />
</Iterate.Toolbar>
</Iterate.ViewContainer>
</Iterate.Array>,
)
```

The function receives the following parameters as an object:

- `index` the index of the current item in the array.
- `value` the value of the current item.
- `items` the array of items.

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

render(
<Iterate.Array>
<Iterate.ViewContainer>
Item Content
<Iterate.Toolbar>
{({ items, index, value }) => {
return items.length === 1 ? null : (
<>
<Iterate.ViewContainer.EditButton />
<Iterate.ViewContainer.RemoveButton />
</>
)
}}
</Iterate.Toolbar>
</Iterate.ViewContainer>
</Iterate.Array>,
)
```
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ render(
You can use the `{itemNr}` variable in the `title` property to display the current item number.

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

render(
<Iterate.Array>
Expand All @@ -44,6 +44,25 @@ render(
)
```

## Customize the Toolbar

```tsx
import { Iterate, Value } from '@dnb/eufemia/extensions/forms'

render(
<Iterate.Array>
<Iterate.ViewContainer>
<Value.Name.Last itemPath="/name" />

<Iterate.Toolbar>
<Iterate.ViewContainer.EditButton />
<Iterate.ViewContainer.RemoveButton />
</Iterate.Toolbar>
</Iterate.ViewContainer>
</Iterate.Array>,
)
```

## Accessibility

The `Iterate.ViewContainer` component has an `aria-label` attribute, which is set to the `title` prop value. It uses a section element to wrap the content, which helps users with screen readers to get the needed announcement.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ breadcrumb:

Change log for the Eufemia Forms extension.

## v10.48

- Make [Iterate.Toolbar](/uilib/extensions/forms/Iterate/Toolbar/) customizable.

## v10.46

- Added [Value.SelectCountry](/uilib/extensions/forms/Value/SelectCountry/) component to render a country value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export interface ContextState {
handlePathChangeUnvalidated: (path: Path, value: any) => void
updateDataValue: (path: Path, value: any) => void
setData: (data: any) => void
clearData?: () => void
mutateDataHandler?: (data: any, mutate: TransformData) => any
filterDataHandler?: (data: any, filter: FilterData) => any
validateData: () => void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function FieldBoundaryProvider({ children }) {

const errorsRef = useRef<Record<Path, boolean>>({})
const showBoundaryErrorsRef = useRef<boolean>(false)
const hasError = Object.keys(errorsRef.current || {}).length > 0
const hasError = Object.keys(errorsRef.current).length > 0
const hasSubmitError = showAllErrors && hasError

const setFieldError = useCallback((path: Path, error: Error) => {
Expand All @@ -20,6 +20,7 @@ export default function FieldBoundaryProvider({ children }) {
} else {
delete errorsRef.current?.[path]
}
forceUpdate()
}, [])

const setShowBoundaryErrors = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,7 @@ export default function Provider<Data extends JsonObject>(
validateData,
updateDataValue,
setData,
clearData,
filterDataHandler,
addOnChangeHandler,
setHandleSubmit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ function SelectCountry(props: Props) {
label={label}
input_icon={false}
data={dataRef.current}
value={value}
value={typeof value === 'string' ? value : null}
disabled={disabled}
on_show={fillData}
on_focus={onFocusHandler}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ describe('Field.String', () => {
await userEvent.type(input, '{Backspace>3}aBc')

expect(input).toHaveValue('ABC')
expect(transformIn).toHaveBeenCalledTimes(13)
expect(transformIn).toHaveBeenCalledTimes(7)
expect(transformIn).toHaveBeenLastCalledWith('abc')
expect(transformOut).toHaveBeenCalledTimes(6)
expect(transformOut).toHaveBeenLastCalledWith('ABc')
Expand All @@ -289,7 +289,7 @@ describe('Field.String', () => {
await userEvent.type(input, '{Backspace>3}EfG')

expect(input).toHaveValue('EFG')
expect(transformIn).toHaveBeenCalledTimes(25)
expect(transformIn).toHaveBeenCalledTimes(13)
expect(transformIn).toHaveBeenLastCalledWith('efg')
expect(transformOut).toHaveBeenCalledTimes(12)
expect(transformOut).toHaveBeenLastCalledWith('EFG')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ function IsolationProvider<Data extends JsonObject>(
// Commit the internal data to the nested context data
handlePathChangeOuter?.(
path,
extendDeep({}, outerData, isolatedData)
Array.isArray(isolatedData)
? isolatedData
: extendDeep({}, outerData, isolatedData)
)

return await onCommitProp?.(
Expand Down Expand Up @@ -202,8 +204,7 @@ function IsolationProvider<Data extends JsonObject>(

const providerProps: IsolationProps<Data> = {
...props,
data: internalDataRef.current,
defaultData: undefined,
[defaultData ? 'defaultData' : 'data']: internalDataRef.current,
onPathChange: onPathChangeHandler,
onCommit,
onClear,
Expand Down
Loading
Loading