--- /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>
+ </head>
+ <body>
+ <a href="/"><< Back to Homepage</a>
+ <hr />
+
+ <main id="app"></main>
+ </body>
+</html>
--- /dev/null
+import { createRouter, createWebHistory } from '../../src'
+import { createApp, defineComponent, onErrorCaptured } from 'vue'
+
+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 ViewData = defineComponent({
+ template: `
+ <div>
+ <h1>With Data</h1>
+
+ <router-view/>
+
+ </div>
+ `,
+
+ async setup() {
+ await delay(300)
+
+ throw new Error('oops')
+
+ return {}
+ },
+})
+
+const router = createRouter({
+ history: createWebHistory('/' + __dirname),
+ routes: [
+ { path: '/', component: Home },
+ {
+ path: '/data',
+ component: ViewData,
+ children: [
+ { path: '', component: ViewRegular },
+ { path: 'data', component: ViewData },
+ ],
+ },
+ {
+ path: '/data-2',
+ component: ViewData,
+ children: [
+ { path: '', component: ViewRegular },
+ { path: 'data', component: ViewData },
+ ],
+ },
+ ],
+})
+
+const app = createApp({
+ setup() {
+ function onPending() {
+ console.log('onPending')
+ }
+ function onResolve() {
+ console.log('onResolve')
+ }
+ function onFallback() {
+ console.log('onFallback')
+ }
+
+ onErrorCaptured((err, target, info) => {
+ console.log('caught', err, target, info)
+ })
+
+ return { onPending, onResolve, onFallback }
+ },
+
+ template: `
+ <div id="app">
+ <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="/data-2">Suspended (2)</router-link></li>
+ <li><router-link to="/data-2/data">Suspended nested (2)</router-link></li>
+ </ul>
+
+ <router-view-suspended />
+
+ </div>
+ `,
+})
+app.use(router)
+
+window.vm = app.mount('#app')
+window.r = router
--- /dev/null
+import {
+ h,
+ inject,
+ provide,
+ defineComponent,
+ PropType,
+ ref,
+ ComponentPublicInstance,
+ VNodeProps,
+ computed,
+ AllowedComponentProps,
+ ComponentCustomProps,
+ watch,
+} 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 RouterViewProps {
+ name?: string
+ // allow looser type for user facing api
+ route?: RouteLocationNormalized
+}
+
+export const RouterViewSuspendedImpl = /*#__PURE__*/ defineComponent({
+ name: 'RouterView',
+ // #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 suspendedRoute = inject(suspendedRouteKey)!
+ const routeToDisplay = computed(() => props.route || injectedRoute.value)
+ const depth = inject(viewDepthKey, 0)
+ const matchedRouteRef = computed<RouteLocationMatched | undefined>(
+ () => routeToDisplay.value.matched[depth]
+ )
+
+ provide(viewDepthKey, depth + 1)
+ provide(matchedRouteKey, matchedRouteRef)
+ provide(routerViewLocationKey, routeToDisplay)
+
+ 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 ViewComponent = matchedRoute && matchedRoute.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
+
+ 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
+ }
+ }
+
+ const component = h(
+ ViewComponent,
+ assign({}, routeProps, attrs, {
+ onVnodeUnmounted,
+ ref: viewRef,
+ })
+ )
+
+ return 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 &
+ RouterViewProps
+ }
+}
__DEV__ ? 'route location' : 'rl'
) as InjectionKey<RouteLocationNormalizedLoaded>
+export const suspendedRouteKey = /*#__PURE__*/ PolySymbol(
+ __DEV__ ? 'suspended route location' : 'srl'
+) as InjectionKey<Ref<RouteLocationNormalizedLoaded | null>>
+
/**
* Allows overriding the current route used by router-view. Internally this is
* used when the `route` prop is passed.
import { warn } from './warning'
import { RouterLink } from './RouterLink'
import { RouterView } from './RouterView'
+import { RouterViewSuspended } from './RouterViewSuspended'
import {
routeLocationKey,
routerKey,
routerViewLocationKey,
+ suspendedRouteKey,
} from './injectionSymbols'
import { addDevtools } from './devtools'
START_LOCATION_NORMALIZED
)
let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED
+ const suspendedRoute = shallowRef<RouteLocationNormalizedLoaded | null>()
// leave the scrollRestoration if no scrollBehavior is provided
if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) {
const router = this
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
+ app.component('RouterViewSuspended', RouterViewSuspended)
app.config.globalProperties.$router = router
Object.defineProperty(app.config.globalProperties, '$route', {
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
+ app.provide(suspendedRouteKey, suspendedRoute)
app.provide(routerViewLocationKey, currentRoute)
let unmountApp = app.unmount