Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
edf45be
wip
pascalbaljet Oct 1, 2025
3fd3af9
wip
pascalbaljet Oct 1, 2025
fabb789
wip
pascalbaljet Oct 1, 2025
e71367c
wip
pascalbaljet Oct 2, 2025
eba97a1
test
pascalbaljet Oct 2, 2025
41ce2a6
Update form-component.spec.ts
pascalbaljet Oct 2, 2025
0a1d80e
wip
pascalbaljet Oct 2, 2025
b3608ac
Update package.json
pascalbaljet Oct 2, 2025
dc31fa0
Update form.ts
pascalbaljet Oct 3, 2025
6bf015a
refactor
pascalbaljet Oct 3, 2025
3ef1f6d
wip
pascalbaljet Oct 3, 2025
131dfe7
test
pascalbaljet Oct 3, 2025
fb3f9e6
tests
pascalbaljet Oct 3, 2025
d205af1
wip
pascalbaljet Oct 3, 2025
e73b45d
split
pascalbaljet Oct 3, 2025
3e43529
valid method
pascalbaljet Oct 3, 2025
2c23e1d
props
pascalbaljet Oct 3, 2025
c2e9fd4
files
pascalbaljet Oct 3, 2025
a54f9a5
Update precognition.ts
pascalbaljet Oct 3, 2025
85a1398
react
pascalbaljet Oct 3, 2025
17f4bbc
svelte
pascalbaljet Oct 3, 2025
b73535c
refactor
pascalbaljet Oct 3, 2025
cea03db
transform + callbacks
pascalbaljet Oct 3, 2025
0da0760
types
pascalbaljet Oct 3, 2025
4c735d3
Update types.ts
pascalbaljet Oct 3, 2025
3e75998
refactor
pascalbaljet Oct 3, 2025
4a59c16
Revert playground
pascalbaljet Oct 3, 2025
0733ab0
Update Form.svelte
pascalbaljet Oct 3, 2025
ec8b445
Used transformed data for validation
pascalbaljet Oct 7, 2025
361c22b
wip
pascalbaljet Oct 7, 2025
d56cce6
React + Svelte + refactor
pascalbaljet Oct 7, 2025
9387e8c
Merge branch 'master' into form-precognition
pascalbaljet Oct 7, 2025
e16c2ab
Update PrecognitionReset.vue
pascalbaljet Oct 7, 2025
25774a3
Force simple errors payload
pascalbaljet Oct 7, 2025
f502fdd
improve test
pascalbaljet Oct 7, 2025
0587acb
`touched()` method
pascalbaljet Oct 8, 2025
cd16aba
Prop for errors in array format
pascalbaljet Oct 8, 2025
8c34a38
Update Form.svelte
pascalbaljet Oct 8, 2025
4601332
Update server.js
pascalbaljet Oct 9, 2025
c734518
Improve cancel test
pascalbaljet Oct 9, 2025
5d40fa5
wip
pascalbaljet Oct 9, 2025
161bc5b
manual cancel
pascalbaljet Oct 9, 2025
5ed51bb
Remove redundant test
pascalbaljet Oct 9, 2025
d105ede
Unify tests
pascalbaljet Oct 9, 2025
8343809
Unify tests
pascalbaljet Oct 9, 2025
eec5986
Unify tests
pascalbaljet Oct 9, 2025
a98ecb4
Refine test suite
pascalbaljet Oct 14, 2025
b68d8e1
Refine tests
pascalbaljet Oct 14, 2025
68d7ed3
Pass validation timeout
pascalbaljet Oct 14, 2025
cb24e3e
Cleanup
pascalbaljet Oct 14, 2025
27aff2b
Vue playground
pascalbaljet Oct 14, 2025
94d83b5
React playground
pascalbaljet Oct 14, 2025
29f49eb
Svelte 4 playground
pascalbaljet Oct 14, 2025
ddd58e2
Svelte 5 playground
pascalbaljet Oct 14, 2025
3298c42
Make Prettier happier
pascalbaljet Oct 14, 2025
be8d679
Renamed `onBeforeValidation` to `onBefore`
pascalbaljet Oct 15, 2025
ae1d2ce
Improve playground
pascalbaljet Oct 15, 2025
192f981
Build upon `laravel-precognition` library
pascalbaljet Oct 30, 2025
8425e8d
React
pascalbaljet Oct 30, 2025
622f237
Svelte
pascalbaljet Oct 30, 2025
975c168
Merge branch 'master' into form-precognition
pascalbaljet Oct 30, 2025
56bc441
fixes
pascalbaljet Oct 30, 2025
701cf43
Fix code style
pascalbaljet Oct 30, 2025
adb1a47
Revert custom implementations
pascalbaljet Oct 30, 2025
3eaca3b
Merge branch 'form-precognition' of https://github.com/inertiajs/iner…
pascalbaljet Oct 30, 2025
2d2307a
Remove dep
pascalbaljet Oct 30, 2025
7a334ea
Revert "Remove dep"
pascalbaljet Oct 30, 2025
1691cc7
align
pascalbaljet Oct 30, 2025
66aa717
Update form.ts
pascalbaljet Oct 30, 2025
1040f92
fix watchers
pascalbaljet Oct 30, 2025
63a2ad9
wip
pascalbaljet Oct 31, 2025
8323d99
Update types.ts
pascalbaljet Oct 31, 2025
cdad30f
fix type
pascalbaljet Oct 31, 2025
7c8a30f
improve tests
pascalbaljet Oct 31, 2025
4f2d66d
cleanup
pascalbaljet Oct 31, 2025
b3edfa2
playgrounds
pascalbaljet Oct 31, 2025
5ad07b7
playgrounds
pascalbaljet Oct 31, 2025
a3146c9
move import
pascalbaljet Oct 31, 2025
7295538
Merge branch 'form-precognition' into precognition-merge
pascalbaljet Nov 4, 2025
af27873
Merge branch 'precognition-useform' into precognition-merge
pascalbaljet Nov 7, 2025
adbd9fa
Merge branch 'precognition-useform' into precognition-merge
pascalbaljet Nov 7, 2025
1c6f45a
wip
pascalbaljet Nov 7, 2025
5e76844
Update form.ts
pascalbaljet Nov 7, 2025
c4ca252
wip
pascalbaljet Nov 7, 2025
3d13ee5
wip
pascalbaljet Nov 7, 2025
76ef218
wip
pascalbaljet Nov 7, 2025
5aeab5e
wip
pascalbaljet Nov 7, 2025
7a991eb
wip
pascalbaljet Nov 11, 2025
3f6807e
Delete PrecognitionMethods.vue
pascalbaljet Nov 11, 2025
1b6f152
Update Form.svelte
pascalbaljet Nov 11, 2025
1e074b2
Merge branch 'precognition-useform' into precognition-merge
pascalbaljet Nov 13, 2025
801009f
Added ref test
pascalbaljet Nov 13, 2025
7d1836c
Merge branch 'precognition-useform' into precognition-merge
pascalbaljet Nov 14, 2025
834f9a9
Flipped `simpleValidationErrors` to `arrayErrors`
pascalbaljet Nov 14, 2025
f411db5
Merge branch 'master' into precognition-merge
pascalbaljet Nov 18, 2025
9281af2
Renamed `arrayErrors` to `withArrayErrors`
pascalbaljet Nov 20, 2025
cdeaf5c
wip
pascalbaljet Nov 20, 2025
5469f10
Merge branch 'precognition-useform' into precognition-merge
pascalbaljet Nov 20, 2025
980d11c
wip
pascalbaljet Nov 20, 2025
cb33051
Merge branch 'precognition-useform' into precognition-merge
pascalbaljet Nov 26, 2025
4ae1009
wip
pascalbaljet Nov 26, 2025
b77c90c
wip
pascalbaljet Nov 26, 2025
5f245ae
Rename `validateTimeout` to `validationTimeout`
pascalbaljet Nov 28, 2025
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
8 changes: 5 additions & 3 deletions packages/core/src/resetFormFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,10 @@ export function resetFormFields(formElement: HTMLFormElement, defaults: FormData
return
}

const resetEntireForm = !fieldNames || fieldNames.length === 0

// If no specific fields provided, reset the entire form
if (!fieldNames || fieldNames.length === 0) {
if (resetEntireForm) {
// Get all field names from both defaults and form elements (including disabled ones)
const formData = new FormData(formElement)
const formElementNames = Array.from(formElement.elements)
Expand All @@ -173,7 +175,7 @@ export function resetFormFields(formElement: HTMLFormElement, defaults: FormData

let hasChanged = false

fieldNames.forEach((fieldName) => {
fieldNames!.forEach((fieldName) => {
const elements = formElement.elements.namedItem(fieldName)

if (elements) {
Expand All @@ -184,7 +186,7 @@ export function resetFormFields(formElement: HTMLFormElement, defaults: FormData
})

// Dispatch reset event if any field changed (matching native form.reset() behavior)
if (hasChanged) {
if (hasChanged && resetEntireForm) {
formElement.dispatchEvent(new Event('reset', { bubbles: true }))
}
}
11 changes: 11 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AxiosProgressEvent, AxiosResponse } from 'axios'
import { NamedInputEvent, ValidationConfig, Validator } from 'laravel-precognition'
import { Response } from './response'

declare module 'axios' {
Expand Down Expand Up @@ -621,6 +622,9 @@ export type FormComponentProps = Partial<
resetOnSuccess?: boolean | string[]
resetOnError?: boolean | string[]
setDefaultsOnSuccess?: boolean
validateFiles?: boolean
validationTimeout?: number
withAllErrors?: boolean
}

export type FormComponentMethods = {
Expand All @@ -633,6 +637,12 @@ export type FormComponentMethods = {
defaults: () => void
getData: () => Record<string, FormDataConvertible>
getFormData: () => FormData
valid: (field: string) => boolean
invalid: (field: string) => boolean
validate(field?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig): void
touch: (...fields: string[]) => void
touched(field?: string): boolean
validator: () => Validator
}

export type FormComponentonSubmitCompleteArguments = Pick<FormComponentMethods, 'reset' | 'defaults'>
Expand All @@ -645,6 +655,7 @@ export type FormComponentState = {
wasSuccessful: boolean
recentlySuccessful: boolean
isDirty: boolean
validating: boolean
}

export type FormComponentSlotProps = FormComponentMethods & FormComponentState
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/useFormUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NamedInputEvent, ValidationConfig } from 'laravel-precognition'
import {
FormDataType,
Method,
Expand Down Expand Up @@ -113,4 +114,34 @@ export class UseFormUtils {
// Use Precognition endpoint with optional options...
return { ...precognitionEndpoint!(), options: (args[0] as UseFormSubmitOptions) ?? {} }
}

/**
* Merges headers into the Precognition validate() arguments.
*/
public static mergeHeadersForValidation(
field?: string | NamedInputEvent | ValidationConfig,
config?: ValidationConfig,
headers?: Record<string, string>,
): [string | NamedInputEvent | ValidationConfig | undefined, ValidationConfig | undefined] {
const merge = (config: ValidationConfig): ValidationConfig => {
config.headers = {
...(headers ?? {}),
...(config.headers ?? {}),
}

return config
}

if (field && typeof field === 'object' && !('target' in field)) {
field = merge(field)
} else if (config && typeof config === 'object') {
config = merge(config)
} else if (typeof field === 'string') {
config = merge(config ?? {})
} else {
field = merge(field ?? {})
}

return [field, config]
}
}
83 changes: 73 additions & 10 deletions packages/react/src/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {
mergeDataIntoQueryString,
Method,
resetFormFields,
UseFormUtils,
VisitOptions,
} from '@inertiajs/core'
import { NamedInputEvent, ValidationConfig } from 'laravel-precognition'
import { isEqual } from 'lodash-es'
import React, {
createElement,
Expand Down Expand Up @@ -64,12 +66,36 @@ const Form = forwardRef<FormComponentRef, ComponentProps>(
resetOnSuccess = false,
setDefaultsOnSuccess = false,
invalidateCacheTags = [],
validateFiles = false,
validationTimeout = 1500,
withAllErrors = false,
children,
...props
},
ref,
) => {
const getTransformedData = (): Record<string, FormDataConvertible> => {
const [_url, data] = getUrlAndData()
return transform(data)
}

const form = useForm<Record<string, any>>({})
.withPrecognition(
() => resolvedMethod,
() => getUrlAndData()[0],
)
.setValidationTimeout(validationTimeout)

if (validateFiles) {
form.validateFiles()
}

if (withAllErrors) {
form.withAllErrors()
}

form.transform(getTransformedData)

const formElement = useRef<HTMLFormElement>(undefined)

const resolvedMethod = useMemo(() => {
Expand All @@ -86,29 +112,62 @@ const Form = forwardRef<FormComponentRef, ComponentProps>(
// expects an object, and submitting a FormData instance directly causes problems with nested objects.
const getData = (): Record<string, FormDataConvertible> => formDataToObject(getFormData())

const getUrlAndData = (): [string, Record<string, FormDataConvertible>] => {
return mergeDataIntoQueryString(
resolvedMethod,
isUrlMethodPair(action) ? action.url : action,
getData(),
queryStringArrayFormat,
)
}

const updateDirtyState = (event: Event) =>
deferStateUpdate(() =>
setIsDirty(event.type === 'reset' ? false : !isEqual(getData(), formDataToObject(defaultData.current))),
)

const clearErrors = (...names: string[]) => {
form.clearErrors(...names)

return form
}

useEffect(() => {
defaultData.current = getFormData()

form.setDefaults(getData())

const formEvents: Array<keyof HTMLElementEventMap> = ['input', 'change', 'reset']

formEvents.forEach((e) => formElement.current!.addEventListener(e, updateDirtyState))

return () => formEvents.forEach((e) => formElement.current?.removeEventListener(e, updateDirtyState))
return () => {
formEvents.forEach((e) => formElement.current?.removeEventListener(e, updateDirtyState))
}
}, [])

useEffect(() => {
form.setValidationTimeout(validationTimeout)
}, [validationTimeout])

useEffect(() => {
if (validateFiles) {
form.validateFiles()
} else {
form.withoutFileValidation()
}
}, [validateFiles])

const reset = (...fields: string[]) => {
if (formElement.current) {
resetFormFields(formElement.current, defaultData.current, fields)
}

form.reset(...fields)
}

const resetAndClearErrors = (...fields: string[]) => {
form.clearErrors(...fields)
clearErrors(...fields)
reset(...fields)
}

Expand All @@ -125,12 +184,7 @@ const Form = forwardRef<FormComponentRef, ComponentProps>(
}

const submit = () => {
const [url, _data] = mergeDataIntoQueryString(
resolvedMethod,
isUrlMethodPair(action) ? action.url : action,
getData(),
queryStringArrayFormat,
)
const [url, _data] = getUrlAndData()

const submitOptions: FormSubmitOptions = {
headers,
Expand Down Expand Up @@ -163,7 +217,6 @@ const Form = forwardRef<FormComponentRef, ComponentProps>(
...options,
}

form.transform(() => transform(_data))
form.submit(resolvedMethod, url, submitOptions)
}

Expand All @@ -180,14 +233,24 @@ const Form = forwardRef<FormComponentRef, ComponentProps>(
wasSuccessful: form.wasSuccessful,
recentlySuccessful: form.recentlySuccessful,
isDirty,
clearErrors: form.clearErrors,
clearErrors,
resetAndClearErrors,
setError: form.setError,
reset,
submit,
defaults,
getData,
getFormData,

// Precognition
validator: () => form.validator(),
validating: form.validating,
valid: form.valid,
invalid: form.invalid,
validate: (field?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig) =>
form.validate(...UseFormUtils.mergeHeadersForValidation(field, config, headers)),
touch: form.touch,
touched: form.touched,
})

useImperativeHandle(ref, exposed, [form, isDirty, submit])
Expand Down
15 changes: 11 additions & 4 deletions packages/react/src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,20 +296,27 @@ export default function useForm<TForm extends FormDataType<TForm>>(
const setDefaultsFunction = useCallback(
(fieldOrFields?: FormDataKeys<TForm> | Partial<TForm>, maybeValue?: unknown) => {
setDefaultsCalledInOnSuccess.current = true
let newDefaults = {} as TForm

if (typeof fieldOrFields === 'undefined') {
newDefaults = { ...dataRef.current }
setDefaults(dataRef.current)
// If setData was called right before setDefaults, data was not
// updated in that render yet, so we set a flag to update
// defaults right after the next render.
setDataAsDefaults(true)
} else {
setDefaults((defaults) => {
return typeof fieldOrFields === 'string'
? set(cloneDeep(defaults), fieldOrFields, maybeValue)
: Object.assign(cloneDeep(defaults), fieldOrFields)
newDefaults =
typeof fieldOrFields === 'string'
? set(cloneDeep(defaults), fieldOrFields, maybeValue)
: Object.assign(cloneDeep(defaults), fieldOrFields)

return newDefaults as TForm
})
}

validatorRef.current?.defaults(newDefaults)
},
[setDefaults],
)
Expand Down Expand Up @@ -496,7 +503,7 @@ export default function useForm<TForm extends FormDataType<TForm>>(
const currentData = dataRef.current
const transformedData = transform.current(currentData) as Record<string, unknown>
return client[method](url, transformedData)
}, defaults)
}, cloneDeep(defaults))

validatorRef.current = validator

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Form } from '@inertiajs/react'
import { isEqual } from 'lodash-es'

export default function PrecognitionBefore() {
const handleBeforeValidation = (
newRequest: { data: Record<string, unknown> | null; touched: string[] },
oldRequest: { data: Record<string, unknown> | null; touched: string[] },
) => {
const payloadIsCorrect =
isEqual(newRequest, { data: { name: 'block' }, touched: ['name'] }) &&
isEqual(oldRequest, { data: {}, touched: [] })

if (payloadIsCorrect && newRequest.data?.name === 'block') {
return false
}

return true
}

return (
<div>
<h1>Precognition - onBefore</h1>

<Form action="/precognition/default" method="post" validationTimeout={100}>
{({ errors, invalid, validate, validating }) => (
<>
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
name="name"
onChange={() =>
validate('name', {
onBeforeValidation: handleBeforeValidation,
})
}
/>
{invalid('name') && <p className="error">{errors.name}</p>}
</div>

<div>
<label htmlFor="email">Email:</label>
<input id="email" name="email" onChange={() => validate('email')} />
{invalid('email') && <p className="error">{errors.email}</p>}
</div>

{validating && <p className="validating">Validating...</p>}

<button type="submit">Submit</button>
</>
)}
</Form>
</div>
)
}
Loading