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): add Form.Isolation path support when used inside Form.Section #3829

Merged
merged 5 commits into from
Aug 19, 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 @@ -16,10 +16,7 @@ export const UsingCommitButton = () => {
>
<Flex.Stack>
<Field.String required label="Isolated" path="/isolated" />

<Flex.Horizontal>
<Form.Isolation.CommitButton text="Commit" />
</Flex.Horizontal>
<Form.Isolation.CommitButton text="Commit" />
</Flex.Stack>
</Form.Isolation>

Expand Down Expand Up @@ -58,10 +55,14 @@ export const CommitHandleRef = () => {
>
<Card stack>
<Form.SubHeading>Ny hovedkontaktperson</Form.SubHeading>
<Field.Selection
variant="radio"
dataPath="/contactPersons"
/>

<HeightAnimation>
<Field.Selection
variant="radio"
dataPath="/contactPersons"
/>
</HeightAnimation>

<Form.Isolation
commitHandleRef={commitHandleRef}
transformOnCommit={(isolatedData, handlerData) => {
Expand Down Expand Up @@ -176,9 +177,8 @@ export const TransformCommitData = () => {
],
}
}}
id="my-isolated-area"
onCommit={() => {
Form.clearData('my-isolated-area')
onCommit={(data, { clearData }) => {
clearData()
}}
>
<Flex.Stack>
Expand All @@ -201,3 +201,45 @@ export const TransformCommitData = () => {
</ComponentBox>
)
}

export const InsideSection = () => {
return (
<ComponentBox>
<Form.Handler
defaultData={{
mySection: {
isolated: 'Isolated value defined outside',
regular: 'Outer regular value',
},
}}
onChange={(data) => {
console.log('Outer onChange:', data)
}}
>
<Form.Section path="/mySection">
<Flex.Stack>
<Form.Isolation
defaultData={{
isolated: 'The real initial "isolated" value',
}}
onPathChange={(path, value) => {
console.log('Isolated onChange:', path, value)
}}
onCommit={(data) => console.log('onCommit:', data)}
>
<Flex.Stack>
<Field.String label="Isolated" path="/isolated" required />
<Form.Isolation.CommitButton />
</Flex.Stack>
</Form.Isolation>

<Field.String label="Synced" path="/isolated" />
<Field.String label="Regular" path="/regular" required />

<Form.SubmitButton />
</Flex.Stack>
</Form.Section>
</Form.Handler>
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ import * as Examples from './Examples'
### Using commitHandleRef

<Examples.CommitHandleRef />

### Inside a section

<Examples.InsideSection />
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ It's a provider that lets you provide a `schema` or `data` very similar to what
- Input fields are prevented from submitting the form when pressing enter. Pressing enter on input fields will commit the isolated data to the `Form.Handler` context instead.
- You can provide a `schema`, `data` or `defaultData` like you would do with the `Form.Handler`.
- You can also provide `data` or `defaultData` to the `Form.Handler`, defining the data that will be used for the isolated data.
- Using `Form.Isolation` inside of a `Form.Section` is supported.
- `onChange` on the `Form.Handler` will be called when the isolated data gets commited.
- `onChange` on the `Form.Isolation` will be called on every change of the isolated data. Use `onCommit` to get the data that gets commited.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ import {
OnChange,
EventReturnWithStateObject,
ValueProps,
OnCommit,
} from '../../types'
import type { IsolationProviderProps } from '../../Form/Isolation/Isolation'
import { debounce } from '../../../../shared/helpers'
import { extendDeep } from '../../../../shared/component-helper'
import FieldPropsProvider from '../../Form/FieldProps'
import useMountEffect from '../../../../shared/helpers/useMountEffect'
import useUpdateEffect from '../../../../shared/helpers/useUpdateEffect'
Expand All @@ -53,7 +52,8 @@ import structuredClone from '@ungap/structured-clone'
const useLayoutEffect =
typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect

export interface Props<Data extends JsonObject> {
export interface Props<Data extends JsonObject>
extends IsolationProviderProps<Data> {
/**
* Unique ID to communicate with the hook Form.useData
*/
Expand Down Expand Up @@ -108,7 +108,7 @@ export interface Props<Data extends JsonObject> {
*/
onPathChange?: (
path: Path,
value: any
value: unknown
) =>
| EventReturnWithStateObject
| void
Expand All @@ -135,11 +135,6 @@ export interface Props<Data extends JsonObject> {
| EventReturnWithStateObject
| void
| Promise<EventReturnWithStateObject | void>
/**
* Used internally by the Form.Isolation component.
* Will emit on a nested form context commit – if validation has passed.
*/
onCommit?: OnCommit<Data>
/**
* Minimum time to display the submit indicator.
*/
Expand Down Expand Up @@ -168,18 +163,6 @@ export interface Props<Data extends JsonObject> {
* Make all fields required
*/
required?: boolean
/**
* Used internally by the Form.Isolation component
*/
path?: Path
/**
* Used internally by the Form.Isolation component
*/
isolate?: boolean
/**
* Transform the data before it gets committed to the form. The first parameter is the isolated data object. The second parameter is the outer context data object (Form.Handler).
*/
transformOnCommit?: (data: Data, contextData: Data) => Data
/**
* The children of the context provider
*/
Expand All @@ -205,6 +188,7 @@ export default function Provider<Data extends JsonObject>(
onSubmitRequest,
onSubmitComplete,
onCommit,
onClear,
scrollTopOnSubmit,
minimumAsyncBehaviorTime,
asyncSubmitTimeout,
Expand All @@ -230,11 +214,7 @@ export default function Provider<Data extends JsonObject>(
)
}

const {
hasContext,
handlePathChange: handlePathChangeNested,
data: dataNested,
} = useContext(Context) || {}
const { hasContext } = useContext(Context) || {}

if (hasContext && !isolate) {
throw new Error('DataContext (Form.Handler) can not be nested')
Expand Down Expand Up @@ -294,6 +274,7 @@ export default function Provider<Data extends JsonObject>(
return JSON.parse(sessionDataJSON)
}
}

return data ?? defaultData
// eslint-disable-next-line react-hooks/exhaustive-deps -- Avoid triggering code that should only run initially
}, [])
Expand Down Expand Up @@ -591,11 +572,14 @@ export default function Provider<Data extends JsonObject>(
sharedData.data !== internalDataRef.current
) {
cacheRef.current.shared = sharedData.data
if (sharedData.data.clearForm) {
const clear = {} as Data
sharedData.set(clear)

// Reset the shared state, if clearForm is set
if (sharedData.data?.clearForm) {
const clear = (cacheRef.current.shared = clearedData as Data)
setSharedData(clear)
return clear
}

return {
...internalDataRef.current,
...sharedData.data,
Expand All @@ -609,13 +593,19 @@ export default function Provider<Data extends JsonObject>(
}

return internalDataRef.current
}, [data, id, initialData, sharedData])
}, [id, initialData, sharedData, data, setSharedData])

internalDataRef.current =
props.path && pointer.has(internalData, props.path)
? pointer.get(internalData, props.path)
: internalData

useEffect(() => {
if (sharedData.data?.clearForm) {
onClear?.()
}
}, [onClear, sharedData.data?.clearForm])

useLayoutEffect(() => {
// Set the shared state, if initialData was given
if (id && initialData && !sharedData.data) {
Expand Down Expand Up @@ -837,6 +827,16 @@ export default function Provider<Data extends JsonObject>(
}
}, [])

const clearData = useCallback(() => {
internalDataRef.current = clearedData as Data
if (id) {
setSharedData?.(internalDataRef.current)
} else {
forceUpdate()
}
onClear?.()
}, [id, onClear, setSharedData])

/**
* Shared logic dedicated to submit the whole form
*/
Expand Down Expand Up @@ -892,26 +892,19 @@ export default function Provider<Data extends JsonObject>(

try {
if (isolate) {
const path = props.path ?? '/'
const outerData =
props.path && pointer.has(dataNested, path)
? pointer.get(dataNested, path)
: dataNested
let isolatedData = internalDataRef.current

if (typeof props.transformOnCommit === 'function') {
isolatedData = props.transformOnCommit(
isolatedData,
outerData
)
}

// Commit the internal data to the nested context data
handlePathChangeNested?.(
path,
extendDeep({}, outerData, isolatedData)
)
result = await onCommit?.(isolatedData)
const mounterData = {} as Data
mountedFieldPathsRef.current.forEach((path) => {
if (pointer.has(internalDataRef.current, path)) {
pointer.set(
mounterData,
path,
pointer.get(internalDataRef.current, path)
)
}
})
result = await onCommit?.(mounterData, {
clearData,
})
} else {
result = await onSubmit()
}
Expand Down Expand Up @@ -964,15 +957,13 @@ export default function Provider<Data extends JsonObject>(
return internalDataRef.current
},
[
dataNested,
handlePathChangeNested,
clearData,
hasErrors,
hasFieldState,
hasFieldWithAsyncValidator,
isolate,
onCommit,
onSubmitRequest,
props.path,
setFormState,
setShowAllErrors,
setSubmitState,
Expand Down Expand Up @@ -1004,14 +995,7 @@ export default function Provider<Data extends JsonObject>(

forceUpdate() // in order to fill "empty fields" again with their internal states
},
clearData: () => {
internalDataRef.current = {} as Data
if (id) {
setSharedData?.({} as Data)
} else {
forceUpdate()
}
},
clearData,
}

let result = undefined
Expand Down Expand Up @@ -1042,16 +1026,15 @@ export default function Provider<Data extends JsonObject>(
})
},
[
clearData,
filterDataHandler,
handleSubmitCall,
id,
mutateDataHandler,
onSubmit,
onSubmitComplete,
scrollToTop,
scrollTopOnSubmit,
sessionStorageId,
setSharedData,
transformOut,
]
)
Expand Down Expand Up @@ -1332,3 +1315,5 @@ function useFormStatusBuffer(props: FormStatusBufferProps) {

return { bufferedFormState: stateRef.current }
}

export const clearedData = Object.freeze({})
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,9 @@ export const ProviderEvents: PropertiesTableProps = {
type: 'function',
status: 'optional',
},
onClear: {
doc: 'Will be called when the form is cleared via `Form.clearData` or via the `onSubmit` event (or `onCommit`) argument `{ clearData }`.',
type: 'function',
status: 'optional',
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default function FormHandler<Data extends JsonObject>({
onSubmit,
onSubmitRequest,
onSubmitComplete,
onClear,
minimumAsyncBehaviorTime,
asyncSubmitTimeout,
scrollTopOnSubmit,
Expand Down Expand Up @@ -63,6 +64,7 @@ export default function FormHandler<Data extends JsonObject>({
onSubmit,
onSubmitRequest,
onSubmitComplete,
onClear,
minimumAsyncBehaviorTime,
asyncSubmitTimeout,
scrollTopOnSubmit,
Expand Down
Loading
Loading