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
4 changes: 2 additions & 2 deletions src/components/routerView.browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ test('Renders the rejection component when the rejection is not registered on th
const myRejection = createRejection({
type: 'myRejection',
component: {
template: rejectionText
template: rejectionText,
},
})

Expand Down Expand Up @@ -678,4 +678,4 @@ test('Renders the rejection component when the rejection is not registered on th
await flushPromises()

expect(wrapper.text()).toBe(rejectionText)
})
})
6 changes: 3 additions & 3 deletions src/components/routerView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createUseRejection } from '@/compositions/useRejection'
import { createUseRoute } from '@/compositions/useRoute'
import { createUseRouter } from '@/compositions/useRouter'
import { createUseRouterDepth } from '@/compositions/useRouterDepth'
import { RouterRejection } from '@/types/rejection'
import { isRejection, RouterRejection } from '@/types/rejection'
import { RouterRoute } from '@/types/routerRoute'
import { Router } from '@/types/router'
import { Component, computed, defineComponent, EmitsOptions, h, InjectionKey, onServerPrefetch, SetupContext, SlotsType, UnwrapRef, VNode } from 'vue'
Expand Down Expand Up @@ -46,8 +46,8 @@ export function createRouterView<TRouter extends Router>(routerKey: InjectionKey
return null
}

if (rejection.value) {
return rejection.value.component
if (isRejection(rejection.value) && !!rejection.value.route.matched.component) {
return rejection.value.route.matched.component
}

const match = route.matches.at(depth)
Expand Down
16 changes: 14 additions & 2 deletions src/services/createCurrentRejection.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Rejection, RouterRejection } from '@/types/rejection'
import { ref } from 'vue'
import { isRejection, Rejection, RouterRejection } from '@/types/rejection'
import { ref, ComputedRef, computed } from 'vue'
import { ResolvedRoute } from '@/types/resolved'
import { createResolvedRoute } from './createResolvedRoute'

type RejectionUpdate = (rejection: Rejection) => void
type RejectionClear = () => void

type CurrentRejectionContext = {
currentRejection: RouterRejection,
currentRejectionRoute: ComputedRef<ResolvedRoute | null>,
updateRejection: RejectionUpdate,
clearRejection: RejectionClear,
}
Expand All @@ -21,8 +24,17 @@ export function createCurrentRejection(): CurrentRejectionContext {

const currentRejection: RouterRejection = ref<Rejection | null>(null)

const currentRejectionRoute = computed(() => {
if (isRejection(currentRejection.value)) {
return createResolvedRoute(currentRejection.value.route)
}

return null
})

return {
currentRejection,
currentRejectionRoute,
updateRejection,
clearRejection,
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/createExternalRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { combineUrl } from '@/services/combineUrl'
import { ExternalRouteHooks } from '@/types/hooks'
import { ExtractRouteContext } from '@/types/routeContext'
import { RouteRedirects } from '@/types/redirects'
import { createRouteTitle, RouteSetTitle } from '@/types/titles'
import { createRouteTitle, RouteSetTitle } from '@/types/routeTitle'

export function createExternalRoute<
const TOptions extends CreateRouteOptions & WithHost & WithoutParent
Expand Down
49 changes: 14 additions & 35 deletions src/services/createRejection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,36 @@ import { genericRejection } from '@/components/rejection'
import { RejectionHooks } from '@/types/hooks'
import { IS_REJECTION_SYMBOL, Rejection, RejectionInternal } from '@/types/rejection'
import { Component, markRaw } from 'vue'
import { ResolvedRoute } from '@/types/resolved'
import { createRouteId } from './createRouteId'
import { createResolvedRouteQuery } from './createResolvedRouteQuery'
import { createRoute } from '@/services/createRoute'
import { RouteSetTitle } from '@/types/routeTitle'

export function createRejection<TType extends string>(options: {
type: TType,
component?: Component,
}): Rejection<TType> & RejectionHooks<TType>

export function createRejection(options: { type: string, component?: Component }): Rejection & RejectionHooks {
const component = markRaw(options.component ?? genericRejection(options.type))
const route = getRejectionRoute(options.type, component)
}): Rejection<TType> & RejectionHooks<TType> & RouteSetTitle

export function createRejection({ type, component }: { type: string, component?: Component }): Rejection {
const { store, ...hooks } = createRejectionHooks()

const route = createRoute({
name: type,
component: markRaw(component ?? genericRejection(type)),
})

const { setTitle } = route

const internal = {
[IS_REJECTION_SYMBOL]: true,
route,
hooks: [store],
} satisfies RejectionInternal

const rejection = {
type: options.type,
component,
type,
setTitle,
...hooks,
...internal,
} satisfies Rejection & RejectionInternal & RejectionHooks
} satisfies Rejection & RejectionInternal & RejectionHooks & RouteSetTitle

return rejection
}

function getRejectionRoute(type: string, component: Component): ResolvedRoute {
const route = {
id: createRouteId(),
component,
meta: {},
state: {},
}

const resolved = {
id: route.id,
matched: route,
matches: [route],
name: type,
query: createResolvedRouteQuery(''),
params: {},
state: {},
href: '/',
hash: '',
title: Promise.resolve(undefined),
} satisfies ResolvedRoute

return resolved
}
2 changes: 1 addition & 1 deletion src/services/createRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { combineUrl } from '@/services/combineUrl'
import { InternalRouteHooks } from '@/types/hooks'
import { ExtractRouteContext } from '@/types/routeContext'
import { RouteRedirects } from '@/types/redirects'
import { createRouteTitle, RouteSetTitle } from '@/types/titles'
import { createRouteTitle, RouteSetTitle } from '@/types/routeTitle'

type CreateRouteWithProps<
TOptions extends CreateRouteOptions,
Expand Down
11 changes: 7 additions & 4 deletions src/services/createRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function createRouter<
const shouldRemoveTrailingSlashes = options?.removeTrailingSlashes ?? true
const { routes, getRouteByName, getRejectionByType } = getRoutesForRouter(routesOrArrayOfRoutes, plugins, options)
const notFoundRejection = getRejectionByType('NotFound')
const notFoundRoute = createResolvedRoute(notFoundRejection.route)

const hooks = createRouterHooks()

Expand Down Expand Up @@ -130,7 +131,7 @@ export function createRouter<

history.stopListening()

const to = find(url, options) ?? notFoundRejection.route
const to = find(url, options) ?? notFoundRoute

const from = getFromRouteForHooks(navigationId)

Expand Down Expand Up @@ -186,7 +187,7 @@ export function createRouter<
throw new Error(`Switch is not exhaustive for after hook response status: ${JSON.stringify(exhaustive)}`)
}

setDocumentTitle(to)
setDocumentTitle(currentRejectionRoute.value ?? to)

history.startListening()
}
Expand Down Expand Up @@ -318,15 +319,17 @@ export function createRouter<
}

hooks.runRejectionHooks(rejection, { to, from })

updateRejection(rejection)
}

const reject: RouterReject<TOptions['rejections'] | TPlugin['rejections']> = (type) => {
setRejection(type)
setDocumentTitle(currentRejectionRoute.value)
}

const { currentRejection, updateRejection, clearRejection } = createCurrentRejection()
const { currentRoute, routerRoute, updateRoute } = createCurrentRoute<TRoutes | TPlugin['routes']>(routerKey, notFoundRejection.route, push)
const { currentRejection, currentRejectionRoute, updateRejection, clearRejection } = createCurrentRejection()
const { currentRoute, routerRoute, updateRoute } = createCurrentRoute<TRoutes | TPlugin['routes']>(routerKey, notFoundRoute, push)

const initialUrl = getInitialUrl(options?.initialUrl)
const initialState = history.location.state
Expand Down
10 changes: 3 additions & 7 deletions src/types/rejection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, Ref } from 'vue'
import { ResolvedRoute } from '@/types/resolved'
import { Ref } from 'vue'
import { Route } from '@/types/route'
import { Router } from '@/types/router'
import { RouterReject } from '@/types/routerReject'
import { Hooks } from '@/models/hooks'
Expand All @@ -18,7 +18,7 @@ export function isRejection(value: unknown): value is Rejection & RejectionInter

export type RejectionInternal = {
[IS_REJECTION_SYMBOL]: true,
route: ResolvedRoute,
route: Route,
hooks: Hooks[],
}

Expand All @@ -32,10 +32,6 @@ export type Rejection<TType extends string = string> = {
* The type of rejection.
*/
type: TType,
/**
* The component to render when the rejection occurs.
*/
component: Component,
}

export type RejectionType<TRejections extends Rejections | undefined> =
Expand Down
4 changes: 2 additions & 2 deletions src/types/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { LastInArray } from '@/types/utilities'
import { CreateRouteOptions } from '@/types/createRouteOptions'
import { RouteContext } from '@/types/routeContext'
import { Url } from '@/types/url'
import { GetTitle } from '@/types/titles'
import { GetRouteTitle } from '@/types/routeTitle'
import { Hooks } from '@/models/hooks'
import { RouteRedirect } from './redirects'

Expand All @@ -20,7 +20,7 @@ export type RouteInternal = {
depth: number,
hooks: Hooks[],
redirect: RouteRedirect,
getTitle: GetTitle,
getTitle: GetRouteTitle,
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test, vi } from 'vitest'
import { createRoute } from '@/services/createRoute'
import { component } from '@/utilities/testHelpers'
import { createRouter } from '@/main'
import { createRouter } from '@/services/createRouter'
import { flushPromises } from '@vue/test-utils'

test('route with title updates document title', async () => {
Expand All @@ -20,7 +20,7 @@ test('route with title updates document title', async () => {
initialUrl: '/',
})

router.start()
await router.start()

await flushPromises()

Expand Down Expand Up @@ -51,7 +51,7 @@ test('route with title and parent with title does not call parent getTitle', asy
initialUrl: '/parent/child',
})

router.start()
await router.start()

await flushPromises()

Expand Down Expand Up @@ -88,7 +88,7 @@ test('route with title and parent with title does call parent getTitle when call
initialUrl: '/parent/child',
})

router.start()
await router.start()

await flushPromises()

Expand Down Expand Up @@ -132,7 +132,7 @@ test('route with title and parent with title does call parent getTitle when call
initialUrl: '/parent/child/grandchild',
})

router.start()
await router.start()

await flushPromises()

Expand Down Expand Up @@ -161,7 +161,7 @@ test('route without title and parent with title updates document title', async (
initialUrl: '/parent/child',
})

router.start()
await router.start()

await flushPromises()

Expand Down
22 changes: 11 additions & 11 deletions src/types/titles.ts → src/types/routeTitle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,35 @@ import { ResolvedRoute, ResolvedRouteUnion } from '@/types/resolved'
import { isRoute, Route } from './route'
import { MaybePromise } from './utilities'

export type SetTitleContext = {
export type SetRouteTitleContext = {
from: ResolvedRoute,
getParentTitle: () => Promise<string | undefined>,
}

export type SetTitleCallback<TRoute extends Route = Route> = (to: ResolvedRouteUnion<TRoute>, context: SetTitleContext) => MaybePromise<string>
export type GetTitle<TRoute extends Route = Route> = (to: ResolvedRouteUnion<TRoute>) => Promise<string | undefined>
export type SetTitle<TRoute extends Route = Route> = (callback: SetTitleCallback<TRoute>) => void
export type SetRouteTitleCallback<TRoute extends Route = Route> = (to: ResolvedRouteUnion<TRoute>, context: SetRouteTitleContext) => MaybePromise<string>
export type GetRouteTitle<TRoute extends Route = Route> = (to: ResolvedRouteUnion<TRoute>) => Promise<string | undefined>
export type SetRouteTitle<TRoute extends Route = Route> = (callback: SetRouteTitleCallback<TRoute>) => void

export type RouteSetTitle<TRoute extends Route = Route> = {
/**
* Adds a callback to set the document title for the route.
*/
setTitle: SetTitle<TRoute>,
setTitle: SetRouteTitle<TRoute>,
}

type CreateRouteTitle = {
setTitle: SetTitle,
getTitle: GetTitle,
setTitle: SetRouteTitle,
getTitle: GetRouteTitle,
}

export function createRouteTitle(parent: Route | undefined): CreateRouteTitle {
let setTitleCallback: SetTitleCallback | undefined
export function createRouteTitle(parent?: Route): CreateRouteTitle {
let setTitleCallback: SetRouteTitleCallback | undefined

const setTitle: SetTitle = (callback) => {
const setTitle: SetRouteTitle = (callback) => {
setTitleCallback = callback
}

const getTitle: GetTitle = async (to) => {
const getTitle: GetRouteTitle = async (to) => {
const getParentTitle = async (): Promise<string | undefined> => {
if (parent && isRoute(parent)) {
return parent.getTitle(to)
Expand Down
2 changes: 1 addition & 1 deletion src/utilities/setDocumentTitle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { isBrowser } from '@/utilities/isBrowser'

let defaultTitle: string | undefined

export function setDocumentTitle(to: ResolvedRoute): void {
export function setDocumentTitle(to: ResolvedRoute | null): void {
if (!isRoute(to) || !isBrowser()) {
return
}
Expand Down
Loading