Skip to content

Commit 7d1836c

Browse files
committed
Merge branch 'precognition-useform' into precognition-merge
2 parents 801009f + 3364fcd commit 7d1836c

File tree

26 files changed

+681
-84
lines changed

26 files changed

+681
-84
lines changed

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,4 @@
6969
"esbuild-node-externals": "^1.19.1",
7070
"typescript": "^5.9.3"
7171
}
72-
}
72+
}

packages/react/src/WhenVisible.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ReloadOptions, router } from '@inertiajs/core'
22
import { createElement, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
3+
import usePage from './usePage'
34

45
interface WhenVisibleProps {
56
children: ReactNode | (() => ReactNode)
@@ -17,9 +18,25 @@ const WhenVisible = ({ children, data, params, buffer, as, always, fallback }: W
1718
fallback = fallback ?? null
1819

1920
const [loaded, setLoaded] = useState(false)
20-
const hasFetched = useRef<boolean>(false)
2121
const fetching = useRef<boolean>(false)
2222
const ref = useRef<HTMLDivElement>(null)
23+
const observer = useRef<IntersectionObserver | null>(null)
24+
25+
const page = usePage()
26+
27+
useEffect(() => {
28+
if (Array.isArray(data)) {
29+
// For arrays, reset loaded if any prop becomes undefined
30+
if (data.some((key) => page.props[key] === undefined)) {
31+
setLoaded(false)
32+
}
33+
} else if (data) {
34+
// For single prop, reset loaded if prop becomes undefined
35+
if (page.props[data] === undefined) {
36+
setLoaded(false)
37+
}
38+
}
39+
}, [data, ...(Array.isArray(data) ? data.map((key) => page.props[key]) : [page.props[data!]])])
2340

2441
const getReloadParams = useCallback<() => Partial<ReloadOptions>>(() => {
2542
if (data) {
@@ -35,26 +52,23 @@ const WhenVisible = ({ children, data, params, buffer, as, always, fallback }: W
3552
return params
3653
}, [params, data])
3754

38-
useEffect(() => {
39-
if (!ref.current) {
40-
return
41-
}
55+
const registerObserver = () => {
56+
observer.current?.disconnect()
4257

43-
const observer = new IntersectionObserver(
58+
observer.current = new IntersectionObserver(
4459
(entries) => {
4560
if (!entries[0].isIntersecting) {
4661
return
4762
}
4863

49-
if (!always && hasFetched.current) {
50-
observer.disconnect()
64+
if (fetching.current) {
65+
return
5166
}
5267

53-
if (fetching.current) {
68+
if (!always && loaded) {
5469
return
5570
}
5671

57-
hasFetched.current = true
5872
fetching.current = true
5973

6074
const reloadParams = getReloadParams()
@@ -71,7 +85,7 @@ const WhenVisible = ({ children, data, params, buffer, as, always, fallback }: W
7185
reloadParams.onFinish?.(e)
7286

7387
if (!always) {
74-
observer.disconnect()
88+
observer.current?.disconnect()
7589
}
7690
},
7791
})
@@ -81,12 +95,20 @@ const WhenVisible = ({ children, data, params, buffer, as, always, fallback }: W
8195
},
8296
)
8397

84-
observer.observe(ref.current)
98+
observer.current.observe(ref.current!)
99+
}
100+
101+
useEffect(() => {
102+
if (!ref.current) {
103+
return
104+
}
105+
106+
registerObserver()
85107

86108
return () => {
87-
observer.disconnect()
109+
observer.current?.disconnect()
88110
}
89-
}, [ref, getReloadParams, buffer])
111+
}, [loaded, ref, getReloadParams, buffer])
90112

91113
const resolveChildren = () => (typeof children === 'function' ? children() : children)
92114
const resolveFallback = () => (typeof fallback === 'function' ? fallback() : fallback)

packages/react/src/useForm.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export interface InertiaFormValidationProps<TForm extends object> {
9292
validateFiles(): InertiaPrecognitiveFormProps<TForm>
9393
validating: boolean
9494
validator: () => Validator
95-
withFullErrors(): InertiaPrecognitiveFormProps<TForm>
95+
withArrayErrors(): InertiaPrecognitiveFormProps<TForm>
9696
withoutFileValidation(): InertiaPrecognitiveFormProps<TForm>
9797
// Backward compatibility for easy migration from the original Precognition libraries
9898
setErrors(errors: FormDataErrors<TForm>): InertiaPrecognitiveFormProps<TForm>
@@ -148,8 +148,8 @@ export default function useForm<TForm extends FormDataType<TForm>>(
148148
const [validating, setValidating] = useState(false)
149149
const [touchedFields, setTouchedFields] = useState<string[]>([])
150150
const [validFields, setValidFields] = useState<string[]>([])
151-
const simpleValidationErrors = useRef(true)
152151
const validatorDefaults = cloneDeep(defaults)
152+
const arrayErrors = useRef(false)
153153

154154
useEffect(() => {
155155
isMounted.current = true
@@ -524,9 +524,9 @@ export default function useForm<TForm extends FormDataType<TForm>>(
524524
setTouchedFields(validator.touched())
525525
})
526526
.on('errorsChanged', () => {
527-
const validationErrors = simpleValidationErrors.current
528-
? toSimpleValidationErrors(validator.errors())
529-
: validator.errors()
527+
const validationErrors = arrayErrors.current
528+
? validator.errors()
529+
: toSimpleValidationErrors(validator.errors())
530530

531531
setErrors(validationErrors as FormDataErrors<TForm>)
532532
setHasErrors(Object.keys(validationErrors).length > 0)
@@ -557,7 +557,7 @@ export default function useForm<TForm extends FormDataType<TForm>>(
557557

558558
return precognitiveForm
559559
},
560-
withFullErrors: () => tap(precognitiveForm, () => (simpleValidationErrors.current = false)),
560+
withArrayErrors: () => tap(precognitiveForm, () => (arrayErrors.current = true)),
561561
setValidationTimeout: (duration: number) =>
562562
tap(precognitiveForm, () => validatorRef.current?.setTimeout(duration)),
563563
validateFiles: () => tap(precognitiveForm, () => validatorRef.current?.validateFiles()),
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Deferred, router, usePage } from '@inertiajs/react'
2+
3+
const FooTimestamp = () => {
4+
const { foo } = usePage<{ foo?: { timestamp: string } }>().props
5+
6+
return <div id="foo-timestamp">{foo?.timestamp}</div>
7+
}
8+
9+
const BarTimestamp = () => {
10+
const { bar } = usePage<{ bar?: { timestamp: string } }>().props
11+
12+
return <div id="bar-timestamp">{bar?.timestamp}</div>
13+
}
14+
15+
const PartialReloads = () => {
16+
const reloadOnlyFoo = () => {
17+
router.reload({
18+
only: ['foo'],
19+
})
20+
}
21+
22+
const reloadOnlyBar = () => {
23+
router.reload({
24+
only: ['bar'],
25+
})
26+
}
27+
28+
const reloadBoth = () => {
29+
router.reload({
30+
only: ['foo', 'bar'],
31+
})
32+
}
33+
34+
return (
35+
<>
36+
<Deferred data="foo" fallback={<div>Loading foo...</div>}>
37+
<FooTimestamp />
38+
</Deferred>
39+
40+
<Deferred data="bar" fallback={<div>Loading bar...</div>}>
41+
<BarTimestamp />
42+
</Deferred>
43+
44+
<button onClick={reloadOnlyFoo}>Reload foo only</button>
45+
<button onClick={reloadOnlyBar}>Reload bar only</button>
46+
<button onClick={reloadBoth}>Reload both</button>
47+
</>
48+
)
49+
}
50+
51+
export default PartialReloads

packages/react/test-app/Pages/FormHelper/Precognition/AllErrors.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default () => {
77
})
88
.withPrecognition('post', '/precognition/array-errors')
99
.setValidationTimeout(100)
10-
.withFullErrors()
10+
.withArrayErrors()
1111

1212
return (
1313
<div>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { router, WhenVisible } from '@inertiajs/react'
2+
3+
interface Props {
4+
firstData?: {
5+
text: string
6+
}
7+
secondData?: {
8+
text: string
9+
}
10+
}
11+
12+
export default ({ firstData, secondData }: Props) => {
13+
const handleReload = () => {
14+
router.reload()
15+
}
16+
17+
return (
18+
<div>
19+
<h1>WhenVisible + Array Props + Reload</h1>
20+
21+
<button onClick={handleReload}>Reload Page</button>
22+
23+
<div style={{ marginTop: '2000px', padding: '20px', border: '1px solid #ccc' }}>
24+
<WhenVisible data={['firstData', 'secondData']} fallback={<p>Loading array data...</p>}>
25+
<div>
26+
<p>{firstData?.text}</p>
27+
<p>{secondData?.text}</p>
28+
</div>
29+
</WhenVisible>
30+
</div>
31+
</div>
32+
)
33+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { router, WhenVisible } from '@inertiajs/react'
2+
3+
interface Props {
4+
lazyData?: {
5+
text: string
6+
}
7+
}
8+
9+
export default ({ lazyData }: Props) => {
10+
const handleReload = () => {
11+
router.reload()
12+
}
13+
14+
return (
15+
<div>
16+
<h1>WhenVisible + Reload</h1>
17+
18+
<button onClick={handleReload}>Reload Page</button>
19+
20+
<div style={{ marginTop: '2000px', padding: '20px', border: '1px solid #ccc' }}>
21+
<WhenVisible data="lazyData" fallback={<p>Loading lazy data...</p>}>
22+
{lazyData?.text}
23+
</WhenVisible>
24+
</div>
25+
</div>
26+
)
27+
}

packages/svelte/src/components/WhenVisible.svelte

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { router, type ReloadOptions } from '@inertiajs/core'
3-
import { onDestroy, onMount } from 'svelte'
3+
import { onDestroy } from 'svelte'
4+
import { usePage } from '../page'
45
56
export let data: string | string[] = ''
67
export let params: ReloadOptions = {}
@@ -13,22 +14,38 @@
1314
let el: HTMLElement
1415
let observer: IntersectionObserver | null = null
1516
16-
onMount(() => {
17-
if (!el) {
18-
return
17+
const page = usePage()
18+
19+
// Watch for page prop changes and reset loaded state when data becomes undefined
20+
$: {
21+
if (Array.isArray(data)) {
22+
// For arrays, reset loaded if any prop becomes undefined
23+
if (data.some((key) => $page.props[key] === undefined)) {
24+
loaded = false
25+
}
26+
} else if ($page.props[data as string] === undefined) {
27+
loaded = false
1928
}
29+
}
30+
31+
$: if (el) {
32+
registerObserver()
33+
}
34+
35+
function registerObserver() {
36+
observer?.disconnect()
2037
2138
observer = new IntersectionObserver(
2239
(entries) => {
2340
if (!entries[0].isIntersecting) {
2441
return
2542
}
2643
27-
if (!always) {
28-
observer?.disconnect()
44+
if (fetching) {
45+
return
2946
}
3047
31-
if (fetching) {
48+
if (!always && loaded) {
3249
return
3350
}
3451
@@ -46,6 +63,10 @@
4663
loaded = true
4764
fetching = false
4865
reloadParams.onFinish?.(event)
66+
67+
if (!always) {
68+
observer?.disconnect()
69+
}
4970
},
5071
})
5172
},
@@ -55,7 +76,7 @@
5576
)
5677
5778
observer.observe(el)
58-
})
79+
}
5980
6081
onDestroy(() => {
6182
observer?.disconnect()

packages/svelte/src/useForm.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export interface InertiaFormValidationProps<TForm extends object> {
8181
validateFiles(): this
8282
validating: boolean
8383
validator: () => Validator
84-
withFullErrors(): this
84+
withArrayErrors(): this
8585
withoutFileValidation(): this
8686
// Backward compatibility for easy migration from the original Precognition libraries
8787
setErrors(errors: FormDataErrors<TForm> | Record<string, string | string[]>): this
@@ -145,7 +145,7 @@ export default function useForm<TForm extends FormDataType<TForm>>(
145145
const formWithPrecognition = () =>
146146
getStore(store) as any as InertiaPrecognitiveForm<TForm> & InternalPrecognitionState
147147

148-
let simpleValidationErrors = true
148+
let arrayErrors = false
149149

150150
if (!validatorRef) {
151151
const validator = createValidator((client) => {
@@ -168,9 +168,7 @@ export default function useForm<TForm extends FormDataType<TForm>>(
168168
setFormState('__touched', validator.touched())
169169
})
170170
.on('errorsChanged', () => {
171-
const validationErrors = simpleValidationErrors
172-
? toSimpleValidationErrors(validator.errors())
173-
: validator.errors()
171+
const validationErrors = arrayErrors ? validator.errors() : toSimpleValidationErrors(validator.errors())
174172

175173
setFormState('errors', {} as FormDataErrors<TForm>)
176174
formWithPrecognition().setError(validationErrors as FormDataErrors<TForm>)
@@ -235,7 +233,7 @@ export default function useForm<TForm extends FormDataType<TForm>>(
235233
validateFiles: () => tap(formWithPrecognition(), () => validatorRef?.validateFiles()),
236234
setValidationTimeout: (duration: number) =>
237235
tap(formWithPrecognition(), () => validatorRef!.setTimeout(duration)),
238-
withFullErrors: () => tap(formWithPrecognition(), () => (simpleValidationErrors = false)),
236+
withArrayErrors: () => tap(formWithPrecognition(), () => (arrayErrors = true)),
239237
// @ts-expect-error - Not released yet...
240238
withoutFileValidation: () => tap(formWithPrecognition(), () => validatorRef?.withoutFileValidation()),
241239
valid: (field: string) => formWithPrecognition().__valid.includes(field),

0 commit comments

Comments
 (0)