Skip to content

Commit

Permalink
fix(Forms): ensure update('/path', undefined) does not show error m…
Browse files Browse the repository at this point in the history
…essage
  • Loading branch information
tujoworker committed Feb 4, 2025
1 parent d24c0d3 commit 840d283
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { createContext } from 'react'
import { renderHook, act, render, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { makeUniqueId } from '../../../../../shared/component-helper'
import { Field, Form, Wizard } from '../../..'
import { Button } from '../../../../../components'
import { DataContext, Field, Form, Wizard } from '../../..'
import { FilterData } from '../../../DataContext/Context'
import Provider from '../../../DataContext/Provider'
import useData from '../useData'
import { FilterData } from '../../../DataContext/Context'
import userEvent from '@testing-library/user-event'

describe('Form.useData', () => {
let identifier: string
Expand Down Expand Up @@ -402,6 +403,208 @@ describe('Form.useData', () => {
bar: 'bar',
})
})

it('should set emptyValue as the value of the field', async () => {
let dataContext = null

const MockComponent = () => {
const { update } = useData()

return (
<>
<Field.String
path="/foo"
emptyValue="empty"
defaultValue="foo"
/>
<Button
onClick={() => {
update('/foo', 'empty')
}}
/>

<DataContext.Consumer>
{(context) => {
dataContext = context
return null
}}
</DataContext.Consumer>
</>
)
}

render(
<Form.Handler>
<MockComponent />
</Form.Handler>
)

expect(dataContext.data).toEqual({ foo: 'foo' })
expect(document.querySelector('input')).toHaveValue('foo')

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

expect(dataContext.data).toEqual({ foo: 'empty' })
expect(document.querySelector('input')).toHaveValue('empty')
})

it('should set emptyValue when no value is given', () => {
let dataContext = null

render(
<Form.Handler>
<Field.String path="/foo" required emptyValue="empty" />

<DataContext.Consumer>
{(context) => {
dataContext = context
return null
}}
</DataContext.Consumer>
</Form.Handler>
)

expect(dataContext.data).toEqual({ foo: 'empty' })
expect(document.querySelector('input')).toHaveValue('empty')
})

it('should prioritize defaultValue over emptyValue', () => {
let dataContext = null

render(
<Form.Handler>
<Field.String
path="/foo"
required
emptyValue="empty"
defaultValue="foo"
/>

<DataContext.Consumer>
{(context) => {
dataContext = context
return null
}}
</DataContext.Consumer>
</Form.Handler>
)

expect(dataContext.data).toEqual({ foo: 'foo' })
expect(document.querySelector('input')).toHaveValue('foo')
})

it('should set emptyValue as the value of the field without showing error', async () => {
let dataContext = null

const MockComponent = () => {
const { update } = Form.useData()

return (
<>
<Field.String
path="/foo"
required
emptyValue="empty"
defaultValue="foo"
/>
<Button
onClick={() => {
update('/foo', 'empty')
}}
/>

<DataContext.Consumer>
{(context) => {
dataContext = context
return null
}}
</DataContext.Consumer>
</>
)
}

render(
<Form.Handler>
<MockComponent />
</Form.Handler>
)

expect(dataContext.data).toEqual({ foo: 'foo' })
expect(document.querySelector('input')).toHaveValue('foo')
expect(
document.querySelector('.dnb-form-status')
).not.toBeInTheDocument()

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

expect(dataContext.data).toEqual({ foo: 'empty' })
expect(document.querySelector('input')).toHaveValue('empty')
expect(
document.querySelector('.dnb-form-status')
).not.toBeInTheDocument()

fireEvent.submit(document.querySelector('form'))
expect(
document.querySelector('.dnb-form-status')
).toBeInTheDocument()
})

it('should validate the field after useData update', async () => {
let dataContext = null

const MockComponent = () => {
const { update } = Form.useData()

return (
<>
<Field.String path="/foo" required defaultValue="foo" />
<Button
onClick={() => {
update('/foo', undefined)
}}
/>

<DataContext.Consumer>
{(context) => {
dataContext = context
return null
}}
</DataContext.Consumer>
</>
)
}

render(
<Form.Handler>
<MockComponent />
</Form.Handler>
)

expect(dataContext.data).toEqual({ foo: 'foo' })
expect(document.querySelector('input')).toHaveValue('foo')
expect(
document.querySelector('.dnb-form-status')
).not.toBeInTheDocument()

await userEvent.click(document.querySelector('button'))
await userEvent.type(document.querySelector('input'), 'bar')

expect(dataContext.data).toEqual({ foo: 'bar' })
expect(document.querySelector('input')).toHaveValue('bar')

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

expect(dataContext.data).toEqual({ foo: undefined })
expect(document.querySelector('input')).toHaveValue('')
expect(
document.querySelector('.dnb-form-status')
).not.toBeInTheDocument()

fireEvent.submit(document.querySelector('form'))
expect(
document.querySelector('.dnb-form-status')
).toBeInTheDocument()
})
})

it('should rerender when shared state calls "set"', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function useExternalValue<Value>(props: Props<Value>) {
const {
path,
itemPath,
value,
value = undefined,
transformers,
emptyValue = undefined,
} = props
Expand All @@ -28,7 +28,7 @@ export default function useExternalValue<Value>(props: Props<Value>) {
const { value: iterateElementValue } = iterateItemContext || {}

return useMemo(() => {
if (value !== emptyValue) {
if (value !== undefined && value !== emptyValue) {
// Value-prop sent directly to the field has highest priority, overriding any surrounding source
return transformers?.current?.fromExternal?.(value) ?? emptyValue
}
Expand Down
26 changes: 19 additions & 7 deletions packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export default function useFieldProps<Value, EmptyValue, Props>(
itemPath,
value: valueProp,
transformers,
emptyValue,
emptyValue: defaultValue ? undefined : emptyValue,
})
const externalValueDeps = tmpValue
const externalValue = transformers.current.transformIn(
Expand Down Expand Up @@ -1914,6 +1914,14 @@ export default function useFieldProps<Value, EmptyValue, Props>(
// Error or removed error for this field from the surrounding data context (by path)
if (externalValueDidChangeRef.current) {
externalValueDidChangeRef.current = false

// Hide error when the external value has changed, but is the same as the empty value.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if (emptyValue === valueRef.current) {
hideError()
}

validateValue()
forceUpdate()
}
Expand Down Expand Up @@ -1952,7 +1960,7 @@ export default function useFieldProps<Value, EmptyValue, Props>(
return // stop here
}

let valueToStore: Value | unknown = valueProp ?? emptyValue
let valueToStore: Value | unknown = valueProp

const data = wizardContext?.prerenderFieldProps
? dataContext.data
Expand Down Expand Up @@ -1992,11 +2000,15 @@ export default function useFieldProps<Value, EmptyValue, Props>(
typeof defaultValueRef.current !== 'undefined' &&
typeof valueToStore === 'undefined'

if (hasDefaultValue) {
// Set the default value if it's not set yet.
// This takes precedence over the valueToStore.
valueToStore = defaultValueRef.current
defaultValueRef.current = undefined
if (!hasValue) {
if (hasDefaultValue) {
// Set the default value if it's not set yet.
// This takes precedence over the valueToStore.
valueToStore = defaultValueRef.current
defaultValueRef.current = undefined
} else if (typeof valueToStore === 'undefined') {
valueToStore = emptyValue
}
}

let skipEqualCheck = false
Expand Down

0 comments on commit 840d283

Please sign in to comment.