Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parent context with name and prop values to the callback context of route props #433

Merged
merged 8 commits into from
Jan 22, 2025
56 changes: 56 additions & 0 deletions src/services/createPropStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CallbackPushResponse, CallbackRejectResponse, CallbackSuccessResponse,
import { CallbackContextPushError } from '@/errors/callbackContextPushError'
import { CallbackContextRejectionError } from '@/errors/callbackContextRejectionError'
import { getPropsValue } from '@/utilities/props'
import { PropsCallbackParent } from '@/types/props'

export const propStoreKey: InjectionKey<PropStore> = Symbol()

Expand Down Expand Up @@ -39,6 +40,7 @@ export function createPropStore(): PropStore {
push,
replace,
reject,
parent: getParentContext(route, true),
}))

response[key] = value
Expand Down Expand Up @@ -72,6 +74,7 @@ export function createPropStore(): PropStore {
push,
replace,
reject,
parent: getParentContext(route),
}))

store.set(key, value)
Expand Down Expand Up @@ -111,6 +114,59 @@ export function createPropStore(): PropStore {
return store.get(key)
}

function getParentContext(route: ResolvedRoute, prefetch: boolean = false): PropsCallbackParent {
const parent = route.matches.at(-2)
pleek91 marked this conversation as resolved.
Show resolved Hide resolved

if (!parent) {
return
}

if (isWithComponentProps(parent)) {
return {
name: parent.name ?? '',
get props() {
return getParentProps(parent, 'default', route, prefetch)
},
pleek91 marked this conversation as resolved.
Show resolved Hide resolved
}
}

if (isWithComponentPropsRecord(parent)) {
return {
name: parent.name ?? '',
props: new Proxy({}, {
get(target, name) {
if (typeof name !== 'string') {
return Reflect.get(target, name)
}

return getParentProps(parent, name, route, prefetch)
},
}),
}
}

return {
name: parent.name ?? '',
props: undefined,
}
}

function getParentProps(parent: Route['matched'], name: string, route: ResolvedRoute, prefetch: boolean = false): unknown {
const value = getProps(parent.id, name, route)

if (prefetch && !value) {
const parentName = parent.name ?? 'unknown'
pleek91 marked this conversation as resolved.
Show resolved Hide resolved
const routeName = route.name || 'unknown'

console.warn(`
Unable to access parent props "${name}" from route "${parentName}" while prefetching props for route "${routeName}".
This may occur if the parent route's props were not also prefetched.
`)
}

return value
}

function getPropKey(id: string, name: string, route: ResolvedRoute): string {
return [id, name, route.id, JSON.stringify(route.params)].join('-')
}
Expand Down
102 changes: 102 additions & 0 deletions src/services/createRoute.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,3 +406,105 @@ test('undefined is not a valid value for 2nd argument of createRoute', () => {

expectTypeOf<Source>().toEqualTypeOf<Expect>()
})

test('parent props are undefined when parent has no props', () => {
const parent = createRoute({
name: 'parent',
})

createRoute({
name: 'child',
parent: parent,
}, (__, { parent }) => {
expectTypeOf(parent.props).toEqualTypeOf<undefined>()
expectTypeOf(parent.name).toEqualTypeOf<'parent'>()

return {}
})
})

test('sync parent props are passed to child props', () => {
const parent = createRoute({
name: 'parent',
}, () => ({ foo: 123 }))

createRoute({
name: 'child',
parent: parent,
}, (__, { parent }) => {
expectTypeOf(parent.props).toEqualTypeOf<{ foo: number }>()
expectTypeOf(parent.name).toEqualTypeOf<'parent'>()

return {}
})
})

test('async parent props are passed to child props', () => {
const parent = createRoute({
name: 'parent',
}, async () => ({ foo: 123 }))

createRoute({
name: 'child',
parent: parent,
}, (__, { parent }) => {
expectTypeOf(parent.props).toEqualTypeOf<Promise<{ foo: number }>>()
expectTypeOf(parent.name).toEqualTypeOf<'parent'>()

return {}
})
})

test('parent props are passed to child props when multiple parent components are used', () => {
const parent = createRoute({
name: 'parent',
components: {
one: component,
two: component,
},
}, {
one: () => ({ foo: 123 }),
two: async () => ({ foo: 456 }),
})

createRoute({
name: 'child',
parent: parent,
}, (__, { parent }) => {
expectTypeOf(parent.props).toEqualTypeOf<{
one: { foo: number },
two: Promise<{ foo: number }>,
}>()
expectTypeOf(parent.name).toEqualTypeOf<'parent'>()

return {}
})
})

test('parent props are passed to child props when multiple child components are used', () => {
const parent = createRoute({
name: 'parent',
}, async () => ({ foo: 123 }))

createRoute({
name: 'child',
parent: parent,
components: {
one: component,
two: component,
},
}, {
one: (__, { parent }) => {
expectTypeOf(parent.props).toEqualTypeOf<Promise<{ foo: number }>>()
expectTypeOf(parent.name).toEqualTypeOf<'parent'>()

return {}
},
two: (__, { parent }) => {
expectTypeOf(parent.props).toEqualTypeOf<Promise<{ foo: number }>>()
expectTypeOf(parent.name).toEqualTypeOf<'parent'>()

return {}
},
})
})
157 changes: 156 additions & 1 deletion src/services/createRoute.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { expect, test } from 'vitest'
import { expect, test, vi } from 'vitest'
import { createRoute } from '@/services/createRoute'
import { path } from '@/services/path'
import { query } from '@/services/query'
import { createRouter } from '@/main'
import { component } from '@/utilities/testHelpers'

test('given parent, path is combined', () => {
const parent = createRoute({
Expand Down Expand Up @@ -110,3 +112,156 @@ test('given parent and child without meta, meta matches parent', () => {
foo: 123,
})
})

test('parent context is passed to child props', async () => {
const spy = vi.fn()
const parent = createRoute({
name: 'parent',
})

const child = createRoute({
name: 'child',
parent: parent,
path: '/child',
}, (_, { parent }) => {
return spy(parent)
})

const router = createRouter([parent, child], {
initialUrl: '/child',
})

await router.start()

expect(spy).toHaveBeenCalledWith({ name: 'parent', props: undefined })
})

test('sync parent props are passed to child props', async () => {
const spy = vi.fn()

const parent = createRoute({
name: 'parent',
}, () => ({ foo: 123 }))

const child = createRoute({
name: 'child',
parent: parent,
path: '/child',
}, (__, { parent }) => {
return spy({ value: parent.props.foo })
})

const router = createRouter([parent, child], {
initialUrl: '/child',
})

await router.start()

expect(spy).toHaveBeenCalledWith({ value: 123 })
})

test('async parent props are passed to child props', async () => {
const spy = vi.fn()

const parent = createRoute({
name: 'parent',
}, async () => ({ foo: 123 }))

const child = createRoute({
name: 'child',
parent: parent,
path: '/child',
}, async (__, { parent }) => {
expect(parent.props).toBeDefined()
expect(parent.props).toBeInstanceOf(Promise)

const { foo: value } = await parent.props

return spy({ value })
})

const router = createRouter([parent, child], {
initialUrl: '/child',
})

await router.start()

expect(spy).toHaveBeenCalledWith({ value: 123 })
})

test('sync parent props with multiple views are passed to child props', async () => {
const spy = vi.fn()

const parent = createRoute({
name: 'parent',
components: {
one: component,
two: component,
three: component,
},
}, {
one: () => ({ foo: 123 }),
two: () => ({ bar: 456 }),
})

const child = createRoute({
name: 'child',
parent: parent,
path: '/child',
}, (__, { parent }) => {
return spy({
value1: parent.props.one.foo,
value2: parent.props.two.bar,
})
})

const router = createRouter([parent, child], {
initialUrl: '/child',
})

await router.start()

expect(spy).toHaveBeenCalledWith({ value1: 123, value2: 456 })
})

test('async parent props with multiple views are passed to child props', async () => {
const spy = vi.fn()

const parent = createRoute({
name: 'parent',
components: {
one: component,
two: component,
three: component,
},
}, {
one: async () => ({ foo: 123 }),
two: async () => ({ bar: 456 }),
})

const child = createRoute({
name: 'child',
parent: parent,
path: '/child',
}, async (__, { parent }) => {
expect(parent.props).toBeDefined()
expect(parent.props.one).toBeInstanceOf(Promise)
expect(parent.props.two).toBeInstanceOf(Promise)

const { foo: value1 } = await parent.props.one
const { bar: value2 } = await parent.props.two

return spy({
value1,
value2,
})
})

const router = createRouter([parent, child], {
initialUrl: '/child',
})

await router.start()

expect(spy).toHaveBeenCalledWith({ value1: 123, value2: 456 })
})
2 changes: 1 addition & 1 deletion src/types/createRouteOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export type CreateRouteOptions<
export type PropsGetter<
TOptions extends CreateRouteOptions = CreateRouteOptions,
TComponent extends Component = Component
> = (route: ResolvedRoute<ToRoute<TOptions, undefined>>, context: PropsCallbackContext) => MaybePromise<ComponentProps<TComponent>>
> = (route: ResolvedRoute<ToRoute<TOptions, undefined>>, context: PropsCallbackContext<TOptions['parent']>) => MaybePromise<ComponentProps<TComponent>>

type ComponentPropsAreOptional<
TComponent extends Component
Expand Down
Loading
Loading