@tanstack/vue-router
Version:
Modern and scalable routing for Vue applications
189 lines • 7.45 kB
JSX
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