]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
wip: loader data fetching wip/loader-pattern-playground
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 7 Jul 2022 16:12:57 +0000 (18:12 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 7 Jul 2022 16:12:57 +0000 (18:12 +0200)
packages/playground/env.d.ts
packages/playground/src/api/index.ts
packages/playground/src/router.ts
packages/playground/src/views/UserDetail.vue [new file with mode: 0644]

index befb0fbf18cbdf7dd3ad73e0fd949e151da62a81..1b310ac1bbec66b651ec95e83324fce2ea4568d8 100644 (file)
@@ -1,8 +1,40 @@
 /// <reference types="vite/client" />
 /// <reference path="vue-router/global.d.ts"/>
 
-declare module '*.vue' {
-  import { Component } from 'vue'
-  var component: Component
-  export default component
+// declare module '*.vue' {
+//   import { Component } from 'vue'
+//   var component: Component
+//   export default component
+// }
+
+declare module '@vue-router' {
+  import type { Ref } from 'vue'
+
+  export interface LoaderResult<Load> {
+    data: Load extends _Loader<any, infer R> ? R : unknown
+    isLoading: Ref<boolean>
+  }
+
+  export function useLoader<Name extends RouteNames>(
+    name?: Name
+  ): RouteNamedMap[Name]
+
+  export interface _Loader<Params, Result> {
+    (to: { params: Params }): Promise<Result>
+  }
+
+  export function defineLoader<Name extends RouteNames, Result>(
+    name: Name,
+    loader: _Loader<RouteParams<Name>, Result>
+  ): _Loader<RouteParams<Name>, Result>
+
+  export type RouteNames = 'user' | 'admin' | 'home' | 'about'
+
+  export interface RouteNamedMap {
+    user: LoaderResult<typeof import('./src/views/UserDetail.vue').load>
+  }
+
+  export type RouteParams<Name extends RouteNames> = {
+    user: { id: string }
+  }[Name]
 }
index 628521c936f30469cd121bc8d0db022df05cfcac..825139baee1a23827a18419924d94e831e24be60 100644 (file)
@@ -9,3 +9,11 @@ export async function getData() {
     time: Date.now(),
   }
 }
+
+export async function getUserById(id: string) {
+  await delay(200)
+  return {
+    id: 1,
+    name: 'Eduardo',
+  }
+}
index 4270b6a1cfcb6447ca4aab9b3101030b3fae4851..80e4e32d313442d2433a755aadbd58b265674523 100644 (file)
@@ -1,5 +1,10 @@
-import { createRouter, createWebHistory, RouterView } from 'vue-router'
-import type { RouterLinkTyped } from 'vue-router'
+import {
+  createRouter,
+  createWebHistory,
+  RouterView,
+  type RouteLocationNormalized,
+  type RouteRecordRaw,
+} from 'vue-router'
 import Home from './views/Home.vue'
 import Nested from './views/Nested.vue'
 import NestedWithId from './views/NestedWithId.vue'
@@ -23,150 +28,186 @@ let removeRoute: (() => void) | undefined
 const TransparentWrapper: FunctionalComponent = () => h(RouterView)
 TransparentWrapper.displayName = 'NestedView'
 
-export const routerHistory = createWebHistory()
-export const router = createRouter({
-  history: routerHistory,
-  strict: true,
-  routes: [
-    { path: '/home', redirect: '/' },
-    {
-      path: '/',
-      components: { default: Home, other: component },
-      props: { default: to => ({ waited: to.meta.waitedFor }) },
+const routes: RouteRecordRaw[] = [
+  { path: '/home', redirect: '/' },
+  {
+    path: '/',
+    components: { default: Home, other: component },
+    props: { default: to => ({ waited: to.meta.waitedFor }) },
+  },
+  {
+    path: '/always-redirect',
+    redirect: () => ({
+      name: 'user',
+      params: { id: String(Math.round(Math.random() * 100)) },
+    }),
+  },
+  { path: '/users/:id', name: 'user', component: User, props: true },
+  { path: '/documents/:id', name: 'docs', component: User, props: true },
+  { path: '/optional/:id?', name: 'optional', component: User, props: true },
+  { path: encodeURI('/n/€'), name: 'euro', component },
+  { path: '/n/:n', name: 'increment', component },
+  { path: '/multiple/:a/:b', name: 'multiple', component },
+  { path: '/long-:n', name: 'long', component: LongView },
+  {
+    path: '/lazy',
+    meta: { transition: 'slide-left' },
+    component: async () => {
+      await delay(500)
+      return component
     },
-    {
-      path: '/always-redirect',
-      redirect: () => ({
-        name: 'user',
-        params: { id: String(Math.round(Math.random() * 100)) },
-      }),
+  },
+  {
+    path: '/with-guard/:n',
+    name: 'guarded',
+    component,
+    beforeEnter(to) {
+      if (to.params.n !== 'valid') return false
     },
-    { path: '/users/:id', name: 'user', component: User, props: true },
-    { path: '/documents/:id', name: 'docs', component: User, props: true },
-    { path: '/optional/:id?', name: 'optional', component: User, props: true },
-    { path: encodeURI('/n/€'), name: 'euro', component },
-    { path: '/n/:n', name: 'increment', component },
-    { path: '/multiple/:a/:b', name: 'multiple', component },
-    { path: '/long-:n', name: 'long', component: LongView },
-    {
-      path: '/lazy',
-      meta: { transition: 'slide-left' },
-      component: async () => {
-        await delay(500)
-        return component
+  },
+  { path: '/cant-leave', component: GuardedWithLeave },
+  {
+    path: '/children',
+    name: 'WithChildren',
+    component: Nested,
+    children: [
+      { path: '', alias: 'alias', name: 'default-child', component: Nested },
+      { path: 'a', name: 'a-child', component: Nested },
+      {
+        path: 'b',
+        name: 'WithChildrenB',
+        component: Nested,
+        children: [
+          {
+            path: '',
+            name: 'b-child',
+            component: Nested,
+          },
+          { path: 'a2', component: Nested },
+          { path: 'b2', component: Nested },
+        ],
       },
-    },
-    {
-      path: '/with-guard/:n',
-      name: 'guarded',
-      component,
-      beforeEnter(to) {
-        if (to.params.n !== 'valid') return false
+    ],
+  },
+  { path: '/with-data', component: ComponentWithData, name: 'WithData' },
+  { path: '/rep/:a*', component: RepeatedParams, name: 'repeat' },
+  { path: '/:data(.*)', component: NotFound, name: 'NotFound' },
+  {
+    path: '/nested',
+    alias: '/anidado',
+    component: Nested,
+    name: 'Nested',
+    children: [
+      {
+        path: 'nested',
+        alias: 'a',
+        name: 'NestedNested',
+        component: Nested,
+        children: [
+          {
+            name: 'NestedNestedNested',
+            path: 'nested',
+            component: Nested,
+          },
+        ],
       },
-    },
-    { path: '/cant-leave', component: GuardedWithLeave },
-    {
-      path: '/children',
-      name: 'WithChildren',
-      component: Nested,
-      children: [
-        { path: '', alias: 'alias', name: 'default-child', component: Nested },
-        { path: 'a', name: 'a-child', component: Nested },
-        {
-          path: 'b',
-          name: 'WithChildrenB',
-          component: Nested,
-          children: [
-            {
-              path: '',
-              name: 'b-child',
-              component: Nested,
-            },
-            { path: 'a2', component: Nested },
-            { path: 'b2', component: Nested },
-          ],
-        },
-      ],
-    },
-    { path: '/with-data', component: ComponentWithData, name: 'WithData' },
-    { path: '/rep/:a*', component: RepeatedParams, name: 'repeat' },
-    { path: '/:data(.*)', component: NotFound, name: 'NotFound' },
-    {
-      path: '/nested',
-      alias: '/anidado',
-      component: Nested,
-      name: 'Nested',
-      children: [
-        {
-          path: 'nested',
-          alias: 'a',
-          name: 'NestedNested',
-          component: Nested,
-          children: [
-            {
-              name: 'NestedNestedNested',
-              path: 'nested',
-              component: Nested,
-            },
-          ],
-        },
-        {
-          path: 'other',
-          alias: 'otherAlias',
-          component: Nested,
-          name: 'NestedOther',
-        },
-        {
-          path: 'also-as-absolute',
-          alias: '/absolute',
-          name: 'absolute-child',
-          component: Nested,
-        },
-      ],
-    },
-
-    {
-      path: '/parent/:id',
-      name: 'parent',
-      component: NestedWithId,
-      props: true,
-      alias: '/p/:id',
-      children: [
-        // empty child
-        { path: '', name: 'child-id', component },
-        // child with absolute path. we need to add an `id` because the parent needs it
-        { path: '/p_:id/absolute-a', alias: 'as-absolute-a', component },
-        // same as above but the alias is absolute
-        { path: 'as-absolute-b', alias: '/p_:id/absolute-b', component },
-      ],
-    },
-    {
-      path: '/dynamic',
-      name: 'dynamic',
-      component: Nested,
-      end: false,
-      strict: true,
-      beforeEnter(to) {
-        if (!removeRoute) {
-          removeRoute = router.addRoute('dynamic', {
-            path: 'child',
-            component: Dynamic,
-          })
-          return to.fullPath
-        }
+      {
+        path: 'other',
+        alias: 'otherAlias',
+        component: Nested,
+        name: 'NestedOther',
       },
-    },
+      {
+        path: 'also-as-absolute',
+        alias: '/absolute',
+        name: 'absolute-child',
+        component: Nested,
+      },
+    ],
+  },
 
-    {
-      path: '/admin',
-      component: TransparentWrapper,
-      children: [
-        { path: '', component },
-        { path: 'dashboard', component },
-        { path: 'settings', component },
-      ],
+  {
+    path: '/parent/:id',
+    name: 'parent',
+    component: NestedWithId,
+    props: true,
+    alias: '/p/:id',
+    children: [
+      // empty child
+      { path: '', name: 'child-id', component },
+      // child with absolute path. we need to add an `id` because the parent needs it
+      { path: '/p_:id/absolute-a', alias: 'as-absolute-a', component },
+      // same as above but the alias is absolute
+      { path: 'as-absolute-b', alias: '/p_:id/absolute-b', component },
+    ],
+  },
+  {
+    path: '/dynamic',
+    name: 'dynamic',
+    component: Nested,
+    end: false,
+    strict: true,
+    beforeEnter(to) {
+      if (!removeRoute) {
+        removeRoute = router.addRoute('dynamic', {
+          path: 'child',
+          component: Dynamic,
+        })
+        return to.fullPath
+      }
     },
-  ] as const,
+  },
+
+  {
+    path: '/admin',
+    component: TransparentWrapper,
+    children: [
+      { path: '', component },
+      { path: 'dashboard', component },
+      { path: 'settings', component },
+    ],
+  },
+]
+
+function mergeRouteProps(
+  record: Exclude<RouteRecordRaw, { redirect: any }>,
+  to: RouteLocationNormalized
+) {
+  const originalProps = record.props
+  // TODO: named views can have an object
+  const originalPropsResult =
+    typeof originalProps === 'function'
+      ? originalProps(to)
+      : typeof originalProps === 'boolean'
+      ? {}
+      : originalProps
+  return { ...originalPropsResult, ...to.meta.data }
+}
+
+function setPropsToData(record: RouteRecordRaw) {
+  if (!('redirect' in record)) {
+    const originalProps = record.props
+    record.props = mergeRouteProps.bind(null, record)
+  }
+  if (record.children) {
+    record.children.forEach(setPropsToData)
+  }
+}
+routes.forEach(setPropsToData)
+
+declare module 'vue-router' {
+  export interface RouteMeta {
+    pendingRoute?: RouteLocationNormalizedLoaded
+    load?: () => Promise<Record<any, unknown>> | Record<any, unknown>
+    data?: Record<any, unknown>
+  }
+}
+
+export const routerHistory = createWebHistory()
+export const router = createRouter({
+  history: routerHistory,
+  strict: true,
+  routes,
   async scrollBehavior(to, from, savedPosition) {
     await scrollWaiter.wait()
     if (savedPosition) {
@@ -181,13 +222,28 @@ export const router = createRouter({
   },
 })
 
-declare module 'vue-router' {
-  export interface Config {
-    Router: typeof router
+router.beforeEach((to, from) => {
+  delete from.meta.pendingRoute
+})
+router.beforeResolve(async (to, from) => {
+  if (to.meta.load) {
+    from.meta.pendingRoute = to
+    to.meta.data = await to.meta.load()
+  } else {
   }
-}
 
-// router.push({ name: 'user', params: {} })
+  from.meta.pendingRoute = to
+  const loaders = to.matched
+    .map(record =>
+      // TODO: avoid refetching if the route is already loaded
+      // Find a strategy to do it
+      record.meta.load?.()
+    )
+    .filter(Boolean)
+
+  const loadedData = await Promise.all(loaders)
+  loadedData.forEach((data, i) => {})
+})
 
 const delay = (t: number) => new Promise(resolve => setTimeout(resolve, t))
 
diff --git a/packages/playground/src/views/UserDetail.vue b/packages/playground/src/views/UserDetail.vue
new file mode 100644 (file)
index 0000000..d1d778f
--- /dev/null
@@ -0,0 +1,39 @@
+<script lang="ts">
+import { getUserById } from '../api'
+import { defineLoader } from '@vue-router'
+
+export const load = defineLoader('user', async route => {
+  const user = await getUserById(route.params.id)
+  //   // ...
+  return user
+})
+// Also tried this but then it cannot infer the ReturnType, it must be manually typed
+// export const load: Loader<'user'> = async ({ params }) => {
+//   const user = await getUserById(params.id)
+//   // ...
+//   return user
+// }
+</script>
+
+<script lang="ts" setup>
+import { useLoader } from '@vue-router'
+
+const { data: user, isLoading } =
+  // the argument has autocompletion and provides typed values
+  useLoader('user')
+
+// it is the same as useLoader<'/users/:id'>() but that doesn't autocomplete
+// user is always present, isLoading changes when going from '/users/2' to '/users/3'
+// note this can be removed during the build
+</script>
+
+<template>
+  <div>User: {{ user }}</div>
+  <p v-if="isLoading">Fetching the new user</p>
+</template>
+
+<!-- <script lang="ts" loader>
+const route = useRoute('user')
+
+const user = await getUserById(route.params.id)
+</script> -->