Skip to content

Commit

Permalink
feat(Forms): add validator support to Iterate.Array (#3926)
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker authored Sep 12, 2024
1 parent 5ec2772 commit 6fd439e
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -439,3 +439,43 @@ export const ToolbarVariantMiniumOneItemTwoItems = () => {
</ComponentBox>
)
}

export const WithArrayValidator = () => {
return (
<ComponentBox>
<Form.Handler
defaultData={{ items: ['foo'] }}
onSubmit={async () => console.log('onSubmit')}
>
<Card stack>
<Iterate.Array
path="/items"
validator={(arrayValue) => {
if (!(arrayValue && arrayValue.length > 1)) {
return new Error('You need at least two items')
}
}}
>
<Flex.Horizontal align="flex-end">
<Field.String
label="Item no. {itemNr}"
itemPath="/"
width="medium"
size="medium"
/>
<Iterate.RemoveButton />
</Flex.Horizontal>
</Iterate.Array>

<Iterate.PushButton
top
path="/items"
pushValue={null}
text="Add"
/>
<Form.SubmitButton />
</Card>
</Form.Handler>
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,17 @@ It hides the toolbar in the `EditContainer` when there is only one item in the a
### Value composition

<Examples.ValueComposition />

### Array validator

You can also add a validator to ensure that the array contains at least one item:

```tsx
const validator = (arrayValue) => {
if (!(arrayValue?.length > 0)) {
return new Error('You need at least one item')
}
}
```

<Examples.WithArrayValidator />
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ function LabelDescription({ labelDescription, children }) {
return <div className="dnb-forms-field-block__label">{children}</div>
}

function getMessage(item: Partial<StateWithMessage>): StateMessage {
export function getMessage(item: Partial<StateWithMessage>): StateMessage {
const { content } = item

return ((content instanceof Error && content.message) ||
Expand Down
114 changes: 69 additions & 45 deletions packages/dnb-eufemia/src/extensions/forms/Iterate/Array/Array.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import classnames from 'classnames'
import pointer from 'json-pointer'
import { useFieldProps } from '../../hooks'
import { makeUniqueId } from '../../../../shared/component-helper'
import { Flex } from '../../../../components'
import { Flex, FormStatus } from '../../../../components'
import { pickSpacingProps } from '../../../../components/flex/utils'
import useMountEffect from '../../../../shared/helpers/useMountEffect'
import {
Expand All @@ -28,6 +28,7 @@ import FieldBoundaryProvider from '../../DataContext/FieldBoundary/FieldBoundary
import DataContext from '../../DataContext/Context'
import useDataValue from '../../hooks/useDataValue'
import { useSwitchContainerMode } from '../hooks'
import { getMessage } from '../../FieldBlock'

import type { ContainerMode, ElementChild, Props, Value } from './types'
import type { Identifier } from '../../types'
Expand Down Expand Up @@ -84,16 +85,23 @@ function ArrayComponent(props: Props) {
const {
path,
value: arrayValue,
error,
defaultValue,
withoutFlex,
emptyValue,
placeholder,
containerMode,
handleChange,
setChanged,
onChange,
children,
} = useFieldProps(preparedProps)

useMountEffect(() => {
// To ensure the validator is called when a new item is added
setChanged(true)
})

const idsRef = useRef<Array<Identifier>>([])
const isNewRef = useRef<Record<string, boolean>>({})
const modesRef = useRef<
Expand Down Expand Up @@ -251,54 +259,70 @@ function ArrayComponent(props: Props) {
const WrapperElement = omitFlex ? Fragment : Flex.Stack

return (
<WrapperElement {...(omitFlex ? null : flexProps)}>
{arrayValue === emptyValue || props?.value?.length === 0
? placeholder
: arrayItems.map((itemProps) => {
const { id, value, index } = itemProps
const elementRef = (innerRefs.current[id] =
innerRefs.current[id] || createRef<HTMLDivElement>())

const renderChildren = (elementChild: ElementChild) => {
return typeof elementChild === 'function'
? elementChild(value, index)
: elementChild
}

const contextValue = {
...itemProps,
elementRef,
}

const content = Array.isArray(children)
? children.map((child) => renderChildren(child))
: renderChildren(children)

if (omitFlex) {
<>
<WrapperElement {...(omitFlex ? null : flexProps)}>
{arrayValue === emptyValue || props?.value?.length === 0
? placeholder
: arrayItems.map((itemProps) => {
const { id, value, index } = itemProps
const elementRef = (innerRefs.current[id] =
innerRefs.current[id] || createRef<HTMLDivElement>())

const renderChildren = (elementChild: ElementChild) => {
return typeof elementChild === 'function'
? elementChild(value, index)
: elementChild
}

const contextValue = {
...itemProps,
elementRef,
}

const content = Array.isArray(children)
? children.map((child) => renderChildren(child))
: renderChildren(children)

if (omitFlex) {
return (
<IterateItemContext.Provider
key={`element-${id}`}
value={contextValue}
>
<FieldBoundaryProvider>
{content}
</FieldBoundaryProvider>
</IterateItemContext.Provider>
)
}

return (
<IterateItemContext.Provider
<Flex.Item
className="dnb-forms-iterate__element"
tabIndex={-1}
innerRef={elementRef}
key={`element-${id}`}
value={contextValue}
>
<FieldBoundaryProvider>{content}</FieldBoundaryProvider>
</IterateItemContext.Provider>
<IterateItemContext.Provider value={contextValue}>
<FieldBoundaryProvider>
{content}
</FieldBoundaryProvider>
</IterateItemContext.Provider>
</Flex.Item>
)
}

return (
<Flex.Item
className="dnb-forms-iterate__element"
tabIndex={-1}
innerRef={elementRef}
key={`element-${id}`}
>
<IterateItemContext.Provider value={contextValue}>
<FieldBoundaryProvider>{content}</FieldBoundaryProvider>
</IterateItemContext.Provider>
</Flex.Item>
)
})}
</WrapperElement>
})}
</WrapperElement>

<FormStatus
top={0}
bottom={0}
show={Boolean(error)}
no_animation={false}
shellSpace={{ top: true, bottom: true }}
>
{getMessage({ content: error })}
</FormStatus>
</>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PropertiesTableProps } from '../../../../shared/types'
import { dataValueProperties } from '../../hooks/DataValueDocs'

export const ArrayProperties: PropertiesTableProps = {
value: {
Expand Down Expand Up @@ -41,6 +42,9 @@ export const ArrayProperties: PropertiesTableProps = {
type: 'unknown',
status: 'optional',
},
validator: dataValueProperties.validator,
validateInitially: dataValueProperties.validateInitially,
continuousValidation: dataValueProperties.continuousValidation,
containerMode: {
doc: 'Defines the container mode for all nested containers. Can be `view`, `edit` or `auto`. When using `auto`, it will automatically open if there is an error in the container. When a new item is added, the item before it will change to `view` mode, if it had no validation errors. Defaults to `auto`.',
type: 'string',
Expand Down
Loading

0 comments on commit 6fd439e

Please sign in to comment.