Skip to content

Commit 0e6cc7b

Browse files
authored
Preserve errors when loading deferred props (#2729)
* Preserve errors when loading deferred props * wip
1 parent 5dd64a9 commit 0e6cc7b

File tree

9 files changed

+192
-1
lines changed

9 files changed

+192
-1
lines changed

packages/core/src/requestParams.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export class RequestParams {
5555
return this.params.only.length > 0 || this.params.except.length > 0 || this.params.reset.length > 0
5656
}
5757

58+
public isDeferredPropsRequest() {
59+
return this.params.deferredProps === true
60+
}
61+
5862
public onCancelToken(cb: VoidFunction) {
5963
this.params.onCancelToken({
6064
cancel: cb,

packages/core/src/response.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,15 @@ export class Response {
312312

313313
pageResponse.props = { ...currentPage.get().props, ...pageResponse.props }
314314

315+
if (this.requestParams.isDeferredPropsRequest()) {
316+
const currentErrors = currentPage.get().props.errors
317+
318+
if (currentErrors && Object.keys(currentErrors).length > 0) {
319+
// Preserve existing errors during deferred props requests
320+
pageResponse.props.errors = currentErrors
321+
}
322+
}
323+
315324
// Preserve the existing scrollProps
316325
if (currentPage.get().scrollProps) {
317326
pageResponse.scrollProps = {

packages/core/src/router.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ export class Router {
115115
}
116116

117117
public reload<T extends RequestPayload = RequestPayload>(options: ReloadOptions<T> = {}): void {
118+
return this.doReload(options)
119+
}
120+
121+
protected doReload<T extends RequestPayload = RequestPayload>(
122+
options: ReloadOptions<T> & {
123+
deferredProps?: boolean
124+
} = {},
125+
): void {
118126
if (typeof window === 'undefined') {
119127
return
120128
}
@@ -521,7 +529,7 @@ export class Router {
521529
protected loadDeferredProps(deferred: Page['deferredProps']): void {
522530
if (deferred) {
523531
Object.entries(deferred).forEach(([_, group]) => {
524-
this.reload({ only: group })
532+
this.doReload({ only: group, deferredProps: true })
525533
})
526534
}
527535
}

packages/core/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ export type ActiveVisit<T extends RequestPayload = RequestPayload> = PendingVisi
424424
export type InternalActiveVisit = ActiveVisit & {
425425
onPrefetchResponse?: (response: Response) => void
426426
onPrefetchError?: (error: Error) => void
427+
deferredProps?: boolean
427428
}
428429

429430
export type VisitId = unknown
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Deferred, useForm, usePage } from '@inertiajs/react'
2+
3+
const Foo = () => {
4+
const { foo } = usePage<{ foo?: { text: string } }>().props
5+
6+
return <div id="foo">{foo?.text}</div>
7+
}
8+
9+
export default () => {
10+
const { errors } = usePage<{ errors: { name?: string } }>().props
11+
const form = useForm({
12+
name: '',
13+
})
14+
15+
const submit = () => {
16+
form.post('/deferred-props/with-errors')
17+
}
18+
19+
return (
20+
<>
21+
<Deferred data="foo" fallback={<div>Loading foo...</div>}>
22+
<Foo />
23+
</Deferred>
24+
25+
{errors?.name && <p id="page-error">{errors.name}</p>}
26+
{form.errors.name && <p id="form-error">{form.errors.name}</p>}
27+
28+
<button type="button" onClick={submit}>
29+
Submit
30+
</button>
31+
</>
32+
)
33+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script lang="ts">
2+
import { Deferred, page, useForm } from '@inertiajs/svelte'
3+
4+
export let foo: { text: string } | undefined
5+
6+
const form = useForm({
7+
name: '',
8+
})
9+
10+
const submit = () => {
11+
$form.post('/deferred-props/with-errors')
12+
}
13+
</script>
14+
15+
<Deferred data="foo">
16+
<svelte:fragment slot="fallback">
17+
<div>Loading foo...</div>
18+
</svelte:fragment>
19+
20+
<div id="foo">{foo?.text}</div>
21+
</Deferred>
22+
23+
{#if $page.props.errors?.name}
24+
<p id="page-error">{$page.props.errors.name}</p>
25+
{/if}
26+
{#if $form.errors.name}
27+
<p id="form-error">{$form.errors.name}</p>
28+
{/if}
29+
30+
<button type="button" on:click={submit}>Submit</button>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script setup lang="ts">
2+
import { Deferred, useForm } from '@inertiajs/vue3'
3+
4+
defineProps<{
5+
foo?: { text: string }
6+
}>()
7+
8+
const form = useForm({
9+
name: '',
10+
})
11+
12+
const submit = () => {
13+
form.post('/deferred-props/with-errors')
14+
}
15+
</script>
16+
17+
<template>
18+
<Deferred data="foo">
19+
<template #fallback>
20+
<div>Loading foo...</div>
21+
</template>
22+
23+
<div id="foo">{{ foo?.text }}</div>
24+
</Deferred>
25+
26+
<p v-if="$page.props.errors?.name" id="page-error">{{ $page.props.errors.name }}</p>
27+
<p v-if="form.errors.name" id="form-error">{{ form.errors.name }}</p>
28+
29+
<button type="button" @click="submit">Submit</button>
30+
</template>

tests/app/server.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,44 @@ app.get('/deferred-props/partial-reloads', (req, res) => {
912912
)
913913
})
914914

915+
let deferredPropsWithErrorsState = {}
916+
917+
app.get('/deferred-props/with-errors', (req, res) => {
918+
const errors = { ...deferredPropsWithErrorsState }
919+
920+
deferredPropsWithErrorsState = {}
921+
922+
if (!req.headers['x-inertia-partial-data']) {
923+
return inertia.render(req, res, {
924+
component: 'DeferredProps/WithErrors',
925+
deferredProps: {
926+
default: ['foo'],
927+
},
928+
props: {
929+
errors,
930+
},
931+
})
932+
}
933+
934+
setTimeout(
935+
() =>
936+
inertia.render(req, res, {
937+
component: 'DeferredProps/WithErrors',
938+
props: {
939+
foo: req.headers['x-inertia-partial-data']?.includes('foo') ? { text: 'foo value' } : undefined,
940+
errors: {},
941+
},
942+
}),
943+
250,
944+
)
945+
})
946+
947+
app.post('/deferred-props/with-errors', (req, res) => {
948+
deferredPropsWithErrorsState = { name: 'The name field is required.' }
949+
950+
res.redirect(303, '/deferred-props/with-errors')
951+
})
952+
915953
app.get('/svelte/props-and-page-store', (req, res) =>
916954
inertia.render(req, res, { component: 'Svelte/PropsAndPageStore', props: { foo: req.query.foo || 'default' } }),
917955
)

tests/deferred-props.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,41 @@ test('prefetch works with deferred props without errors', async ({ page }) => {
272272

273273
expect(consoleMessages.errors).toHaveLength(0)
274274
})
275+
276+
test('deferred props do not clear validation errors', async ({ page }) => {
277+
await page.goto('/deferred-props/with-errors')
278+
279+
await expect(page.locator('#page-error')).not.toBeVisible()
280+
await expect(page.locator('#form-error')).not.toBeVisible()
281+
await expect(page.getByText('Loading foo...')).toBeVisible()
282+
283+
await page.waitForResponse(
284+
(response) => response.request().headers()['x-inertia-partial-data'] === 'foo' && response.status() === 200,
285+
)
286+
287+
await expect(page.getByText('foo value')).toBeVisible()
288+
289+
const deferredResponsePromise = page.waitForResponse(
290+
(response) => response.request().headers()['x-inertia-partial-data'] === 'foo' && response.status() === 200,
291+
)
292+
const errorResponsePromise = page.waitForResponse(
293+
(response) => !response.request().headers()['x-inertia-partial-data'] && response.status() === 200,
294+
)
295+
296+
await page.getByRole('button', { name: 'Submit' }).click()
297+
await errorResponsePromise
298+
299+
await expect(page.locator('#page-error')).toBeVisible()
300+
await expect(page.locator('#page-error')).toHaveText('The name field is required.')
301+
await expect(page.locator('#form-error')).toBeVisible()
302+
await expect(page.locator('#form-error')).toHaveText('The name field is required.')
303+
await expect(page.getByText('Loading foo...')).toBeVisible()
304+
305+
await deferredResponsePromise
306+
307+
await expect(page.locator('#page-error')).toBeVisible()
308+
await expect(page.locator('#page-error')).toHaveText('The name field is required.')
309+
await expect(page.locator('#form-error')).toBeVisible()
310+
await expect(page.locator('#form-error')).toHaveText('The name field is required.')
311+
await expect(page.getByText('foo value')).toBeVisible()
312+
})

0 commit comments

Comments
 (0)