Skip to content

Commit

Permalink
Fix PushContainer defaultValue support
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Sep 25, 2024
1 parent 2dc99f2 commit 29dd6f6
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Isolation from '../../Form/Isolation'
import PushContainerContext from './PushContainerContext'
import IterateItemContext from '../IterateItemContext'
import DataContext from '../../DataContext/Context'
import { clearedData } from '../../DataContext/Provider'
import useDataValue from '../../hooks/useDataValue'
import EditContainer, { CancelButton, DoneButton } from '../EditContainer'
import IterateArray, { ContainerMode } from '../Array'
Expand Down Expand Up @@ -57,7 +58,7 @@ export type AllProps = Props & SpacingProps & ArrayItemAreaProps

function PushContainer(props: AllProps) {
const {
data = null,
data,
path,
title,
children,
Expand Down Expand Up @@ -90,9 +91,12 @@ function PushContainer(props: AllProps) {
switchContainerMode: switchContainerModeRef.current,
}

const { getValueByPath } = useDataValue()
const defaultData = useMemo(() => {
return { newItems: [data] }
}, [data])
const value =
data ?? (Array.isArray(getValueByPath(path)) ? null : clearedData)
return { newItems: [value] }
}, [data, getValueByPath, path])

return (
<Isolation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,102 @@ describe('PushContainer', () => {
})
})

it('should keep the defaultValue after clearing', async () => {
const onChange = jest.fn()

render(
<Form.Handler onChange={onChange}>
<Iterate.PushContainer path="/entries">
<Field.Name.First
itemPath="/first"
defaultValue="first name"
required
/>
<Field.Name.Last
itemPath="/last"
defaultValue="last name"
required
/>
</Iterate.PushContainer>
</Form.Handler>
)

const [firstInput, lastInput] = Array.from(
document.querySelectorAll('input')
)
const button = document.querySelector('button')

expect(firstInput).toHaveValue('first name')
expect(lastInput).toHaveValue('last name')

await userEvent.click(button)
expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange).toHaveBeenLastCalledWith(
{
entries: [
{
first: 'first name',
last: 'last name',
},
],
},
expect.anything()
)

expect(firstInput).toHaveValue('first name')
expect(lastInput).toHaveValue('last name')

await userEvent.click(button)
expect(onChange).toHaveBeenCalledTimes(2)
expect(onChange).toHaveBeenLastCalledWith(
{
entries: [
{
first: 'first name',
last: 'last name',
},
{
first: 'first name',
last: 'last name',
},
],
},
expect.anything()
)

expect(firstInput).toHaveValue('first name')
expect(lastInput).toHaveValue('last name')

await userEvent.click(button)
expect(onChange).toHaveBeenCalledTimes(3)
expect(onChange).toHaveBeenLastCalledWith(
{
entries: [
{
first: 'first name',
last: 'last name',
},
{
first: 'first name',
last: 'last name',
},
{
first: 'first name',
last: 'last name',
},
],
},
expect.anything()
)

expect(firstInput).toHaveValue('first name')
expect(lastInput).toHaveValue('last name')

await waitFor(() => {
expect(document.querySelector('.dnb-form-status')).toBeNull()
})
})

it('should validate input values', async () => {
render(
<Form.Handler>
Expand Down Expand Up @@ -451,6 +547,63 @@ describe('PushContainer', () => {
)
})

it('should support defaultValue in fields with root path', async () => {
const onChange = jest.fn()

render(
<Form.Handler onChange={onChange}>
<Iterate.PushContainer path="/">
<Field.String itemPath="/" defaultValue="foo" />
</Iterate.PushContainer>
</Form.Handler>
)

await userEvent.click(document.querySelector('button'))

expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange).toHaveBeenLastCalledWith(['foo'], expect.anything())

await userEvent.click(document.querySelector('button'))

expect(onChange).toHaveBeenCalledTimes(2)
expect(onChange).toHaveBeenLastCalledWith(
['foo', 'foo'],
expect.anything()
)
})

it('should support defaultValue in fields with object path', async () => {
const onChange = jest.fn()

render(
<Form.Handler onChange={onChange}>
<Iterate.PushContainer path="/myList">
<Field.String itemPath="/foo" defaultValue="bar" />
</Iterate.PushContainer>
</Form.Handler>
)

await userEvent.click(document.querySelector('button'))

expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange).toHaveBeenLastCalledWith(
{
myList: [{ foo: 'bar' }],
},
expect.anything()
)

await userEvent.click(document.querySelector('button'))

expect(onChange).toHaveBeenCalledTimes(2)
expect(onChange).toHaveBeenLastCalledWith(
{
myList: [{ foo: 'bar' }, { foo: 'bar' }],
},
expect.anything()
)
})

it('should support initial data as a string', async () => {
const onChange = jest.fn()

Expand Down
57 changes: 40 additions & 17 deletions packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1519,22 +1519,6 @@ export default function useFieldProps<Value, EmptyValue, Props>(
validateValue()
}, [schema, validateValue])

const isEmptyData = useCallback(() => {
return (
dataContext.internalDataRef?.current ===
(dataContext.props?.emptyData ?? clearedData)
)
}, [dataContext.internalDataRef, dataContext.props?.emptyData])

// Use "useLayoutEffect" to be in sync with the data context "updateDataValueDataContext" routine further down.
useLayoutEffect(() => {
if (isEmptyData()) {
changedRef.current = false
hideError()
clearErrorState()
}
}, [externalValue, clearErrorState, hideError, isEmptyData]) // ensure to include "externalValue" in order to properly remove errors

// Use "useLayoutEffect" and "externalValueDidChangeRef"
// to cooperate with the the data context "updateDataValueDataContext" routine further down,
// which also uses useLayoutEffect.
Expand Down Expand Up @@ -1582,7 +1566,7 @@ export default function useFieldProps<Value, EmptyValue, Props>(
// Form.Visibility is an example of a logic, where a field value/defaultValue can be used to set the set state of a path,
// where again other fields depend on it.
const tmpValueRef = useRef<Record<Identifier, unknown>>({})
useLayoutEffect(() => {
const setData = useCallback(() => {
if (hasPath || hasItemPath) {
let value = valueProp

Expand Down Expand Up @@ -1688,6 +1672,45 @@ export default function useFieldProps<Value, EmptyValue, Props>(
valueProp,
])

const isEmptyData = useCallback(
() => {
return (
dataContext.internalDataRef?.current ===
(dataContext.props?.emptyData ?? clearedData)
)
},

// eslint-disable-next-line react-hooks/exhaustive-deps
[
dataContext.internalDataRef,
dataContext.props?.emptyData,
externalValue, // ensure to include "externalValue" in order to properly remove errors
]
)

// Use "useLayoutEffect" to be in sync with the data context "updateDataValueDataContext".
useLayoutEffect(() => {
if (isEmptyData()) {
defaultValueRef.current = defaultValue
changedRef.current = false
hideError()
clearErrorState()
}
}, [clearErrorState, defaultValue, hideError, isEmptyData])

useLayoutEffect(() => {
if (!isEmptyData()) {
setData()
}
}, [isEmptyData, setData])

useEffect(() => {
if (isEmptyData()) {
setData()
validateValue()
}
}, [isEmptyData, setData, validateValue])

useEffect(() => {
if (showAllErrors || showBoundaryErrors) {
// In case of async validation, we don't want to show existing errors before the validation has been completed
Expand Down

0 comments on commit 29dd6f6

Please sign in to comment.