-import { createApp, ComponentPublicInstance } from 'vue'
+import { createApp, ComponentPublicInstance, App } from 'vue'
import { Router } from '../src'
const context = require.context('.', true, /^.{2,}\/index\.ts$/)
context.keys().forEach(path => {
const match = DIR_RE.exec(path)
if (match) examples.push(match[1])
- return name
})
examples.sort()
declare global {
interface Window {
- app: typeof app
+ app: App<Element>
+ // @ts-expect-error: wat?
vm: ComponentPublicInstance
r: Router
}
+++ /dev/null
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <meta http-equiv="X-UA-Compatible" content="ie=edge" />
- <title>Vue Router e2e tests - Suspended RouterView</title>
- <!-- TODO: replace with local imports for promises and anything else needed -->
- <script src="https://polyfill.io/v3/polyfill.min.js?features=default%2Ces2015"></script>
-
- <style>
- .fade-enter-active,
- .fade-leave-active {
- transition: opacity 2s ease;
- }
- .fade-enter-from,
- .fade-leave-active {
- opacity: 0;
- }
- </style>
- </head>
- <body>
- <a href="/"><< Back to Homepage</a>
- <hr />
-
- <main id="app"></main>
- </body>
-</html>
+++ /dev/null
-import {
- createRouter,
- createWebHistory,
- useRoute,
- viewDepthKey,
-} from '../../src'
-import {
- computed,
- createApp,
- defineComponent,
- onErrorCaptured,
- inject,
- shallowRef,
-} from 'vue'
-import {
- NavigationGuardReturn,
- RouteLocationNormalizedLoaded,
-} from '../../src/types'
-
-// override existing style on dev with shorter times
-if (!__CI__) {
- const transitionDuration = '0.3s'
- const styleEl = document.createElement('style')
- styleEl.innerHTML = `
-.fade-enter-active,
-.fade-leave-active {
- transition: opacity ${transitionDuration} ease;
-}
-`
- document.head.append(styleEl)
-}
-
-const delay = (t: number) => new Promise(r => setTimeout(r, t))
-
-const Home = defineComponent({
- template: `
- <div>
- <h1>Home</h1>
- </div>`,
-})
-
-const ViewRegular = defineComponent({
- template: '<div>Regular</div>',
-})
-
-const ViewId = defineComponent({
- template: '<div>Id: {{ $route.params.id }}</div>',
-})
-
-const ViewData = defineComponent({
- template: `
- <div>
- <h1>With Data</h1>
- <p>{{ $route.path }}</p>
-
- <router-view v-slot="{ Component }">
- <transition v-if="Component" name="fade" mode="out-in">
- <suspense :timeout="0" v-bind="suspenseProps">
- <component :is="Component" />
- <template #fallback>
- <p>Loading ViewData...</p>
- </template>
- </suspense>
- </transition>
- </router-view>
-
- </div>
- `,
-
- async setup() {
- const depth = inject(viewDepthKey, 0)
-
- const suspenseProps = createSuspenseProps(`ViewData(${depth})`)
-
- onErrorCaptured((err, target, info) => {
- console.log(`caught at ViewData(${depth})`, err, target, info)
- // stop propagation
- // return false
- })
-
- console.log(`waiting at ${depth}...`)
- await delay(1000)
- console.log(`done at ${depth}!`)
-
- if (depth > 0) {
- // NOTE: artificial increment because suspense is not emitting
- suspenseProps.onPending()
- throw new Error('oops')
- }
-
- return { suspenseProps }
- },
-})
-
-const router = createRouter({
- history: createWebHistory('/' + __dirname),
- routes: [
- { path: '/', component: Home },
- {
- path: '/data',
- component: ViewData,
- children: [
- { path: '', component: ViewRegular },
- { path: 'data', component: ViewData },
- { path: ':id', name: 'id1', component: ViewId },
- ],
- },
- {
- path: '/data-2',
- component: ViewData,
- children: [
- { path: '', component: ViewRegular },
- { path: 'data', component: ViewData },
- { path: ':id', name: 'id2', component: ViewId },
- ],
- },
- ],
-})
-
-function createSuspenseProps(name: string) {
- function onPending() {
- console.log('onPending:' + name)
- console.log('Now remaining', ++remainingValidations)
- }
- function onResolve() {
- console.log('onResolve:' + name)
- if (resolveSuspense) {
- resolveSuspense()
- }
- }
- function onFallback() {
- console.log('onFallback:' + name)
- }
-
- return { onPending, onResolve, onFallback }
-}
-
-let resolveSuspense:
- | ((value?: NavigationGuardReturn) => void)
- | undefined
- | null
-let rejectSuspense: ((reason: any) => void) | undefined | null
-let pendingRoute = shallowRef<
- RouteLocationNormalizedLoaded | undefined | null
->()
-let remainingValidations = 0
-
-router.beforeResolve((to, from) => {
- return new Promise((resolve, reject) => {
- // we need at least one, we increment the rest with onPending
- // should probably be provided and then injected in each routerview to increment the counter
- // then the resolve could decrement and check if it's under 0
- remainingValidations = 0
- if (resolveSuspense) {
- resolveSuspense(false)
- }
- pendingRoute.value = to
- resolveSuspense = () => {
- console.log('resolving suspense', remainingValidations - 1)
- if (--remainingValidations < 1) {
- resolveSuspense = null
- rejectSuspense = null
- resolve()
- // pendingRoute.value = null
- console.log('✅ Resolved')
- }
- }
- rejectSuspense = reason => {
- console.log('rejecting suspense')
- rejectSuspense = null
- resolveSuspense = null
- reject(reason)
- // pendingRoute.value = null
- }
-
- console.log('pendingRoute set', remainingValidations)
- })
-})
-
-const app = createApp({
- setup() {
- const route = useRoute()
-
- onErrorCaptured((err, target, info) => {
- console.log('caught at Root', err, target, info)
- if (rejectSuspense) {
- rejectSuspense(err)
- }
- // stop propagation
- return false
- })
-
- const nextId = computed(() => (Number(route.params.id) || 0) + 1)
- const suspenseProps = createSuspenseProps('Root')
-
- return {
- nextId,
- suspenseProps,
- pendingRoute,
- }
- },
-
- template: `
- <ul>
- <li><router-link to="/">Home</router-link></li>
- <li><router-link to="/data">Suspended</router-link></li>
- <li><router-link to="/data/data">Suspended nested</router-link></li>
- <li><router-link :to="{ name: 'id1', params: { id: nextId }}" v-slot="{ route }">{{ route.fullPath }}</router-link></li>
-
- <li><router-link to="/data-2">Suspended (2)</router-link></li>
- <li><router-link to="/data-2/data">Suspended nested (2)</router-link></li>
- <li><router-link :to="{ name: 'id2', params: { id: nextId }}" v-slot="{ route }">{{ route.fullPath }}</router-link></li>
- </ul>
-
- <pre v-if="pendingRoute">Loading {{ pendingRoute.fullPath }} from {{ $route.fullPath }}</pre>
-
- <router-view v-slot="{ Component }" :route="pendingRoute">
- <transition name="fade" mode="out-in" v-if="Component">
- <suspense :timeout="0" v-bind="suspenseProps">
- <component :is="Component" />
- <template #fallback>
- <p>Loading App...</p>
- </template>
- </suspense>
- </transition>
- </router-view>
- `,
-})
-app.use(router)
-// app.component('RouterView', RouterViewSuspended)
-
-window.vm = app.mount('#app')
-window.r = router
+++ /dev/null
-import {
- h,
- inject,
- provide,
- defineComponent,
- PropType,
- ref,
- ComponentPublicInstance,
- VNodeProps,
- computed,
- AllowedComponentProps,
- ComponentCustomProps,
- watch,
- Suspense,
-} from 'vue'
-import {
- RouteLocationNormalized,
- RouteLocationNormalizedLoaded,
- RouteLocationMatched,
-} from './types'
-import {
- matchedRouteKey,
- viewDepthKey,
- routerViewLocationKey,
- suspendedRouteKey,
-} from './injectionSymbols'
-import { assign } from './utils'
-import { isSameRouteRecord } from './location'
-
-export interface RouterViewSuspendedProps {
- name?: string
- // allow looser type for user facing api
- route?: RouteLocationNormalized
-}
-
-export const RouterViewSuspendedImpl = /*#__PURE__*/ defineComponent({
- name: 'RouterViewSuspended',
- // #674 we manually inherit them
- inheritAttrs: false,
- props: {
- name: {
- type: String as PropType<string>,
- default: 'default',
- },
- route: Object as PropType<RouteLocationNormalizedLoaded>,
- },
-
- setup(props, { attrs }) {
- const injectedRoute = inject(routerViewLocationKey)!
- const isSuspended = inject('isSuspended', false as boolean)
- // TODO: should be pending route -> after leave, update and global navigation guards
- const suspendedRoute = inject(suspendedRouteKey)!
- const routeToDisplay = computed(() => props.route || injectedRoute.value)
- const depth = inject(viewDepthKey, 0)
- const matchedRouteRef = computed<RouteLocationMatched | undefined>(
- () => routeToDisplay.value.matched[depth]
- )
- const suspendedMatchedRouteRef = computed<
- RouteLocationMatched | undefined | null
- >(() => suspendedRoute.value && suspendedRoute.value.matched[depth])
-
- provide(viewDepthKey, depth + 1)
- provide(matchedRouteKey, matchedRouteRef)
- provide(routerViewLocationKey, routeToDisplay)
- provide('isSuspended', true)
-
- const viewRef = ref<ComponentPublicInstance>()
-
- // watch at the same time the component instance, the route record we are
- // rendering, and the name
- watch(
- () => [viewRef.value, matchedRouteRef.value, props.name] as const,
- ([instance, to, name], [oldInstance, from, oldName]) => {
- // copy reused instances
- if (to) {
- // this will update the instance for new instances as well as reused
- // instances when navigating to a new route
- to.instances[name] = instance
- // the component instance is reused for a different route or name so
- // we copy any saved update or leave guards
- if (from && from !== to && instance && instance === oldInstance) {
- to.leaveGuards = from.leaveGuards
- to.updateGuards = from.updateGuards
- }
- }
-
- // trigger beforeRouteEnter next callbacks
- if (
- instance &&
- to &&
- // if there is no instance but to and from are the same this might be
- // the first visit
- (!from || !isSameRouteRecord(to, from) || !oldInstance)
- ) {
- ;(to.enterCallbacks[name] || []).forEach(callback =>
- callback(instance)
- )
- }
- },
- { flush: 'post' }
- )
-
- return () => {
- const route = routeToDisplay.value
- const matchedRoute = matchedRouteRef.value
- const suspendedMatchedRoute = suspendedMatchedRouteRef.value
- const ViewComponent = matchedRoute && matchedRoute.components[props.name]
- const SuspendedViewComponent =
- suspendedMatchedRoute && suspendedMatchedRoute.components[props.name]
- // we need the value at the time we render because when we unmount, we
- // navigated to a different location so the value is different
- const currentName = props.name
-
- console.log('suspended', suspendedMatchedRoute)
-
- // TODO: should be smarter to still display a suspended component
- if (!ViewComponent) {
- return null
- }
-
- // props from route configuration
- const routePropsOption = matchedRoute!.props[props.name]
- const routeProps = routePropsOption
- ? routePropsOption === true
- ? route.params
- : typeof routePropsOption === 'function'
- ? routePropsOption(route)
- : routePropsOption
- : null
-
- const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
- // remove the instance reference to prevent leak
- if (vnode.component!.isUnmounted) {
- matchedRoute!.instances[currentName] = null
- }
- }
-
- function onPending(...args: any[]) {
- console.log('pending', ...args)
- }
-
- function onResolve(...args: any[]) {
- console.log('resolve', ...args)
- }
-
- function onFallback(...args: any[]) {
- console.log('fallback', ...args)
- }
-
- const component = h(
- ViewComponent,
- assign({}, routeProps, attrs, {
- onVnodeUnmounted,
- ref: viewRef,
- })
- )
-
- return isSuspended
- ? component
- : h(
- Suspense,
- {
- timeout: 0,
- onPending,
- onResolve,
- onFallback,
- },
- component
- )
- }
- },
-})
-
-// export the public type for h/tsx inference
-// also to avoid inline import() in generated d.ts files
-/**
- * Component to display the current route the user is at.
- */
-export const RouterViewSuspended = (RouterViewSuspendedImpl as any) as {
- new (): {
- $props: AllowedComponentProps &
- ComponentCustomProps &
- VNodeProps &
- RouterViewSuspendedProps
- }
-}
UseLinkOptions,
} from './RouterLink'
export { RouterView, RouterViewProps } from './RouterView'
-export {
- RouterViewSuspended,
- RouterViewSuspendedProps,
-} from './RouterViewSuspended'
export * from './useApi'