UNPKG

@tanstack/vue-router

Version:

Modern and scalable routing for Vue applications

189 lines 7.45 kB
import * as Vue from 'vue'; import { isServer } from '@tanstack/router-core/isServer'; import { useStore } from '@tanstack/vue-store'; import { CatchBoundary } from './CatchBoundary'; import { useRouter } from './useRouter'; import { useTransitionerSetup } from './Transitioner'; import { matchContext } from './matchContext'; import { Match } from './Match'; // Create a component that renders MatchesInner with Transitioner's setup logic inlined. // This is critical for proper hydration - we call useTransitionerSetup() as a composable // rather than rendering it as a component, which avoids Fragment/element mismatches. const MatchesContent = Vue.defineComponent({ name: 'MatchesContent', setup() { // IMPORTANT: We need to ensure Transitioner's setup() runs. // Transitioner sets up critical functionality: // - router.startTransition // - History subscription via router.history.subscribe(router.load) // - Watchers for router events // // We inline Transitioner's setup logic here. Since Transitioner returns null, // we can call its setup function directly without affecting the render tree. // This is done by importing and calling useTransitionerSetup. useTransitionerSetup(); return () => Vue.h(MatchesInner); }, }); export const Matches = Vue.defineComponent({ name: 'Matches', setup() { const router = useRouter(); return () => { const pendingElement = router?.options?.defaultPendingComponent ? Vue.h(router.options.defaultPendingComponent) : null; // Do not render a root Suspense during SSR or hydrating from SSR const inner = (isServer ?? router?.isServer ?? false) || (typeof document !== 'undefined' && router?.ssr) ? Vue.h(MatchesContent) : Vue.h(Vue.Suspense, { fallback: pendingElement }, { default: () => Vue.h(MatchesContent), }); return router?.options?.InnerWrap ? Vue.h(router.options.InnerWrap, null, { default: () => inner }) : inner; }; }, }); // Create a simple error component function that matches ErrorRouteComponent const errorComponentFn = (props) => { return Vue.h('div', { class: 'error' }, [ Vue.h('h1', null, 'Error'), Vue.h('p', null, props.error.message || String(props.error)), Vue.h('button', { onClick: props.reset }, 'Try Again'), ]); }; const MatchesInner = Vue.defineComponent({ name: 'MatchesInner', setup() { const router = useRouter(); const matchId = useStore(router.stores.firstId, (id) => id); const resetKey = useStore(router.stores.loadedAt, (loadedAt) => loadedAt); // Create a ref for the match id to provide const matchIdRef = Vue.computed(() => matchId.value); // Provide the matchId for child components using the InjectionKey Vue.provide(matchContext, matchIdRef); return () => { // Generate a placeholder element if matchId.value is not present const childElement = matchId.value ? Vue.h(Match, { matchId: matchId.value }) : Vue.h('div'); // If disableGlobalCatchBoundary is true, don't wrap in CatchBoundary if (router.options.disableGlobalCatchBoundary) { return childElement; } return Vue.h(CatchBoundary, { getResetKey: () => resetKey.value, errorComponent: errorComponentFn, onCatch: process.env.NODE_ENV !== 'production' ? (error) => { console.warn(`Warning: The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`); console.warn(`Warning: ${error.message || error.toString()}`); } : undefined, children: childElement, }); }; }, }); export function useMatchRoute() { const router = useRouter(); const routerState = useStore(router.stores.matchRouteDeps, (value) => value); return (opts) => { const { pending, caseSensitive, fuzzy, includeSearch, ...rest } = opts; const matchRoute = Vue.computed(() => { // Access routerState to establish dependency routerState.value; return router.matchRoute(rest, { pending, caseSensitive, fuzzy, includeSearch, }); }); return matchRoute; }; } export const MatchRoute = Vue.defineComponent({ name: 'MatchRoute', props: { // Define props to match MakeMatchRouteOptions from: { type: String, required: false, }, to: { type: String, required: false, }, fuzzy: { type: Boolean, required: false, }, caseSensitive: { type: Boolean, required: false, }, includeSearch: { type: Boolean, required: false, }, pending: { type: Boolean, required: false, }, }, setup(props, { slots }) { const router = useRouter(); const status = useStore(router.stores.matchRouteDeps, (value) => value.status); return () => { if (!status.value) return null; const matchRoute = useMatchRoute(); const params = matchRoute(props).value; // Create a component that renders the slot in a reactive manner if (!params || !slots.default) { return null; } // For function slots, pass the params if (typeof slots.default === 'function') { // Use h to create a wrapper component that will call the slot function return Vue.h(Vue.Fragment, null, slots.default(params)); } // For normal slots, just render them return Vue.h(Vue.Fragment, null, slots.default); }; }, }); export function useMatches(opts) { const router = useRouter(); return useStore(router.stores.matches, (matches) => { return opts?.select ? opts.select(matches) : matches; }); } export function useParentMatches(opts) { // Use matchContext with proper type const contextMatchId = Vue.inject(matchContext); const safeMatchId = Vue.computed(() => contextMatchId?.value || ''); return useMatches({ select: (matches) => { matches = matches.slice(0, matches.findIndex((d) => d.id === safeMatchId.value)); return opts?.select ? opts.select(matches) : matches; }, }); } export function useChildMatches(opts) { // Use matchContext with proper type const contextMatchId = Vue.inject(matchContext); const safeMatchId = Vue.computed(() => contextMatchId?.value || ''); return useMatches({ select: (matches) => { matches = matches.slice(matches.findIndex((d) => d.id === safeMatchId.value) + 1); return opts?.select ? opts.select(matches) : matches; }, }); } //# sourceMappingURL=Matches.jsx.map