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
6 changes: 6 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare module 'axios' {

export type DefaultInertiaConfig = {
errorValueType: string
sharedPageProps: PageProps
}
/**
* Designed to allow overriding of some core types using TypeScript
Expand All @@ -20,6 +21,10 @@ export type DefaultInertiaConfig = {
* declare module '@inertiajs/core' {
* export interface InertiaConfig {
* errorValueType: string[]
* sharedPageProps: {
* auth: { user: User | null }
* flash: { success?: string; error?: string }
* }
* }
* }
* ```
Expand All @@ -29,6 +34,7 @@ export type InertiaConfigFor<Key extends keyof DefaultInertiaConfig> = Key exten
? InertiaConfig[Key]
: DefaultInertiaConfig[Key]
export type ErrorValue = InertiaConfigFor<'errorValueType'>
export type SharedPageProps = InertiaConfigFor<'sharedPageProps'>

export type Errors = Record<string, ErrorValue>
export type ErrorBag = Record<string, Errors>
Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/usePage.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Page, PageProps } from '@inertiajs/core'
import { Page, PageProps, SharedPageProps } from '@inertiajs/core'
import { useContext } from 'react'
import PageContext from './PageContext'

export default function usePage<TPageProps extends PageProps = PageProps>(): Page<TPageProps> {
export default function usePage<TPageProps extends PageProps = PageProps>(): Page<TPageProps & SharedPageProps> {
const page = useContext(PageContext)

if (!page) {
throw new Error('usePage must be used within the Inertia component')
}

return page
return page as Page<TPageProps & SharedPageProps>
}
35 changes: 35 additions & 0 deletions packages/react/test-app/Pages/TypeScriptProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// This component is used for checking the TypeScript implementation; there is no Playwright test depending on it.
import { usePage } from '@inertiajs/react'

declare module '@inertiajs/core' {
export interface InertiaConfig {
sharedPageProps: {
flash: { success?: string; error?: string }
}
}
}

type PageProps = {
posts: { id: number; title: string }[]
}

export default function TypeScriptProps() {
const page = usePage<PageProps>()

const error = page.props.flash.error
const postTitles = page.props.posts.map((post) => post.title)

// @ts-expect-error - 'message' does not exist on flash
const flashMessage = page.props.flash.message
// @ts-expect-error - 'users' does not exist on page props
const userNames = page.props.users.map((user) => user.name)

console.log({
error,
postTitles,
flashMessage,
userNames,
})

return null
}
2 changes: 1 addition & 1 deletion packages/svelte/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export { default as Link } from './components/Link.svelte'
export { default as WhenVisible } from './components/WhenVisible.svelte'
export { default as createInertiaApp } from './createInertiaApp'
export { default as inertia } from './link'
export { default as page } from './page'
export { default as page, usePage } from './page'
export { type ResolvedComponent } from './types'
export { default as useForm, type InertiaForm, type InertiaFormProps } from './useForm'
export { default as usePoll } from './usePoll'
Expand Down
12 changes: 8 additions & 4 deletions packages/svelte/src/page.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { type Page } from '@inertiajs/core'
import { writable } from 'svelte/store'
import { type Page, type PageProps, type SharedPageProps } from '@inertiajs/core'
import { derived, writable, type Readable } from 'svelte/store'

type SveltePage = Omit<Page, 'props'> & {
props: Page['props'] & {
type SveltePage<TPageProps extends PageProps = PageProps> = Omit<Page<TPageProps & SharedPageProps>, 'props'> & {
props: Page<TPageProps & SharedPageProps>['props'] & {
[key: string]: any
}
}
Expand All @@ -12,3 +12,7 @@ const { set, subscribe } = writable<SveltePage>()
export const setPage = set

export default { subscribe }

export function usePage<TPageProps extends PageProps = PageProps>(): Readable<SveltePage<TPageProps>> {
return derived({ subscribe }, ($page) => $page as SveltePage<TPageProps>)
}
24 changes: 15 additions & 9 deletions packages/svelte/test-app/Pages/Svelte/PropsAndPageStore.svelte
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
<script lang="ts">
import { inertia, page, useForm } from '@inertiajs/svelte'
import { inertia, page, usePage, useForm } from '@inertiajs/svelte'
import { onMount } from 'svelte'

type PageProps = {
foo: string
}

export let foo

const form = useForm({ foo })

const pageProps: PageProps = {
foo: $page.props.foo,
}

const sveltePage = usePage<PageProps>()

console.log('[script] foo prop is', foo)
console.log('[script] $page.props.foo is', $page.props.foo)
console.log('[script] $sveltePage.props.foo is', $sveltePage.props.foo)

$: console.log('[reactive expression] foo prop is', foo)
$: console.log('[reactive expression] $page.props.foo is', $page.props.foo)
$: console.log('[reactive expression] $sveltePage.props.foo is', $sveltePage.props.foo)

onMount(() => {
console.log('[onMount] foo prop is', foo)
console.log('[onMount] $page.props.foo is', $page.props.foo)
console.log('[onMount] $sveltePage.props.foo is', $sveltePage.props.foo)
})

type PageProps = {
foo: string
}

const pageProps: PageProps = {
foo: $page.props.foo,
}
</script>

<div>
<input id="input" type="text" bind:value={$form.foo} />
<p>foo prop is {foo}</p>
<p>$page.props.foo is {$page.props.foo}</p>
<p>pageProps.foo is {pageProps.foo}</p>
<p>$sveltePage.props.foo is {$sveltePage.props.foo}</p>

<a href="/svelte/props-and-page-store" use:inertia={{ data: { foo: 'bar' } }}> Bar </a>
<a href="/svelte/props-and-page-store" use:inertia={{ data: { foo: 'baz' } }}> Baz </a>
Expand Down
35 changes: 35 additions & 0 deletions packages/svelte/test-app/Pages/TypeScriptProps.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts" context="module">
declare module '@inertiajs/core' {
export interface InertiaConfig {
sharedPageProps: {
flash: { success?: string; error?: string }
}
}
}
</script>

<script lang="ts">
// This component is used for checking the TypeScript implementation; there is no Playwright test depending on it.
import { usePage } from '@inertiajs/svelte'

type PageProps = {
posts: { id: number; title: string }[]
}

const page = usePage<PageProps>()

$: error = $page.props.flash.error
$: postTitles = $page.props.posts.map((post) => post.title)

// @ts-expect-error - 'message' does not exist on flash
$: flashMessage = $page.props.flash.message
// @ts-expect-error - 'users' does not exist on page props
$: userNames = $page.props.users.map((user) => user.name)

console.log({
error,
postTitles,
flashMessage,
userNames,
})
</script>
5 changes: 3 additions & 2 deletions packages/vue3/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Page,
PageProps,
router,
SharedPageProps,
} from '@inertiajs/core'
import {
computed,
Expand Down Expand Up @@ -133,7 +134,7 @@ export const plugin: Plugin = {
},
}

export function usePage<SharedProps extends PageProps>(): Page<SharedProps> {
export function usePage<TPageProps extends PageProps = PageProps>(): Page<TPageProps & SharedPageProps> {
return reactive({
props: computed(() => page.value?.props),
url: computed(() => page.value?.url),
Expand All @@ -147,5 +148,5 @@ export function usePage<SharedProps extends PageProps>(): Page<SharedProps> {
matchPropsOn: computed(() => page.value?.matchPropsOn),
rememberedState: computed(() => page.value?.rememberedState),
encryptHistory: computed(() => page.value?.encryptHistory),
})
}) as Page<TPageProps>
}
4 changes: 2 additions & 2 deletions packages/vue3/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createHeadManager, Page, PageHandler, router } from '@inertiajs/core'
import { createHeadManager, Page, PageHandler, PageProps, router, SharedPageProps } from '@inertiajs/core'
import { ComponentPublicInstance } from 'vue'
import useForm from './useForm'

Expand All @@ -15,7 +15,7 @@ declare module '@inertiajs/core' {
declare module 'vue' {
export interface ComponentCustomProperties {
$inertia: typeof router
$page: Page
$page: Page<PageProps & SharedPageProps>
$headManager: ReturnType<typeof createHeadManager>
}

Expand Down
40 changes: 40 additions & 0 deletions packages/vue3/test-app/Pages/TypeScriptProps.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup lang="ts">
// This component is used for checking the TypeScript implementation; there is no Playwright test depending on it.
import { usePage } from '@inertiajs/vue3'

declare module '@inertiajs/core' {
export interface InertiaConfig {
sharedPageProps: {
flash: { success?: string; error?: string }
}
}
}

type PageProps = {
posts: { id: number; title: string }[]
}

const page = usePage<PageProps>()

const error = page.props.flash.error
const postTitles = page.props.posts.map((post) => post.title)

// @ts-expect-error - 'message' does not exist on flash
const flashMessage = page.props.flash.message
// @ts-expect-error - 'users' does not exist on page props
const userNames = page.props.users.map((user) => user.name)

console.log({
error,
postTitles,
flashMessage,
userNames,
})
</script>

<template>
{{ $page.props.flash.error }}

<!-- @vue-expect-error - 'message' does not exist on flash -->
{{ $page.props.flash.message }}
</template>
72 changes: 47 additions & 25 deletions tests/svelte.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,44 +13,61 @@ test('props and page store are in sync', async ({ page }) => {
await expect(page.getByText('foo prop is default')).toBeVisible()
await expect(page.getByText('$page.props.foo is default')).toBeVisible()
await expect(page.getByText('pageProps.foo is default')).toBeVisible()
await expect(consoleMessages.messages).toHaveLength(7)
await expect(page.getByText('$sveltePage.props.foo is default')).toBeVisible()
await expect(consoleMessages.messages).toHaveLength(11)
await expect(consoleMessages.messages[0]).toBe('[script] foo prop is default')
await expect(consoleMessages.messages[1]).toBe('[script] $page.props.foo is default')
await expect(consoleMessages.messages[2]).toBe('[reactive expression] foo prop is default')
await expect(consoleMessages.messages[3]).toBe('[reactive expression] $page.props.foo is default')
await expect(consoleMessages.messages[4]).toBe('[onMount] foo prop is default')
await expect(consoleMessages.messages[5]).toBe('[onMount] $page.props.foo is default')
await expect(consoleMessages.messages[6]).toBe('[reactive expression] $page.props.foo is default')
await expect(consoleMessages.messages[2]).toBe('[script] $sveltePage.props.foo is default')
await expect(consoleMessages.messages[3]).toBe('[reactive expression] foo prop is default')
await expect(consoleMessages.messages[4]).toBe('[reactive expression] $page.props.foo is default')
await expect(consoleMessages.messages[5]).toBe('[reactive expression] $sveltePage.props.foo is default')
await expect(consoleMessages.messages[6]).toBe('[onMount] foo prop is default')
await expect(consoleMessages.messages[7]).toBe('[onMount] $page.props.foo is default')
await expect(consoleMessages.messages[8]).toBe('[onMount] $sveltePage.props.foo is default')
await expect(consoleMessages.messages[9]).toBe('[reactive expression] $page.props.foo is default')
await expect(consoleMessages.messages[10]).toBe('[reactive expression] $sveltePage.props.foo is default')
await expect(await page.locator('#input').inputValue()).toEqual('default')

consoleMessages.messages = []
await page.getByRole('link', { name: 'Bar' }).click()

await expect(page.getByText('foo prop is bar')).toBeVisible()
await expect(page.getByText('$page.props.foo is bar')).toBeVisible()
await expect(consoleMessages.messages).toHaveLength(7)
await expect(page.getByText('pageProps.foo is bar')).toBeVisible()
await expect(page.getByText('$sveltePage.props.foo is bar')).toBeVisible()
await expect(consoleMessages.messages).toHaveLength(11)
await expect(consoleMessages.messages[0]).toBe('[reactive expression] $page.props.foo is bar')
await expect(consoleMessages.messages[1]).toBe('[script] foo prop is bar')
await expect(consoleMessages.messages[2]).toBe('[script] $page.props.foo is bar')
await expect(consoleMessages.messages[3]).toBe('[reactive expression] foo prop is bar')
await expect(consoleMessages.messages[4]).toBe('[reactive expression] $page.props.foo is bar')
await expect(consoleMessages.messages[5]).toBe('[onMount] foo prop is bar')
await expect(consoleMessages.messages[6]).toBe('[onMount] $page.props.foo is bar')
await expect(consoleMessages.messages[1]).toBe('[reactive expression] $sveltePage.props.foo is bar')
await expect(consoleMessages.messages[2]).toBe('[script] foo prop is bar')
await expect(consoleMessages.messages[3]).toBe('[script] $page.props.foo is bar')
await expect(consoleMessages.messages[4]).toBe('[script] $sveltePage.props.foo is bar')
await expect(consoleMessages.messages[5]).toBe('[reactive expression] foo prop is bar')
await expect(consoleMessages.messages[6]).toBe('[reactive expression] $page.props.foo is bar')
await expect(consoleMessages.messages[7]).toBe('[reactive expression] $sveltePage.props.foo is bar')
await expect(consoleMessages.messages[8]).toBe('[onMount] foo prop is bar')
await expect(consoleMessages.messages[9]).toBe('[onMount] $page.props.foo is bar')
await expect(consoleMessages.messages[10]).toBe('[onMount] $sveltePage.props.foo is bar')
await expect(await page.locator('#input').inputValue()).toEqual('bar')

consoleMessages.messages = []
await page.getByRole('link', { name: 'Baz' }).click()

await expect(page.getByText('foo prop is baz')).toBeVisible()
await expect(page.getByText('$page.props.foo is baz')).toBeVisible()
await expect(consoleMessages.messages).toHaveLength(7)
await expect(page.getByText('pageProps.foo is baz')).toBeVisible()
await expect(page.getByText('$sveltePage.props.foo is baz')).toBeVisible()
await expect(consoleMessages.messages).toHaveLength(11)
await expect(consoleMessages.messages[0]).toBe('[reactive expression] $page.props.foo is baz')
await expect(consoleMessages.messages[1]).toBe('[script] foo prop is baz')
await expect(consoleMessages.messages[2]).toBe('[script] $page.props.foo is baz')
await expect(consoleMessages.messages[3]).toBe('[reactive expression] foo prop is baz')
await expect(consoleMessages.messages[4]).toBe('[reactive expression] $page.props.foo is baz')
await expect(consoleMessages.messages[5]).toBe('[onMount] foo prop is baz')
await expect(consoleMessages.messages[6]).toBe('[onMount] $page.props.foo is baz')
await expect(consoleMessages.messages[1]).toBe('[reactive expression] $sveltePage.props.foo is baz')
await expect(consoleMessages.messages[2]).toBe('[script] foo prop is baz')
await expect(consoleMessages.messages[3]).toBe('[script] $page.props.foo is baz')
await expect(consoleMessages.messages[4]).toBe('[script] $sveltePage.props.foo is baz')
await expect(consoleMessages.messages[5]).toBe('[reactive expression] foo prop is baz')
await expect(consoleMessages.messages[6]).toBe('[reactive expression] $page.props.foo is baz')
await expect(consoleMessages.messages[7]).toBe('[reactive expression] $sveltePage.props.foo is baz')
await expect(consoleMessages.messages[8]).toBe('[onMount] foo prop is baz')
await expect(consoleMessages.messages[9]).toBe('[onMount] $page.props.foo is baz')
await expect(consoleMessages.messages[10]).toBe('[onMount] $sveltePage.props.foo is baz')
await expect(await page.locator('#input').inputValue()).toEqual('baz')

await page.getByRole('link', { name: 'Home' }).click()
Expand All @@ -62,11 +79,16 @@ test('props and page store are in sync', async ({ page }) => {

await expect(page.getByText('foo prop is baz')).toBeVisible()
await expect(page.getByText('$page.props.foo is baz')).toBeVisible()
await expect(consoleMessages.messages).toHaveLength(6)
await expect(page.getByText('pageProps.foo is baz')).toBeVisible()
await expect(page.getByText('$sveltePage.props.foo is baz')).toBeVisible()
await expect(consoleMessages.messages).toHaveLength(9)
await expect(consoleMessages.messages[0]).toBe('[script] foo prop is baz')
await expect(consoleMessages.messages[1]).toBe('[script] $page.props.foo is baz')
await expect(consoleMessages.messages[2]).toBe('[reactive expression] foo prop is baz')
await expect(consoleMessages.messages[3]).toBe('[reactive expression] $page.props.foo is baz')
await expect(consoleMessages.messages[4]).toBe('[onMount] foo prop is baz')
await expect(consoleMessages.messages[5]).toBe('[onMount] $page.props.foo is baz')
await expect(consoleMessages.messages[2]).toBe('[script] $sveltePage.props.foo is baz')
await expect(consoleMessages.messages[3]).toBe('[reactive expression] foo prop is baz')
await expect(consoleMessages.messages[4]).toBe('[reactive expression] $page.props.foo is baz')
await expect(consoleMessages.messages[5]).toBe('[reactive expression] $sveltePage.props.foo is baz')
await expect(consoleMessages.messages[6]).toBe('[onMount] foo prop is baz')
await expect(consoleMessages.messages[7]).toBe('[onMount] $page.props.foo is baz')
await expect(consoleMessages.messages[8]).toBe('[onMount] $sveltePage.props.foo is baz')
})