diff --git a/packages/core/src/router.ts b/packages/core/src/router.ts index b71b0314d..7d27fe232 100644 --- a/packages/core/src/router.ts +++ b/packages/core/src/router.ts @@ -11,6 +11,7 @@ import { RequestStream } from './requestStream' import { Scroll } from './scroll' import { ActiveVisit, + ClientSideVisitOptions, GlobalEvent, GlobalEventNames, GlobalEventResult, @@ -255,6 +256,33 @@ export class Router { return history.decrypt() } + public replace(params: ClientSideVisitOptions): void { + this.clientVisit(params, { replace: true }) + } + + public push(params: ClientSideVisitOptions): void { + this.clientVisit(params) + } + + protected clientVisit(params: ClientSideVisitOptions, { replace = false }: { replace?: boolean } = {}): void { + const current = currentPage.get() + + const props = typeof params.props === 'function' ? params.props(current.props) : params.props ?? current.props + + currentPage.set( + { + ...current, + ...params, + props, + }, + { + replace, + preserveScroll: params.preserveScroll, + preserveState: params.preserveState, + }, + ) + } + protected getPrefetchParams(href: string | URL, options: VisitOptions): ActiveVisit { return { ...this.getPendingVisit(href, { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 879916b63..860c8793a 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -49,6 +49,16 @@ export interface Page { rememberedState: Record } +export interface ClientSideVisitOptions { + component?: Page['component'] + url?: Page['url'] + props?: ((props: Page['props']) => Page['props']) | Page['props'] + clearHistory?: Page['clearHistory'] + encryptHistory?: Page['encryptHistory'] + preserveScroll?: VisitOptions['preserveScroll'] + preserveState?: VisitOptions['preserveState'] +} + export type PageResolver = (name: string) => Component export type PageHandler = ({ diff --git a/packages/react/test-app/Pages/ClientSideVisit/Page1.jsx b/packages/react/test-app/Pages/ClientSideVisit/Page1.jsx new file mode 100644 index 000000000..ae4545efe --- /dev/null +++ b/packages/react/test-app/Pages/ClientSideVisit/Page1.jsx @@ -0,0 +1,26 @@ +import { router } from '@inertiajs/react' + +export default ({ foo, bar }) => { + const replace = () => { + router.replace({ + props: (props) => ({ ...props, foo: 'foo from client' }), + }) + } + + const push = () => { + router.push({ + url: '/client-side-visit-2', + component: 'ClientSideVisit/Page2', + props: { baz: 'baz from client' }, + }) + } + + return ( +
+
{foo}
+
{bar}
+ + +
+ ) +} diff --git a/packages/react/test-app/Pages/ClientSideVisit/Page2.jsx b/packages/react/test-app/Pages/ClientSideVisit/Page2.jsx new file mode 100644 index 000000000..724dc41d4 --- /dev/null +++ b/packages/react/test-app/Pages/ClientSideVisit/Page2.jsx @@ -0,0 +1,3 @@ +export default ({ baz }) => { + return
{baz}
+} diff --git a/packages/svelte/test-app/Pages/ClientSideVisit/Page1.svelte b/packages/svelte/test-app/Pages/ClientSideVisit/Page1.svelte new file mode 100644 index 000000000..ad1c01653 --- /dev/null +++ b/packages/svelte/test-app/Pages/ClientSideVisit/Page1.svelte @@ -0,0 +1,27 @@ + + +
+
{foo}
+
{bar}
+ + +
diff --git a/packages/svelte/test-app/Pages/ClientSideVisit/Page2.svelte b/packages/svelte/test-app/Pages/ClientSideVisit/Page2.svelte new file mode 100644 index 000000000..eb6953a23 --- /dev/null +++ b/packages/svelte/test-app/Pages/ClientSideVisit/Page2.svelte @@ -0,0 +1,5 @@ + + +
{baz}
diff --git a/packages/vue3/test-app/Pages/ClientSideVisit/Page1.vue b/packages/vue3/test-app/Pages/ClientSideVisit/Page1.vue new file mode 100644 index 000000000..fd80f58ff --- /dev/null +++ b/packages/vue3/test-app/Pages/ClientSideVisit/Page1.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/vue3/test-app/Pages/ClientSideVisit/Page2.vue b/packages/vue3/test-app/Pages/ClientSideVisit/Page2.vue new file mode 100644 index 000000000..50e75e112 --- /dev/null +++ b/packages/vue3/test-app/Pages/ClientSideVisit/Page2.vue @@ -0,0 +1,9 @@ + + + diff --git a/tests/app/server.js b/tests/app/server.js index e17e23f3d..91b69610e 100644 --- a/tests/app/server.js +++ b/tests/app/server.js @@ -74,6 +74,13 @@ app.get('/links/headers/version', (req, res) => app.get('/links/data-loading', (req, res) => inertia.render(req, res, { component: 'Links/DataLoading' })) app.get('/links/prop-update', (req, res) => inertia.render(req, res, { component: 'Links/PropUpdate' })) +app.get('/client-side-visit', (req, res) => + inertia.render(req, res, { + component: 'ClientSideVisit/Page1', + props: { foo: 'foo from server', bar: 'bar from server' }, + }), +) + app.get('/visits/partial-reloads', (req, res) => inertia.render(req, res, { component: 'Visits/PartialReloads', diff --git a/tests/client-side-visits.spec.ts b/tests/client-side-visits.spec.ts new file mode 100644 index 000000000..de0e0cf99 --- /dev/null +++ b/tests/client-side-visits.spec.ts @@ -0,0 +1,52 @@ +import test, { expect } from '@playwright/test' +import { pageLoads, requests } from './support' + +test('replaces the page client side', async ({ page }) => { + pageLoads.watch(page) + + await page.goto('/client-side-visit') + + requests.listen(page) + + await expect(page.getByText('foo from server')).toBeVisible() + await expect(page.getByText('bar from server')).toBeVisible() + await expect(page.getByText('foo from client')).not.toBeVisible() + + await page.getByRole('button', { name: 'Replace' }).click() + + await expect(page).toHaveURL('/client-side-visit') + await expect(page.getByText('foo from server')).not.toBeVisible() + await expect(page.getByText('foo from client')).toBeVisible() + await expect(page.getByText('bar from server')).toBeVisible() + + await expect(requests.requests.length).toBe(0) + + const historyLength = await page.evaluate(() => window.history.length) + + await expect(historyLength).toBe(2) +}) + +test('pushes the page client side', async ({ page }) => { + pageLoads.watch(page) + + await page.goto('/client-side-visit') + + requests.listen(page) + + await expect(page.getByText('foo from server')).toBeVisible() + await expect(page.getByText('bar from server')).toBeVisible() + await expect(page.getByText('baz from client')).not.toBeVisible() + + await page.getByRole('button', { name: 'Push' }).click() + + await expect(page).toHaveURL('/client-side-visit-2') + await expect(page.getByText('foo from server')).not.toBeVisible() + await expect(page.getByText('bar from server')).not.toBeVisible() + await expect(page.getByText('baz from client')).toBeVisible() + + await expect(requests.requests.length).toBe(0) + + const historyLength = await page.evaluate(() => window.history.length) + + await expect(historyLength).toBe(3) +})