Skip to content

Commit

Permalink
Merge pull request #433 from kitbagjs/prop-context-parent
Browse files Browse the repository at this point in the history
Add `parent` context with name and prop values to the callback context of route props
  • Loading branch information
pleek91 authored Jan 22, 2025
2 parents 080adee + 0ceb710 commit e2e793d
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 3 deletions.
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)

if (!parent) {
return
}

if (isWithComponentProps(parent)) {
return {
name: parent.name ?? '',
get props() {
return getParentProps(parent, 'default', route, prefetch)
},
}
}

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'
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

0 comments on commit e2e793d

Please sign in to comment.