Skip to content
Merged
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
21 changes: 8 additions & 13 deletions packages/core/src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,7 @@ import { eventHandler } from './eventHandler'
import { fireNavigateEvent } from './events'
import { history } from './history'
import { Scroll } from './scroll'
import {
Component,
Page,
PageEvent,
PageHandler,
PageResolver,
PreserveStateOption,
RouterInitParams,
VisitOptions,
} from './types'
import { Component, Page, PageEvent, PageHandler, PageResolver, RouterInitParams } from './types'
import { hrefToUrl, isSameUrlWithoutHash } from './url'

class CurrentPage {
Expand Down Expand Up @@ -41,7 +32,11 @@ class CurrentPage {
replace = false,
preserveScroll = false,
preserveState = false,
}: Partial<Pick<VisitOptions, 'replace' | 'preserveScroll' | 'preserveState'>> = {},
}: {
replace?: boolean
preserveScroll?: boolean
preserveState?: boolean
} = {},
): Promise<void> {
if (Object.keys(page.deferredProps || {}).length) {
this.pendingDeferredProps = {
Expand Down Expand Up @@ -123,7 +118,7 @@ class CurrentPage {
{
preserveState = false,
}: {
preserveState?: PreserveStateOption
preserveState?: boolean
} = {},
) {
return this.resolve(page.component).then((component) => {
Expand Down Expand Up @@ -167,7 +162,7 @@ class CurrentPage {
}: {
component: Component
page: Page
preserveState: PreserveStateOption
preserveState: boolean
}): Promise<unknown> {
return this.swapComponent({ component, page, preserveState })
}
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/requestParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ export class RequestParams {
}

public setPreserveOptions(page: Page) {
this.params.preserveScroll = this.resolvePreserveOption(this.params.preserveScroll, page)
this.params.preserveState = this.resolvePreserveOption(this.params.preserveState, page)
this.params.preserveScroll = RequestParams.resolvePreserveOption(this.params.preserveScroll, page)
this.params.preserveState = RequestParams.resolvePreserveOption(this.params.preserveState, page)
}

public runCallbacks() {
Expand Down Expand Up @@ -169,7 +169,7 @@ export class RequestParams {
this.callbacks.push({ name, args })
}

protected resolvePreserveOption(value: PreserveStateOption, page: Page): boolean {
public static resolvePreserveOption(value: PreserveStateOption, page: Page): boolean {
if (typeof value === 'function') {
return value(page)
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ export class Response {

return currentPage.set(pageResponse, {
replace: this.requestParams.all().replace,
preserveScroll: this.requestParams.all().preserveScroll,
preserveState: this.requestParams.all().preserveState,
preserveScroll: this.requestParams.all().preserveScroll as boolean,
preserveState: this.requestParams.all().preserveState as boolean,
})
}

Expand Down
27 changes: 15 additions & 12 deletions packages/core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { page as currentPage } from './page'
import { polls } from './polls'
import { prefetchedRequests } from './prefetched'
import { Request } from './request'
import { RequestParams } from './requestParams'
import { RequestStream } from './requestStream'
import { Scroll } from './scroll'
import {
Expand Down Expand Up @@ -386,19 +387,21 @@ export class Router {

const { onError, onFinish, onSuccess, ...pageParams } = params

const page = {
...current,
...pageParams,
props: props as Page['props'],
}

const preserveScroll = RequestParams.resolvePreserveOption(params.preserveScroll ?? false, page)
const preserveState = RequestParams.resolvePreserveOption(params.preserveState ?? false, page)

currentPage
.set(
{
...current,
...pageParams,
props: props as Page['props'],
},
{
replace,
preserveScroll: params.preserveScroll,
preserveState: params.preserveState,
},
)
.set(page, {
replace,
preserveScroll,
preserveState,
})
.then(() => {
const errors = currentPage.get().props.errors || {}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export type PageHandler = ({
}: {
component: Component
page: Page
preserveState: PreserveStateOption
preserveState: boolean
}) => Promise<unknown>

export type PreserveStateOption = boolean | 'errors' | ((page: Page) => boolean)
Expand Down
13 changes: 13 additions & 0 deletions packages/react/test-app/Pages/ClientSideVisit/Page1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default ({ foo, bar }: PageProps) => {
const [errors, setErrors] = useState(0)
const [finished, setFinished] = useState(0)
const [success, setSuccess] = useState(0)
const [random, setRandom] = useState(Math.random())

const bagErrors = () => {
router.replace({
Expand Down Expand Up @@ -46,6 +47,13 @@ export default ({ foo, bar }: PageProps) => {
})
}

const replaceAndPreserveStateWithErrors = (errors = {}) => {
router.replace({
preserveState: 'errors',
props: (props: PageProps) => ({ ...props, errors }),
})
}

const push = () => {
router.push({
url: '/client-side-visit-2',
Expand All @@ -59,12 +67,17 @@ export default ({ foo, bar }: PageProps) => {
<div>{foo}</div>
<div>{bar}</div>
<button onClick={replace}>Replace</button>
<button onClick={() => replaceAndPreserveStateWithErrors({ name: 'Field is required' })}>
Replace with errors
</button>
<button onClick={() => replaceAndPreserveStateWithErrors()}>Replace without errors</button>
<button onClick={push}>Push</button>
<button onClick={defaultErrors}>Errors (default)</button>
<button onClick={bagErrors}>Errors (bag)</button>
<div>Errors: {errors}</div>
<div>Finished: {finished}</div>
<div>Success: {success}</div>
<div id="random">Random: {random}</div>
</div>
)
}
13 changes: 13 additions & 0 deletions packages/svelte/test-app/Pages/ClientSideVisit/Page1.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
let errors = 0
let finished = 0
let success = 0
let random = Math.random()

const bagErrors = () => {
router.replace({
Expand Down Expand Up @@ -48,6 +49,13 @@
})
}

const replaceAndPreserveStateWithErrors = (errors = {}) => {
router.replace({
preserveState: 'errors',
props: (props: PageProps) => ({ ...props, errors }),
})
}

const push = () => {
router.push({
url: '/client-side-visit-2',
Expand All @@ -61,10 +69,15 @@
<div>{foo}</div>
<div>{bar}</div>
<button on:click={replace}>Replace</button>
<button on:click={() => replaceAndPreserveStateWithErrors({ name: 'Field is required' })}>
Replace with errors
</button>
<button on:click={() => replaceAndPreserveStateWithErrors()}>Replace without errors</button>
<button on:click={push}>Push</button>
<button on:click={defaultErrors}>Errors (default)</button>
<button on:click={bagErrors}>Errors (bag)</button>
<div>Errors: {errors}</div>
<div>Finished: {finished}</div>
<div>Success: {success}</div>
<div id="random">Random: {random}</div>
</div>
11 changes: 11 additions & 0 deletions packages/vue3/test-app/Pages/ClientSideVisit/Page1.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const props = defineProps<PageProps>()
const errors = ref(0)
const finished = ref(0)
const success = ref(0)
const random = ref(Math.random())

const bagErrors = () => {
router.replace({
Expand Down Expand Up @@ -56,6 +57,13 @@ const replace = () => {
})
}

const replaceAndPreserveStateWithErrors = (errors = {}) => {
router.replace({
preserveState: 'errors',
props: (props: PageProps) => ({ ...props, errors }),
})
}

const push = () => {
router.push({
url: '/client-side-visit-2',
Expand All @@ -71,10 +79,13 @@ const push = () => {
<div>{{ foo }}</div>
<div>{{ bar }}</div>
<button @click="replace">Replace</button>
<button @click="() => replaceAndPreserveStateWithErrors({ name: 'Field is required' })">Replace with errors</button>
<button @click="() => replaceAndPreserveStateWithErrors()">Replace without errors</button>
<button @click="push">Push</button>
<button @click="defaultErrors">Errors (default)</button>
<button @click="bagErrors">Errors (bag)</button>
<div>Errors: {{ errors }}</div>
<div>Finished: {{ finished }}</div>
<div>Success: {{ success }}</div>
<div id="random">Random: {{ random }}</div>
</template>
15 changes: 14 additions & 1 deletion tests/client-side-visits.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ test('replaces the page client side', async ({ page }) => {
await expect(page.getByText('Finished: 0')).toBeVisible()
await expect(page.getByText('Success: 0')).toBeVisible()

await page.getByRole('button', { name: 'Replace' }).click()
await page.getByRole('button', { name: 'Replace', exact: true }).click()

await expect(page).toHaveURL('/client-side-visit')
await expect(page.getByText('foo from server')).not.toBeVisible()
Expand All @@ -30,6 +30,19 @@ test('replaces the page client side', async ({ page }) => {
await expect(historyLength).toBe(2)
})

test('preserves the state based on the errors object', async ({ page }) => {
await page.goto('/client-side-visit')
const randomValue = await page.locator('#random').innerText()

await page.getByRole('button', { name: 'Replace with errors' }).click()
const randomValueAfter = await page.locator('#random').innerText()
await expect(randomValueAfter).toBe(randomValue)

await page.getByRole('button', { name: 'Replace without errors' }).click()
const randomValueAfterSecond = await page.locator('#random').innerText()
await expect(randomValueAfterSecond).not.toBe(randomValue)
})

test('fires an onError callback when the props has errors', async ({ page }) => {
pageLoads.watch(page)

Expand Down