Skip to content
Open
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
57 changes: 35 additions & 22 deletions packages/react/src/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import {
} from '@inertiajs/core'
import { isEqual } from 'lodash-es'
import React, {
createContext,
createElement,
FormEvent,
forwardRef,
ReactNode,
useContext,
useEffect,
useImperativeHandle,
useMemo,
Expand All @@ -39,6 +41,8 @@ type FormSubmitOptions = Omit<VisitOptions, 'data' | 'onPrefetched' | 'onPrefetc

const noop = () => undefined

export const FormContext = createContext<FormComponentRef | undefined>(undefined)

const Form = forwardRef<FormComponentRef, ComponentProps>(
(
{
Expand Down Expand Up @@ -172,27 +176,30 @@ const Form = forwardRef<FormComponentRef, ComponentProps>(
setIsDirty(false)
}

const exposed = () => ({
errors: form.errors,
hasErrors: form.hasErrors,
processing: form.processing,
progress: form.progress,
wasSuccessful: form.wasSuccessful,
recentlySuccessful: form.recentlySuccessful,
isDirty,
clearErrors: form.clearErrors,
resetAndClearErrors,
setError: form.setError,
reset,
submit,
defaults,
getData,
getFormData,
})

useImperativeHandle(ref, exposed, [form, isDirty, submit])

return createElement(
const exposedValue = useMemo(
() => ({
errors: form.errors,
hasErrors: form.hasErrors,
processing: form.processing,
progress: form.progress,
wasSuccessful: form.wasSuccessful,
recentlySuccessful: form.recentlySuccessful,
isDirty,
clearErrors: form.clearErrors,
resetAndClearErrors,
setError: form.setError,
reset,
submit,
defaults,
getData,
getFormData,
}),
[form, isDirty, submit],
)

useImperativeHandle(ref, () => exposedValue, [exposedValue])

const formElement_el = createElement(
'form',
{
...props,
Expand All @@ -209,11 +216,17 @@ const Form = forwardRef<FormComponentRef, ComponentProps>(
// See: https://github.com/inertiajs/inertia/pull/2536
inert: disableWhileProcessing && form.processing && 'true',
},
typeof children === 'function' ? children(exposed()) : children,
typeof children === 'function' ? children(exposedValue) : children,
)

return createElement(FormContext.Provider, { value: exposedValue }, formElement_el)
},
)

Form.displayName = 'InertiaForm'

export function useFormContext(): FormComponentRef | undefined {
return useContext(FormContext)
}

export default Form
2 changes: 1 addition & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const router = Router
export { default as App } from './App'
export { default as createInertiaApp } from './createInertiaApp'
export { default as Deferred } from './Deferred'
export { default as Form } from './Form'
export { default as Form, FormContext, useFormContext } from './Form'
export { default as Head } from './Head'
export { default as InfiniteScroll } from './InfiniteScroll'
export { InertiaLinkProps, default as Link } from './Link'
Expand Down
66 changes: 66 additions & 0 deletions packages/react/test-app/Pages/FormComponent/Context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Form } from '@inertiajs/react'
import ChildComponent from './Context/ChildComponent'
import NestedComponent from './Context/NestedComponent'
import OutsideFormComponent from './Context/OutsideFormComponent'

export default function Context() {
return (
<div>
<h1>Form Context Test</h1>

<Form action="/dump/post" method="post">
{({ isDirty, hasErrors, errors, processing }) => (
<>
{/* Parent form state display */}
<div id="parent-state">
<div>
Parent: Form is <span>{isDirty ? 'dirty' : 'clean'}</span>
</div>
{hasErrors && <div>Parent: Form has errors</div>}
{processing && <div>Parent: Form is processing</div>}
{errors.name && <div id="parent_error_name">Parent Error: {errors.name}</div>}
</div>

<div>
<label htmlFor="name">Name</label>
<input type="text" name="name" id="name" placeholder="Name" defaultValue="John Doe" />
</div>

<div>
<label htmlFor="email">Email</label>
<input type="email" name="email" id="email" placeholder="Email" defaultValue="[email protected]" />
</div>

<div>
<button type="submit" id="submit-button">
Submit via Form
</button>
</div>

{/* Child component that uses useFormContext */}
<ChildComponent />

{/* Nested child component to test deep context propagation */}
<NestedComponent />
</>
)}
</Form>

{/* Component outside the Form to test undefined context */}
<OutsideFormComponent />

<div>
<h2>Instructions</h2>
<p>
This test demonstrates that child components can access the form context using useFormContext(). Both the
parent form and child components should display the same form state.
</p>
<ul>
<li>Child components inside the Form should have access to form state and methods</li>
<li>Context should propagate through multiple levels of nesting</li>
<li>Components outside the Form should receive undefined from useFormContext()</li>
</ul>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useFormContext } from '@inertiajs/react'

export default function ChildComponent() {
const form = useFormContext()

const submitFromChild = () => {
form?.submit()
}

const resetFromChild = () => {
form?.reset()
}

const clearErrorsFromChild = () => {
form?.clearErrors()
}

const setTestError = () => {
form?.setError('name', 'Error set from child component')
}

const setDefaultsFromChild = () => {
form?.defaults()
}

return (
<div id="child-component" style={{ border: '2px solid blue', padding: '10px', margin: '10px 0' }}>
<h3>Child Component (using useFormContext)</h3>

{form ? (
<div id="child-state">
<div>
Child: Form is <span>{form.isDirty ? 'dirty' : 'clean'}</span>
</div>
{form.hasErrors && <div>Child: Form has errors</div>}
{form.processing && <div>Child: Form is processing</div>}
{form.errors.name && <div id="child_error_name">Child Error: {form.errors.name}</div>}
{form.wasSuccessful && <div id="child_was_successful">Child: Form was successful</div>}
{form.recentlySuccessful && <div id="child_recently_successful">Child: Form recently successful</div>}
</div>
) : (
<div id="child-no-context">No form context available</div>
)}

<div style={{ marginTop: '10px' }}>
<button type="button" onClick={submitFromChild} id="child-submit-button">
Submit from Child
</button>
<button type="button" onClick={resetFromChild} id="child-reset-button">
Reset from Child
</button>
<button type="button" onClick={clearErrorsFromChild} id="child-clear-errors-button">
Clear Errors from Child
</button>
<button type="button" onClick={setTestError} id="child-set-error-button">
Set Error from Child
</button>
<button type="button" onClick={setDefaultsFromChild} id="child-defaults-button">
Set Defaults from Child
</button>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useFormContext } from '@inertiajs/react'
import { useMemo } from 'react'

export default function DeeplyNestedComponent() {
const form = useFormContext()

// Test that we can use useMemo with the context
const formData = useMemo(() => {
if (!form) return null
return form.getData()
}, [form])

const dataDisplay = useMemo(() => {
if (!formData) return 'No data'
return JSON.stringify(formData, null, 2)
}, [formData])

return (
<div id="deeply-nested-component" style={{ border: '2px solid purple', padding: '10px', margin: '10px 0' }}>
<h4>Deeply Nested Component (using useFormContext)</h4>

{form ? (
<div id="deeply-nested-state">
<div>
Deeply Nested: Form is <span>{form.isDirty ? 'dirty' : 'clean'}</span>
</div>
{form.hasErrors && <div>Deeply Nested: Form has errors ({Object.keys(form.errors).length})</div>}
{form.processing && <div>Deeply Nested: Form is processing</div>}

<details>
<summary>Form Data (from getData())</summary>
<pre id="form-data-display">{dataDisplay}</pre>
</details>
</div>
) : (
<div id="deeply-nested-no-context">No form context available in deeply nested component</div>
)}
</div>
)
}
Loading
Loading