From: Eduardo San Martin Morote Date: Thu, 7 Jul 2022 16:12:57 +0000 (+0200) Subject: wip: loader data fetching X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fheads%2Fwip%2Floader-pattern-playground;p=thirdparty%2Fvuejs%2Frouter.git wip: loader data fetching --- diff --git a/packages/playground/env.d.ts b/packages/playground/env.d.ts index befb0fbf..1b310ac1 100644 --- a/packages/playground/env.d.ts +++ b/packages/playground/env.d.ts @@ -1,8 +1,40 @@ /// /// -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 { + data: Load extends _Loader ? R : unknown + isLoading: Ref + } + + export function useLoader( + name?: Name + ): RouteNamedMap[Name] + + export interface _Loader { + (to: { params: Params }): Promise + } + + export function defineLoader( + name: Name, + loader: _Loader, Result> + ): _Loader, Result> + + export type RouteNames = 'user' | 'admin' | 'home' | 'about' + + export interface RouteNamedMap { + user: LoaderResult + } + + export type RouteParams = { + user: { id: string } + }[Name] } diff --git a/packages/playground/src/api/index.ts b/packages/playground/src/api/index.ts index 628521c9..825139ba 100644 --- a/packages/playground/src/api/index.ts +++ b/packages/playground/src/api/index.ts @@ -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', + } +} diff --git a/packages/playground/src/router.ts b/packages/playground/src/router.ts index 4270b6a1..80e4e32d 100644 --- a/packages/playground/src/router.ts +++ b/packages/playground/src/router.ts @@ -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, + 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 + data?: Record + } +} + +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 index 00000000..d1d778f4 --- /dev/null +++ b/packages/playground/src/views/UserDetail.vue @@ -0,0 +1,39 @@ + + + + + + +