UNPKG

@esmx/router-vue

Version:

Vue integration for @esmx/router - A universal router that works seamlessly with both Vue 2.7+ and Vue 3

413 lines (383 loc) 11.6 kB
import type { Route, Router, RouterLinkProps } from '@esmx/router'; import { computed, getCurrentInstance, inject, onBeforeUnmount, provide, ref } from 'vue'; import { createDependentProxy, createSymbolProperty } from './util'; export interface VueInstance { $parent?: VueInstance | null; $root?: VueInstance | null; $children?: VueInstance[] | null; } interface RouterContext { router: Router; route: Route; } const ROUTER_CONTEXT_KEY = Symbol('router-context'); const ROUTER_INJECT_KEY = Symbol('router-inject'); const ROUTER_VIEW_DEPTH_KEY = Symbol('router-view-depth'); const routerContextProperty = createSymbolProperty<RouterContext>(ROUTER_CONTEXT_KEY); const routerViewDepthProperty = createSymbolProperty<number>( ROUTER_VIEW_DEPTH_KEY ); function getCurrentProxy(): VueInstance { const instance = getCurrentInstance(); if (!instance || !instance.proxy) { throw new Error( '[@esmx/router-vue] Must be used within setup() or other composition functions' ); } return instance.proxy; } function findRouterContext(vm?: VueInstance): RouterContext { // If no vm provided, try to get current instance if (!vm) { vm = getCurrentProxy(); } let context = routerContextProperty.get(vm); if (context) { return context; } let current = vm.$parent; while (current) { context = routerContextProperty.get(current); if (context) { routerContextProperty.set(vm, context); return context; } current = current.$parent; } throw new Error( '[@esmx/router-vue] Router context not found. Please ensure useProvideRouter() is called in a parent component.' ); } /** * Get router instance from a Vue component instance. * This is a lower-level function used internally by useRouter(). * Use this in Options API, use useRouter() in Composition API. * * @param instance - Vue component instance (optional, will use getCurrentInstance if not provided) * @returns Router instance * @throws {Error} If router context is not found * * @example * ```typescript * // Options API usage * import { defineComponent } from 'vue'; * import { getRouter } from '@esmx/router-vue'; * * export default defineComponent({ * mounted() { * const router = getRouter(this); * router.push('/dashboard'); * }, * methods: { * handleNavigation() { * const router = getRouter(this); * router.replace('/profile'); * } * } * }); * * // Can also be called without instance (uses getCurrentInstance internally) * const router = getRouter(); // Works in globalProperties getters * ``` */ export function getRouter(instance?: VueInstance): Router { return findRouterContext(instance).router; } /** * Get current route from a Vue component instance. * This is a lower-level function used internally by useRoute(). * Use this in Options API, use useRoute() in Composition API. * * @param instance - Vue component instance (optional, will use getCurrentInstance if not provided) * @returns Current route object * @throws {Error} If router context is not found * * @example * ```typescript * // Options API usage * import { defineComponent } from 'vue'; * import { getRoute } from '@esmx/router-vue'; * * export default defineComponent({ * computed: { * routeInfo() { * const route = getRoute(this); * return { * path: route.path, * params: route.params, * query: route.query * }; * } * } * }); * * // Can also be called without instance (uses getCurrentInstance internally) * const route = getRoute(); // Works in globalProperties getters * ``` */ export function getRoute(instance?: VueInstance): Route { return findRouterContext(instance).route; } /** * Get router context using the optimal method available. * First tries provide/inject (works in setup), then falls back to hierarchy traversal. */ function useRouterContext(): RouterContext { // First try to get context from provide/inject (works in setup) const injectedContext = inject<RouterContext>(ROUTER_INJECT_KEY); if (injectedContext) { return injectedContext; } // Fallback to component hierarchy traversal (works after mount) const proxy = getCurrentProxy(); return findRouterContext(proxy); } /** * Get the router instance in a Vue component. * Must be called within setup() or other composition functions. * Use this in Composition API, use getRouter() in Options API. * * @returns Router instance for navigation and route management * @throws {Error} If called outside setup() or router context not found * * @example * ```vue * <script setup lang="ts"> * import { useRouter } from '@esmx/router-vue'; * * const router = useRouter(); * * const navigateToHome = () => { * router.push('/home'); * }; * * const goBack = () => { * router.back(); * }; * * const navigateWithQuery = () => { * router.push({ * path: '/search', * query: { q: 'vue router', page: '1' } * }); * }; * </script> * ``` */ export function useRouter(): Router { return useRouterContext().router; } /** * Get the current route information in a Vue component. * Returns a reactive reference that automatically updates when the route changes. * Must be called within setup() or other composition functions. * Use this in Composition API, use getRoute() in Options API. * * @returns Current route object with path, params, query, etc. * @throws {Error} If called outside setup() or router context not found * * @example * ```vue * <template> * <div> * <h1>{{ route.meta?.title || 'Page' }}</h1> * <p>Path: {{ route.path }}</p> * <p>Params: {{ JSON.stringify(route.params) }}</p> * <p>Query: {{ JSON.stringify(route.query) }}</p> * </div> * </template> * * <script setup lang="ts"> * import { useRoute } from '@esmx/router-vue'; * import { watch } from 'vue'; * * const route = useRoute(); * * watch(() => route.path, (newPath) => { * console.log('Route changed to:', newPath); * }); * </script> * ``` */ export function useRoute(): Route { return useRouterContext().route; } /** * Provide router context to child components. * This must be called in a parent component to make the router available * to child components via useRouter() and useRoute(). * * @param router - Router instance to provide to child components * @throws {Error} If called outside setup() * * @example * ```typescript * // Vue 3 usage * import { createApp } from 'vue'; * import { Router } from '@esmx/router'; * import { useProvideRouter } from '@esmx/router-vue'; * * const routes = [ * { path: '/', component: () => import('./Home.vue') }, * { path: '/about', component: () => import('./About.vue') } * ]; * * const router = new Router({ routes }); * const app = createApp({ * setup() { * useProvideRouter(router); * } * }); * app.mount('#app'); * ``` */ export function useProvideRouter(router: Router): void { const proxy = getCurrentProxy(); const dep = ref(0); const proxiedRouter = createDependentProxy(router, dep); const proxiedRoute = createDependentProxy(router.route, dep); const context: RouterContext = { router: proxiedRouter, route: proxiedRoute }; provide(ROUTER_INJECT_KEY, context); routerContextProperty.set(proxy, context); const unwatch = router.afterEach((to: Route) => { if (router.route === to) { to.syncTo(proxiedRoute); dep.value++; } }); onBeforeUnmount(unwatch); } /** * Get the current RouterView depth in nested routing scenarios. * Returns the depth of the current RouterView component in the component tree. * Useful for advanced routing scenarios where you need to know the nesting level. * * @param isRender - Whether this is used in a RouterView component that needs to provide depth for children (default: false) * @returns Current RouterView depth (0 for root level, 1 for first nested level, etc.) * @throws {Error} If called outside setup() * * @example * ```vue * <template> * <div> * <p>Current RouterView depth: {{ depth }}</p> * <RouterView /> * </div> * </template> * * <script setup lang="ts"> * import { useRouterViewDepth } from '@esmx/router-vue'; * * // Get current depth without providing for children * const depth = useRouterViewDepth(); * console.log('Current RouterView depth:', depth); // 0, 1, 2, etc. * * // Get current depth and provide depth + 1 for children (used in RouterView component) * const depth = useRouterViewDepth(true); * </script> * ``` */ export function _useRouterViewDepth(isRender?: boolean): number { const depth = inject(ROUTER_VIEW_DEPTH_KEY, 0); if (isRender) { provide(ROUTER_VIEW_DEPTH_KEY, depth + 1); const proxy = getCurrentProxy(); routerViewDepthProperty.set(proxy, depth + 1); } return depth; } /** * Get the current RouterView depth in nested routing scenarios. * Returns the depth of the current RouterView component in the component tree. * Useful for advanced routing scenarios where you need to know the nesting level. * * @returns Current RouterView depth (0 for root level, 1 for first nested level, etc.) * @throws {Error} If called outside setup() * * @example * ```vue * <template> * <div> * <p>Current RouterView depth: {{ depth }}</p> * <RouterView /> * </div> * </template> * * <script setup lang="ts"> * import { useRouterViewDepth } from '@esmx/router-vue'; * * // Get current depth without providing for children * const depth = useRouterViewDepth(); * console.log('Current RouterView depth:', depth); // 0, 1, 2, etc. * </script> * ``` */ export function useRouterViewDepth(): number { return _useRouterViewDepth(); } /** * Get injected RouterView depth from a Vue instance's ancestors. * Traverses parent chain to find the value provided under ROUTER_VIEW_DEPTH_KEY. * * @param instance - Vue component instance to start from * @returns Injected RouterView depth value from nearest ancestor * @throws {Error} If no ancestor provided ROUTER_VIEW_DEPTH_KEY */ export function getRouterViewDepth(instance: VueInstance): number { let current = instance.$parent; while (current) { const value = routerViewDepthProperty.get(current); if (typeof value === 'number') return value; current = current.$parent; } throw new Error( '[@esmx/router-vue] RouterView depth not found. Please ensure a RouterView exists in ancestor components.' ); } /** * Create reactive link helpers for navigation elements. * Returns computed properties for link attributes, classes, and event handlers. * * @param props - RouterLink properties configuration * @returns Computed link resolver with attributes and event handlers * * @example * ```vue * <template> * <a * v-bind="link.attributes" * v-on="link.createEventHandlers()" * :class="{ active: link.isActive }" * > * Home * </a> * </template> * * <script setup lang="ts"> * import { useLink } from '@esmx/router-vue'; * * const link = useLink({ * to: '/home', * type: 'push', * exact: 'include' * }).value; * </script> * ``` */ export function useLink(props: RouterLinkProps) { const router = useRouter(); return computed(() => { return router.resolveLink(props); }); }