diff --git a/src/components/routerView.browser.spec.ts b/src/components/routerView.browser.spec.ts index 4d587547..7796e6e0 100644 --- a/src/components/routerView.browser.spec.ts +++ b/src/components/routerView.browser.spec.ts @@ -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, }, }) @@ -678,4 +678,4 @@ test('Renders the rejection component when the rejection is not registered on th await flushPromises() expect(wrapper.text()).toBe(rejectionText) -}) \ No newline at end of file +}) diff --git a/src/components/routerView.ts b/src/components/routerView.ts index 3f2ad0a0..8ead9957 100644 --- a/src/components/routerView.ts +++ b/src/components/routerView.ts @@ -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' @@ -46,8 +46,8 @@ export function createRouterView(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) diff --git a/src/services/createCurrentRejection.ts b/src/services/createCurrentRejection.ts index 6bb505c9..e5f124ea 100644 --- a/src/services/createCurrentRejection.ts +++ b/src/services/createCurrentRejection.ts @@ -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, updateRejection: RejectionUpdate, clearRejection: RejectionClear, } @@ -21,8 +24,17 @@ export function createCurrentRejection(): CurrentRejectionContext { const currentRejection: RouterRejection = ref(null) + const currentRejectionRoute = computed(() => { + if (isRejection(currentRejection.value)) { + return createResolvedRoute(currentRejection.value.route) + } + + return null + }) + return { currentRejection, + currentRejectionRoute, updateRejection, clearRejection, } diff --git a/src/services/createExternalRoute.ts b/src/services/createExternalRoute.ts index ff7621e4..ca6c9e9d 100644 --- a/src/services/createExternalRoute.ts +++ b/src/services/createExternalRoute.ts @@ -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 diff --git a/src/services/createRejection.ts b/src/services/createRejection.ts index af037929..1f82f485 100644 --- a/src/services/createRejection.ts +++ b/src/services/createRejection.ts @@ -3,21 +3,24 @@ 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(options: { type: TType, component?: Component, -}): Rejection & RejectionHooks - -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 & RejectionHooks & 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, @@ -25,35 +28,11 @@ export function createRejection(options: { type: string, component?: Component } } 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 -} diff --git a/src/services/createRoute.ts b/src/services/createRoute.ts index 576b42de..40f5e2bf 100644 --- a/src/services/createRoute.ts +++ b/src/services/createRoute.ts @@ -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, diff --git a/src/services/createRouter.ts b/src/services/createRouter.ts index ccb3870e..03d672cb 100644 --- a/src/services/createRouter.ts +++ b/src/services/createRouter.ts @@ -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() @@ -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) @@ -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() } @@ -318,15 +319,17 @@ export function createRouter< } hooks.runRejectionHooks(rejection, { to, from }) + updateRejection(rejection) } const reject: RouterReject = (type) => { setRejection(type) + setDocumentTitle(currentRejectionRoute.value) } - const { currentRejection, updateRejection, clearRejection } = createCurrentRejection() - const { currentRoute, routerRoute, updateRoute } = createCurrentRoute(routerKey, notFoundRejection.route, push) + const { currentRejection, currentRejectionRoute, updateRejection, clearRejection } = createCurrentRejection() + const { currentRoute, routerRoute, updateRoute } = createCurrentRoute(routerKey, notFoundRoute, push) const initialUrl = getInitialUrl(options?.initialUrl) const initialState = history.location.state diff --git a/src/types/rejection.ts b/src/types/rejection.ts index aadc2927..3e328839 100644 --- a/src/types/rejection.ts +++ b/src/types/rejection.ts @@ -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' @@ -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[], } @@ -32,10 +32,6 @@ export type Rejection = { * The type of rejection. */ type: TType, - /** - * The component to render when the rejection occurs. - */ - component: Component, } export type RejectionType = diff --git a/src/types/route.ts b/src/types/route.ts index d5c9cfb4..9d0f3997 100644 --- a/src/types/route.ts +++ b/src/types/route.ts @@ -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' @@ -20,7 +20,7 @@ export type RouteInternal = { depth: number, hooks: Hooks[], redirect: RouteRedirect, - getTitle: GetTitle, + getTitle: GetRouteTitle, } /** diff --git a/src/types/titles.browser.spec.ts b/src/types/routeTitle.browser.spec.ts similarity index 95% rename from src/types/titles.browser.spec.ts rename to src/types/routeTitle.browser.spec.ts index 0c189f1c..9213a430 100644 --- a/src/types/titles.browser.spec.ts +++ b/src/types/routeTitle.browser.spec.ts @@ -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 () => { @@ -20,7 +20,7 @@ test('route with title updates document title', async () => { initialUrl: '/', }) - router.start() + await router.start() await flushPromises() @@ -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() @@ -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() @@ -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() @@ -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() diff --git a/src/types/titles.ts b/src/types/routeTitle.ts similarity index 52% rename from src/types/titles.ts rename to src/types/routeTitle.ts index a0fe627d..f46e43d3 100644 --- a/src/types/titles.ts +++ b/src/types/routeTitle.ts @@ -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, } -export type SetTitleCallback = (to: ResolvedRouteUnion, context: SetTitleContext) => MaybePromise -export type GetTitle = (to: ResolvedRouteUnion) => Promise -export type SetTitle = (callback: SetTitleCallback) => void +export type SetRouteTitleCallback = (to: ResolvedRouteUnion, context: SetRouteTitleContext) => MaybePromise +export type GetRouteTitle = (to: ResolvedRouteUnion) => Promise +export type SetRouteTitle = (callback: SetRouteTitleCallback) => void export type RouteSetTitle = { /** * Adds a callback to set the document title for the route. */ - setTitle: SetTitle, + setTitle: SetRouteTitle, } 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 => { if (parent && isRoute(parent)) { return parent.getTitle(to) diff --git a/src/utilities/setDocumentTitle.ts b/src/utilities/setDocumentTitle.ts index f47c02d7..b346f548 100644 --- a/src/utilities/setDocumentTitle.ts +++ b/src/utilities/setDocumentTitle.ts @@ -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 }