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

fix(Forms): keep field state during a Wizard step change when used inside Iterate.Array (which used defualtValue as the data source) #4025

Merged
merged 1 commit into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -99,7 +99,11 @@ export interface ContextState {
| unknown
| Promise<EventReturnWithStateObject | unknown>
handlePathChangeUnvalidated: (path: Path, value: any) => void
updateDataValue: (path: Path, value: any) => void
updateDataValue: (
path: Path,
value: any,
options?: { preventUpdate?: boolean }
) => void
setData: (data: any) => void
clearData?: () => void
mutateDataHandler?: MutateDataHandler<unknown>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,7 @@ export default function Provider<Data extends JsonObject>(
}, [sessionStorageId])

const setData = useCallback(
(newData: Data) => {
(newData: Data, preventUpdate = false) => {
// - Mutate the data context
if (transformIn) {
newData = mutateDataHandler(newData, transformIn)
Expand All @@ -760,7 +760,9 @@ export default function Provider<Data extends JsonObject>(
storeInSession()
}

forceUpdate() // Will rerender the whole form initially
if (!preventUpdate) {
forceUpdate() // Will rerender the whole form initially
}
},
[
extendSharedData,
Expand All @@ -778,7 +780,7 @@ export default function Provider<Data extends JsonObject>(
* Update the data set
*/
const updateDataValue: ContextState['updateDataValue'] = useCallback(
(path, value) => {
(path, value, { preventUpdate } = {}) => {
if (!path) {
return
}
Expand All @@ -804,7 +806,7 @@ export default function Provider<Data extends JsonObject>(
pointer.set(newData, path, value)
}

setData(newData)
setData(newData, preventUpdate)
},
[setData]
)
Expand Down
17 changes: 5 additions & 12 deletions packages/dnb-eufemia/src/extensions/forms/Iterate/Array/Array.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import IterateItemContext, {
import SummaryListContext from '../../Value/SummaryList/SummaryListContext'
import ValueBlockContext from '../../ValueBlock/ValueBlockContext'
import FieldBoundaryProvider from '../../DataContext/FieldBoundary/FieldBoundaryProvider'
import DataContext from '../../DataContext/Context'
import useDataValue from '../../hooks/useDataValue'
import { useArrayLimit, useSwitchContainerMode } from '../hooks'
import { getMessage } from '../../FieldBlock'
Expand Down Expand Up @@ -90,7 +89,6 @@ function ArrayComponent(props: Props) {
value: arrayValue,
limit,
error,
defaultValue,
withoutFlex,
emptyValue,
placeholder,
Expand All @@ -100,7 +98,11 @@ function ArrayComponent(props: Props) {
setChanged,
onChange,
children,
} = useFieldProps(preparedProps)
} = useFieldProps(preparedProps, {
// To ensure the defaultValue set on the Iterate.Array is set in the data context,
// and will not overwrite defaultValues set by fields inside the Iterate.Array.
updateContextDataInSync: true,
})

useMountEffect(() => {
// To ensure the validator is called when a new item is added
Expand Down Expand Up @@ -129,15 +131,6 @@ function ArrayComponent(props: Props) {

const omitFlex = withoutFlex ?? (summaryListContext || valueBlockContext)

// To support React.StrictMode, we inject the defaultValue into the data context this way.
// The routine inside useFieldProps where updateDataValueDataContext is called, does not support React.StrictMode
const { handlePathChange } = useContext(DataContext) || {}
useMountEffect(() => {
if (defaultValue) {
handlePathChange?.(path, defaultValue)
}
})

useEffect(() => {
// Update inside the useEffect, to support React.StrictMode
valueCountRef.current = arrayValue || []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -562,64 +562,106 @@ describe('Iterate.Array', () => {
expect(onChangeIterate).toHaveBeenLastCalledWith(['foo'])
})

it('should handle "defaultValue" with React.StrictMode', () => {
const onSubmit = jest.fn()
describe('defaultValue on Iterate.Array', () => {
it('should validate required fields', async () => {
const onSubmit = jest.fn()

render(
<React.StrictMode>
render(
<Form.Handler onSubmit={onSubmit}>
<Iterate.Array path="/myList" defaultValue={['']}>
<Field.String itemPath="/" />
<Iterate.Array path="/myList" defaultValue={[null]}>
<Field.String itemPath="/" required />
</Iterate.Array>
</Form.Handler>
</React.StrictMode>
)
)

const form = document.querySelector('form')
const input = document.querySelector('input')
const form = document.querySelector('form')
fireEvent.submit(form)

expect(input).toHaveValue('')
expect(onSubmit).toHaveLength(0)

fireEvent.submit(form)
await waitFor(() => {
expect(
document.querySelectorAll('.dnb-form-status')
).toHaveLength(1)
})
})

expect(onSubmit).toHaveBeenCalledTimes(1)
expect(onSubmit).toHaveBeenLastCalledWith(
{ myList: [''] },
expect.anything()
)
})
it('should handle "defaultValue" (empty string) in React.StrictMode', () => {
const onSubmit = jest.fn()

it('should warn when "defaultValue" is used inside iterate', () => {
const log = jest.spyOn(console, 'log').mockImplementation()
const onSubmit = jest.fn()
render(
<React.StrictMode>
<Form.Handler onSubmit={onSubmit}>
<Iterate.Array path="/myList" defaultValue={['']}>
<Field.String itemPath="/" />
</Iterate.Array>
</Form.Handler>
</React.StrictMode>
)

render(
<Form.Handler onSubmit={onSubmit}>
<Iterate.Array path="/myList" defaultValue={['']}>
<Field.String itemPath="/" defaultValue="default value" />
</Iterate.Array>
</Form.Handler>
)
const form = document.querySelector('form')
const input = document.querySelector('input')

const form = document.querySelector('form')
const input = document.querySelector('input')
expect(input).toHaveValue('')

expect(input).toHaveValue('')
fireEvent.submit(form)

fireEvent.submit(form)
expect(onSubmit).toHaveBeenCalledTimes(1)
expect(onSubmit).toHaveBeenLastCalledWith(
{ myList: [''] },
expect.anything()
)
})

expect(onSubmit).toHaveBeenCalledTimes(1)
expect(onSubmit).toHaveBeenLastCalledWith(
{ myList: [''] },
expect.anything()
)
it('should handle "defaultValue" (with value) in React.StrictMode', () => {
const onSubmit = jest.fn()

expect(log).toHaveBeenCalledWith(
expect.any(String),
'Using defaultValue="default value" prop inside Iterate is not supported yet'
)
render(
<React.StrictMode>
<Form.Handler onSubmit={onSubmit}>
<Iterate.Array path="/myList" defaultValue={['foo']}>
<Field.String itemPath="/" />
</Iterate.Array>
</Form.Handler>
</React.StrictMode>
)

log.mockRestore()
const form = document.querySelector('form')
const input = document.querySelector('input')

expect(input).toHaveValue('foo')

fireEvent.submit(form)

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

it('should set empty array in the data context', () => {
const onSubmit = jest.fn()

render(
<React.StrictMode>
<Form.Handler onSubmit={onSubmit}>
<Iterate.Array path="/myList" defaultValue={[]}>
content
</Iterate.Array>
</Form.Handler>
</React.StrictMode>
)

const form = document.querySelector('form')
fireEvent.submit(form)

expect(onSubmit).toHaveBeenCalledTimes(1)
expect(onSubmit).toHaveBeenLastCalledWith(
{ myList: [] },
expect.anything()
)
})
})

describe('with primitive elements', () => {
Expand Down Expand Up @@ -1278,46 +1320,6 @@ describe('Iterate.Array', () => {
})
})

describe('value and defaultValue', () => {
it('should warn when "value" prop is used', () => {
const log = jest.spyOn(console, 'log').mockImplementation()

render(
<Form.Handler data={['foo']}>
<Iterate.Array path="/">
<Field.String itemPath="/" value="bar" />
</Iterate.Array>
</Form.Handler>
)

expect(log).toHaveBeenCalledWith(
expect.any(String),
'Using value="bar" prop inside Iterate is not supported yet'
)

log.mockRestore()
})

it('should warn when "defaultValue" prop is used', () => {
const log = jest.spyOn(console, 'log').mockImplementation()

render(
<Form.Handler data={['foo']}>
<Iterate.Array path="/">
<Field.String itemPath="/" defaultValue="bar" />
</Iterate.Array>
</Form.Handler>
)

expect(log).toHaveBeenCalledWith(
expect.any(String),
'Using defaultValue="bar" prop inside Iterate is not supported yet'
)

log.mockRestore()
})
})

it('should contain tabindex of -1', () => {
render(<Iterate.Array value={['one']}>content</Iterate.Array>)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,47 +495,6 @@ describe('PushContainer', () => {
)
})

it('should push "null" to the array when "defaultValue" is given because it is not supported', async () => {
const log = jest.spyOn(console, 'log').mockImplementation()
const onChange = jest.fn()

render(
<Form.Handler data={['foo']} onChange={onChange}>
<Iterate.Array path="/">
<Iterate.ViewContainer>View Content</Iterate.ViewContainer>
<Iterate.EditContainer>Edit Content</Iterate.EditContainer>
</Iterate.Array>

<Iterate.PushContainer path="/">
<Field.String itemPath="/" defaultValue="bar" />
</Iterate.PushContainer>
</Form.Handler>
)

const blocks = Array.from(
document.querySelectorAll('.dnb-forms-section-block')
)
const [, , thirdBlock] = blocks

const input = thirdBlock.querySelector('input')
expect(input).toHaveValue('bar')

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

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

expect(log).toHaveBeenCalledWith(
expect.any(String),
'Using defaultValue="bar" prop inside Iterate is not supported yet'
)

log.mockRestore()
})

it('should support {nextItemNo}', async () => {
render(
<Form.Handler data={{ myList: undefined }}>
Expand Down
Loading
Loading