]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(view): support suspense
authorEduardo San Martin Morote <posva13@gmail.com>
Sat, 23 Jan 2021 17:11:45 +0000 (18:11 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 28 Jun 2021 15:10:17 +0000 (17:10 +0200)
e2e/suspended-router-view/index.html [new file with mode: 0644]
e2e/suspended-router-view/index.ts [new file with mode: 0644]
src/RouterViewSuspended.ts [new file with mode: 0644]
src/injectionSymbols.ts
src/router.ts

diff --git a/e2e/suspended-router-view/index.html b/e2e/suspended-router-view/index.html
new file mode 100644 (file)
index 0000000..fb3729b
--- /dev/null
@@ -0,0 +1,17 @@
+<!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="/">&lt;&lt; Back to Homepage</a>
+    <hr />
+
+    <main id="app"></main>
+  </body>
+</html>
diff --git a/e2e/suspended-router-view/index.ts b/e2e/suspended-router-view/index.ts
new file mode 100644 (file)
index 0000000..24c8007
--- /dev/null
@@ -0,0 +1,96 @@
+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
diff --git a/src/RouterViewSuspended.ts b/src/RouterViewSuspended.ts
new file mode 100644 (file)
index 0000000..0d91b9f
--- /dev/null
@@ -0,0 +1,150 @@
+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
+  }
+}
index 0d345ea1f9ad12ba7df2027675cfa21c81d8520f..6cff569c9c2e2f2a8ad8132d65938cd82585e56e 100644 (file)
@@ -54,6 +54,10 @@ export const routeLocationKey = /*#__PURE__*/ PolySymbol(
   __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.
index 4895b884c98195520a940e9ff61f2e91ac50b05c..3010ba3a35621f3ebe23ba433f7324d49ee8944b 100644 (file)
@@ -61,10 +61,12 @@ import { extractComponentsGuards, guardToPromiseFn } from './navigationGuards'
 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'
 
@@ -369,6 +371,7 @@ export function createRouter(options: RouterOptions): Router {
     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) {
@@ -1153,6 +1156,7 @@ export function createRouter(options: RouterOptions): Router {
       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', {
@@ -1189,6 +1193,7 @@ export function createRouter(options: RouterOptions): Router {
 
       app.provide(routerKey, router)
       app.provide(routeLocationKey, reactive(reactiveRoute))
+      app.provide(suspendedRouteKey, suspendedRoute)
       app.provide(routerViewLocationKey, currentRoute)
 
       let unmountApp = app.unmount