UNPKG

@tanstack/vue-router

Version:

Modern and scalable routing for Vue applications

308 lines 10.9 kB
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