@tanstack/vue-router
Version:
Modern and scalable routing for Vue applications
308 lines • 10.9 kB
JSX
import * as Vue from 'vue';
import { useRouter } from './useRouter';
function _resolveBlockerOpts(opts, condition) {
if (opts === undefined) {
return {
shouldBlockFn: () => true,
withResolver: false,
};
}
if ('shouldBlockFn' in opts) {
return opts;
}
if (typeof opts === 'function') {
const shouldBlock = Boolean(condition ?? true);
const _customBlockerFn = async () => {
if (shouldBlock)
return await opts();
return false;
};
return {
shouldBlockFn: _customBlockerFn,
enableBeforeUnload: shouldBlock,
withResolver: false,
};
}
const shouldBlock = Boolean(opts.condition ?? true);
const fn = opts.blockerFn;
const _customBlockerFn = async () => {
if (shouldBlock && fn !== undefined) {
return await fn();
}
return shouldBlock;
};
return {
shouldBlockFn: _customBlockerFn,
enableBeforeUnload: shouldBlock,
withResolver: fn === undefined,
};
}
export function useBlocker(opts, condition) {
const { shouldBlockFn, enableBeforeUnload = true, disabled = false, withResolver = false, } = _resolveBlockerOpts(opts, condition);
const router = useRouter();
const { history } = router;
const resolver = Vue.ref({
status: 'idle',
current: undefined,
next: undefined,
action: undefined,
proceed: undefined,
reset: undefined,
});
Vue.watchEffect((onCleanup) => {
const blockerFnComposed = async (blockerFnArgs) => {
function getLocation(location) {
const parsedLocation = router.parseLocation(location);
const matchedRoutes = router.getMatchedRoutes(parsedLocation.pathname);
if (matchedRoutes.foundRoute === undefined) {
return {
routeId: '__notFound__',
fullPath: parsedLocation.pathname,
pathname: parsedLocation.pathname,
params: matchedRoutes.routeParams,
search: parsedLocation.search,
};
}
return {
routeId: matchedRoutes.foundRoute.id,
fullPath: matchedRoutes.foundRoute.fullPath,
pathname: parsedLocation.pathname,
params: matchedRoutes.routeParams,
search: parsedLocation.search,
};
}
const current = getLocation(blockerFnArgs.currentLocation);
const next = getLocation(blockerFnArgs.nextLocation);
// Allow navigation away from 404 pages to valid routes
if (current.routeId === '__notFound__' &&
next.routeId !== '__notFound__') {
return false;
}
const shouldBlock = await shouldBlockFn({
action: blockerFnArgs.action,
current,
next,
});
if (!withResolver) {
return shouldBlock;
}
if (!shouldBlock) {
return false;
}
const promise = new Promise((resolve) => {
resolver.value = {
status: 'blocked',
current,
next,
action: blockerFnArgs.action,
proceed: () => resolve(false),
reset: () => resolve(true),
};
});
const canNavigateAsync = await promise;
resolver.value = {
status: 'idle',
current: undefined,
next: undefined,
action: undefined,
proceed: undefined,
reset: undefined,
};
return canNavigateAsync;
};
if (disabled) {
return;
}
const unsubscribe = history.block({
blockerFn: blockerFnComposed,
enableBeforeUnload,
});
onCleanup(() => {
if (unsubscribe)
unsubscribe();
});
});
return withResolver ? resolver : undefined;
}
const _resolvePromptBlockerArgs = (props) => {
if ('shouldBlockFn' in props) {
return { ...props };
}
const shouldBlock = Boolean(props.condition ?? true);
const fn = props.blockerFn;
const _customBlockerFn = async () => {
if (shouldBlock && fn !== undefined) {
return await fn();
}
return shouldBlock;
};
return {
shouldBlockFn: _customBlockerFn,
enableBeforeUnload: shouldBlock,
withResolver: fn === undefined,
};
};
// Internal Block implementation as a proper Vue component for reactivity
const BlockImpl = Vue.defineComponent({
name: 'Block',
props: {
shouldBlockFn: {
type: Function,
required: false,
},
enableBeforeUnload: {
type: [Boolean, Function],
default: true,
},
disabled: {
type: Boolean,
default: false,
},
withResolver: {
type: Boolean,
default: false,
},
// Legacy props
blockerFn: {
type: Function,
required: false,
},
condition: {
type: [Boolean, Object],
required: false,
},
},
setup(props, { slots }) {
// Create a computed that resolves the blocker args reactively
const blockerArgs = Vue.computed(() => {
if (props.shouldBlockFn) {
return {
shouldBlockFn: props.shouldBlockFn,
enableBeforeUnload: props.enableBeforeUnload,
disabled: props.disabled,
withResolver: props.withResolver,
};
}
// Legacy handling
const shouldBlock = Boolean(props.condition ?? true);
const fn = props.blockerFn;
const _customBlockerFn = async () => {
if (shouldBlock && fn !== undefined) {
return await fn();
}
return shouldBlock;
};
return {
shouldBlockFn: _customBlockerFn,
enableBeforeUnload: shouldBlock,
disabled: props.disabled,
withResolver: fn === undefined,
};
});
// Use a reactive useBlocker that re-subscribes when args change
const router = useRouter();
const { history } = router;
const resolver = Vue.ref({
status: 'idle',
current: undefined,
next: undefined,
action: undefined,
proceed: undefined,
reset: undefined,
});
Vue.watchEffect((onCleanup) => {
const args = blockerArgs.value;
if (args.disabled) {
return;
}
const blockerFnComposed = async (blockerFnArgs) => {
function getLocation(location) {
const parsedLocation = router.parseLocation(location);
const matchedRoutes = router.getMatchedRoutes(parsedLocation.pathname);
if (matchedRoutes.foundRoute === undefined) {
return {
routeId: '__notFound__',
fullPath: parsedLocation.pathname,
pathname: parsedLocation.pathname,
params: matchedRoutes.routeParams,
search: parsedLocation.search,
};
}
return {
routeId: matchedRoutes.foundRoute.id,
fullPath: matchedRoutes.foundRoute.fullPath,
pathname: parsedLocation.pathname,
params: matchedRoutes.routeParams,
search: parsedLocation.search,
};
}
const current = getLocation(blockerFnArgs.currentLocation);
const next = getLocation(blockerFnArgs.nextLocation);
// Allow navigation away from 404 pages to valid routes
if (current.routeId === '__notFound__' &&
next.routeId !== '__notFound__') {
return false;
}
const shouldBlock = await args.shouldBlockFn({
action: blockerFnArgs.action,
current,
next,
});
if (!args.withResolver) {
return shouldBlock;
}
if (!shouldBlock) {
return false;
}
const promise = new Promise((resolve) => {
resolver.value = {
status: 'blocked',
current,
next,
action: blockerFnArgs.action,
proceed: () => resolve(false),
reset: () => resolve(true),
};
});
const canNavigateAsync = await promise;
resolver.value = {
status: 'idle',
current: undefined,
next: undefined,
action: undefined,
proceed: undefined,
reset: undefined,
};
return canNavigateAsync;
};
const unsubscribe = history.block({
blockerFn: blockerFnComposed,
enableBeforeUnload: args.enableBeforeUnload,
});
onCleanup(() => {
if (unsubscribe)
unsubscribe();
});
});
return () => {
const defaultSlot = slots.default;
if (!defaultSlot) {
return Vue.h(Vue.Fragment, null);
}
// If slot is a function that takes resolver, call it with the resolver
const slotContent = defaultSlot(resolver.value);
return Vue.h(Vue.Fragment, null, slotContent);
};
},
});
export function Block(opts) {
const { children, ...rest } = opts;
// Convert children to slot format for the component
const slots = children
? typeof children === 'function'
? { default: children }
: { default: () => children }
: undefined;
return Vue.h(BlockImpl, rest, slots);
}
//# sourceMappingURL=useBlocker.jsx.map