Skip to content

Commit

Permalink
feat(Forms): add containerMode, minimumRequiredItems and `hideToo…
Browse files Browse the repository at this point in the history
…lbarWhen` property to Iterate.Array – show first item as initially open

Close items when PushContainer opens

Update packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/demos.mdx

Co-authored-by: Anders <anderslangseth@gmail.com>

Update packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/Examples.tsx

Co-authored-by: Anders <anderslangseth@gmail.com>

Ensure correct error message and avoid cancel click when item has error

Revert useExternalValue path change

Remove prioritizeDataContextInjection in favor of better solution

Revert support for value and defaultValue inside iterate

Ensure auto-closing mechanism works consistently – remove useUnmountEffect because of the lack of possible changing deps

Ensure correct mount/unmount by using correct deps handling

Rename `initial-open` to `initiallyOpen`

Move value warning to its own PR #3886
  • Loading branch information
tujoworker committed Sep 9, 2024
1 parent 7c2758e commit ffcf292
Show file tree
Hide file tree
Showing 39 changed files with 1,578 additions and 347 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ComponentBox from '../../../../../../shared/tags/ComponentBox'
import { Card, Flex, Table, Td, Th, Tr } from '@dnb/eufemia/src'
import { Card, Flex, Section, Table, Td, Th, Tr } from '@dnb/eufemia/src'
import {
Iterate,
Field,
Expand Down Expand Up @@ -172,7 +172,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 +212,7 @@ export const ArrayFromFormHandler = () => {
pushValue={{}}
/>
</Card>
</Flex.Vertical>
</Flex.Stack>
</Form.Handler>
</ComponentBox>
)
Expand Down Expand Up @@ -291,7 +291,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 +304,7 @@ export const ViewAndEditContainer = () => {
</Card>

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

export const InitialOpen = () => {
return (
<ComponentBox scope={{ Iterate }}>
{() => {
const MyEditItemForm = () => {
return (
<Field.SelectCountry
label="Land du er statsborger i"
itemPath="/"
required
/>
)
}

const MyEditItem = (props) => {
return (
<Iterate.EditContainer {...props}>
<MyEditItemForm />
</Iterate.EditContainer>
)
}

const MyViewItem = () => {
return (
<Iterate.ViewContainer>
<Value.SelectCountry
label="Land du er statsborger i"
itemPath="/"
/>
</Iterate.ViewContainer>
)
}

const MyForm = () => {
return (
<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]}
minimumRequiredItems={1}
hideToolbarWhen={(index, items) => items.length === 1}
>
<MyViewItem />
<MyEditItem />
</Iterate.Array>

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

<Form.SubmitButton variant="send" />

<Output />
</Flex.Stack>
</Form.Handler>
)
}

const Output = () => {
const { data } = Form.useData()

return (
<Section
element="output"
backgroundColor="sand-yellow"
style={{ maxWidth: '80vw' }}
innerSpace
>
<pre>All data: {JSON.stringify(data)}</pre>
</Section>
)
}

return <MyForm />
}}
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ With an optional `title` and [Iterate.Toolbar](/uilib/extensions/forms/Iterate/T
### Value composition

<Examples.ValueComposition />

### Initially open

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

### Initial container mode

By defualt, 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.

You can also set `minimumRequiredItems` to 1 to ensure that the first has not remove button as long as there is at least one item.

You can also set `hideToolbarWhen` to `(index, items) => items.length === 1` to hide the toolbar when there is only one item.

## 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 @@ -11,8 +11,6 @@ tabs:
key: '/demos'
- title: Properties
key: '/properties'
- title: Events
key: '/events'
breadcrumb:
- text: Forms
href: /uilib/extensions/forms/
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 @@ -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 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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -33,38 +33,66 @@ export type Props = {
function ElementBlock(props: Props & FlexContainerProps) {
const [, forceUpdate] = useReducer(() => ({}), {})

const contextRef = useRef<
const {
mode,
open,
ariaLabel,
onAnimationEnd,
className,
children,
openDelay = 100,
variant = 'outline',
...restProps
} = props

const localContextRef = useRef<
IterateItemContextState & {
hasError?: boolean
hasSubmitError?: boolean
}
>()
contextRef.current = useContext(IterateItemContext) || {}

const { hasError, hasSubmitError } =
useContext(FieldBoundaryContext) || {}
contextRef.current.hasError = hasError
contextRef.current.hasSubmitError = hasSubmitError

// - Set the container mode to "edit" if we have an error
if (hasSubmitError) {
contextRef.current.containerMode = 'edit'
localContextRef.current = useContext(IterateItemContext) || {}
localContextRef.current.hasError = hasError
localContextRef.current.hasSubmitError = hasSubmitError
const { isNew, value } = localContextRef.current
if (hasSubmitError || !value) {
localContextRef.current.containerMode = 'edit'
}

const { handleRemove, switchContainerMode, containerMode, isNew } =
contextRef.current
const determineMode = useCallback(() => {
if (
mode === 'edit' &&
!localContextRef.current.hasSubmitError &&
localContextRef.current.initialContainerMode === 'auto'
) {
if (hasError) {
// - Set the container mode to "edit" if we have an error
if (isNew) {
localContextRef.current.containerMode = 'edit'
} else {
localContextRef.current.switchContainerMode('edit')
}
}
}
}, [hasError, isNew, mode])

useEffect(() => {
determineMode()
}, [determineMode])

if (localContextRef.current.containerMode === 'auto') {
localContextRef.current.containerMode = 'view'
}

const {
mode,
open,
ariaLabel,
onAnimationEnd,
className,
children,
openDelay = 100,
variant = 'outline',
...restProps
} = props
handleRemove,
switchContainerMode,
containerMode,
initialContainerMode,
} = localContextRef.current

const openRef = useRef(open ?? (containerMode === mode && !isNew))
const isRemoving = useRef(false)
Expand Down Expand Up @@ -98,14 +126,16 @@ function ElementBlock(props: Props & FlexContainerProps) {
const handleAnimationEnd = useCallback(
(state) => {
// - Keep the block open if we have an error
if (contextRef.current.hasSubmitError) {
if (
localContextRef.current.hasSubmitError &&
initialContainerMode !== 'auto'
) {
switchContainerMode?.('edit')
}

const preventFocusOnErrorOpening = !contextRef.current.hasSubmitError
if (preventFocusOnErrorOpening) {
if (!localContextRef.current.hasSubmitError) {
if (state === 'opened') {
contextRef.current?.elementRef?.current?.focus?.()
localContextRef.current?.elementRef?.current?.focus?.()
} else {
// Wait until the element is removed, then check if we can set focus
window.requestAnimationFrame(() => {
Expand All @@ -118,26 +148,26 @@ function ElementBlock(props: Props & FlexContainerProps) {
)
) {
const elements =
contextRef.current?.containerRef.current.querySelectorAll<HTMLDivElement>(
localContextRef.current?.containerRef.current.querySelectorAll<HTMLDivElement>(
'.dnb-forms-iterate__element'
)
elements[elements.length - 1].focus()
}
} catch (e) {
/**/
/* do nothing */
}
})
}
}

if (!openRef.current && isRemoving.current) {
isRemoving.current = false
contextRef.current?.fulfillRemove?.()
localContextRef.current?.fulfillRemove?.()
}

onAnimationEnd?.(state)
},
[onAnimationEnd, switchContainerMode]
[initialContainerMode, onAnimationEnd, switchContainerMode]
)
const handleRemoveBlock = useCallback(() => {
isRemoving.current = true
Expand All @@ -152,7 +182,7 @@ function ElementBlock(props: Props & FlexContainerProps) {
'dnb-forms-section-block',
variant && `dnb-forms-section-block--variant-${variant}`,
isNew && 'dnb-forms-section-block--new',
contextRef.current.hasSubmitError &&
localContextRef.current.hasSubmitError &&
'dnb-forms-section-block--error',
className
)}
Expand Down
Loading

0 comments on commit ffcf292

Please sign in to comment.