Skip to content

Commit

Permalink
feat(data-loaders): allow changing the navigation result
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Dec 19, 2023
1 parent fd038ee commit 7a7da74
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 82 deletions.
33 changes: 14 additions & 19 deletions src/data-fetching_new/createDataLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
import { IS_USE_DATA_LOADER_KEY, STAGED_NO_VALUE } from './symbols'
import { _Awaitable } from '../core/utils'
import { _PromiseMerged } from './utils'
import { NavigationResult } from './navigation-guard'

/**
* Base type for a data loader entry. Each Data Loader has its own entry in the `loaderEntries` (accessible via `[LOADER_ENTRIES_KEY]`) map.
Expand All @@ -17,22 +18,6 @@ export interface DataLoaderEntryBase<
isLazy extends boolean = boolean,
Data = unknown
> {
// route information
// TODO: should be moved to the cached loader version

/**
* Location's params that were used to load the data.
*/
params: Partial<RouteParams>
/**
* Location's query that was used to load the data.
*/
query: Partial<LocationQuery>
/**
* Location's hash that was used to load the data.
*/
hash: string | null

// state

/**
Expand All @@ -43,7 +28,7 @@ export interface DataLoaderEntryBase<
/**
* Error if there was an error.
*/
error: Ref<any> // any is simply more convenient for errors
error: ShallowRef<any> // any is simply more convenient for errors

// TODO: allow delaying pending? maybe allow passing a custom ref that can use refDebounced https://vueuse.org/shared/refDebounced/#refdebounced
/**
Expand Down Expand Up @@ -185,7 +170,6 @@ export interface DataLoaderContextBase {
*/
signal: AbortSignal
}
// export interface DataLoaderContext {}

export interface DefineDataLoader<Context extends DataLoaderContextBase> {
<isLazy extends boolean, Data>(
Expand Down Expand Up @@ -221,11 +205,18 @@ export interface UseDataLoader<
* })
* ```
*/
(): _PromiseMerged<Data, UseDataLoaderResult<isLazy, Data>>
(): _PromiseMerged<
Exclude<Data, NavigationResult>,
UseDataLoaderResult<isLazy, Exclude<Data, NavigationResult>>
>

_: UseDataLoaderInternals<isLazy, Data>
}

/**
* Internal properties of a data loader composable. Used by the internal implementation of `defineLoader()`. **Should
* not be used in application code.**
*/
export interface UseDataLoaderInternals<
isLazy extends boolean = boolean,
Data = unknown
Expand Down Expand Up @@ -254,6 +245,10 @@ export interface UseDataLoaderInternals<
getEntry(router: Router): DataLoaderEntryBase<isLazy, Data>
}

/**
* Generates the type for a `Ref` of a data loader based on the value of `lazy`.
* @internal
*/
export type _DataMaybeLazy<Data, isLazy extends boolean = boolean> =
// no lazy provided, default value is false
boolean extends isLazy ? Data : true extends isLazy ? Data | undefined : Data
Expand Down
30 changes: 29 additions & 1 deletion src/data-fetching_new/defineLoader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { setCurrentContext } from './utils'
import { enableAutoUnmount, mount } from '@vue/test-utils'
import { getRouter } from 'vue-router-mock'
import { DataLoaderPlugin } from './navigation-guard'
import { DataLoaderPlugin, NavigationResult } from './navigation-guard'
import { UseDataLoader } from './createDataLoader'
import { mockPromise, mockedLoader } from '~/tests/utils'
import RouterViewMock from '~/tests/data-loaders/RouterViewMock.vue'
Expand Down Expand Up @@ -839,11 +839,39 @@ dts(async () => {
expectType<{ data: Ref<UserData | undefined> }>(
defineLoader(loaderUser, { lazy: true })()
)
expectType<Promise<UserData>>(defineLoader(loaderUser, { lazy: true })())
expectType<Promise<UserData>>(defineLoader(loaderUser, {})())
expectType<{ data: Ref<UserData> }>(defineLoader(loaderUser, {})())
expectType<{ data: Ref<UserData> }>(
defineLoader(loaderUser, { lazy: false })()
)
expectType<{ data: Ref<UserData> }>(
defineLoader(loaderUser, { lazy: false })()
)

// it should allow returning a Navigation Result without a type error
expectType<{ data: Ref<UserData> }>(
defineLoader(
async () => {
if (Math.random()) {
return loaderUser()
} else {
return new NavigationResult('/')
}
},
{ lazy: false }
)()
)
expectType<Promise<UserData>>(
defineLoader(
async () => {
if (Math.random()) {
return loaderUser()
} else {
return new NavigationResult('/')
}
},
{ lazy: false }
)()
)
})
44 changes: 21 additions & 23 deletions src/data-fetching_new/defineLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ import {
APP_KEY,
IS_USE_DATA_LOADER_KEY,
LOADER_ENTRIES_KEY,
NAVIGATION_RESULTS_KEY,
STAGED_NO_VALUE,
} from './symbols'
import {
IS_CLIENT,
assign,
getCurrentContext,
setCurrentContext,
withinScope,
} from './utils'
import { Ref, UnwrapRef, ref } from 'vue'
import { Ref, UnwrapRef, ref, shallowRef } from 'vue'
import { NavigationResult } from './navigation-guard'

export function defineLoader<
P extends Promise<unknown>,
Expand Down Expand Up @@ -182,7 +183,12 @@ export function defineLoader<
}
// if the entry is null, it means the loader never resolved, maybe there was an error
if (this.staged !== STAGED_NO_VALUE) {
this.data.value = this.staged
// collect navigation results instead of setting the data
if (this.staged instanceof NavigationResult) {
to.meta[NAVIGATION_RESULTS_KEY]!.push(this.staged)
} else {
this.data.value = this.staged
}
}
this.staged = STAGED_NO_VALUE
this.pendingTo = null
Expand Down Expand Up @@ -321,26 +327,18 @@ export function createDefineLoaderEntry<
) => void,
initialData?: Data
): DataLoaderEntryBase<isLazy, Data> {
// TODO: the scope should be passed somehow and be unique per application
return withinScope<DataLoaderEntryBase<isLazy, Data>>(
() =>
({
// force the type to match
data: ref(initialData) as Ref<_DataMaybeLazy<UnwrapRef<Data>, isLazy>>,
pending: ref(false),
error: ref<any>(),

params: {},
query: {},
hash: null,

children: new Set(),
pendingLoad: null,
pendingTo: null,
staged: STAGED_NO_VALUE,
commit,
} satisfies DataLoaderEntryBase<isLazy, Data>)
)
return {
// force the type to match
data: ref(initialData) as Ref<_DataMaybeLazy<UnwrapRef<Data>, isLazy>>,
pending: ref(false),
error: shallowRef<any>(),

children: new Set(),
pendingLoad: null,
pendingTo: null,
staged: STAGED_NO_VALUE,
commit,
} satisfies DataLoaderEntryBase<isLazy, Data>
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/data-fetching_new/meta-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import type {
LOADER_SET_KEY,
PENDING_LOCATION_KEY,
ABORT_CONTROLLER_KEY,
NAVIGATION_RESULTS_KEY,
} from './symbols'
import { NavigationResult } from './navigation-guard'

/**
* Map type for the entries used by data loaders.
Expand Down Expand Up @@ -62,6 +64,12 @@ declare module 'vue-router' {
* @internal
*/
[ABORT_CONTROLLER_KEY]?: AbortController

/**
* The navigation results when the navigation is canceled by the user within a data loader.
* @internal
*/
[NAVIGATION_RESULTS_KEY]?: NavigationResult[]
}
}

Expand Down
83 changes: 81 additions & 2 deletions src/data-fetching_new/navigation-guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { App, createApp, defineComponent } from 'vue'
import { defineLoader } from './defineLoader'
import {
Mock,
afterAll,
afterEach,
beforeAll,
Expand All @@ -15,7 +16,7 @@ import {
} from 'vitest'
import { setCurrentContext } from './utils'
import { getRouter } from 'vue-router-mock'
import { DataLoaderPlugin } from './navigation-guard'
import { DataLoaderPlugin, NavigationResult } from './navigation-guard'
import { mockedLoader } from '~/tests/utils'
import { ABORT_CONTROLLER_KEY, LOADER_SET_KEY } from './symbols'
import {
Expand Down Expand Up @@ -43,11 +44,18 @@ vi.mock(

describe('navigation-guard', () => {
let app: App | undefined
let selectNavigationResult!: Mock
beforeEach(() => {
// @ts-expect-error: normally not allowed
_utils.IS_CLIENT = true
app = createApp({ render: () => null })
app.use(DataLoaderPlugin, { router: getRouter() })
selectNavigationResult = vi
.fn()
.mockImplementation((results) => results[0].value)
app.use(DataLoaderPlugin, {
router: getRouter(),
selectNavigationResult,
})
// invalidate current context
setCurrentContext(undefined)
})
Expand Down Expand Up @@ -354,4 +362,75 @@ describe('navigation-guard', () => {
expect(signal.reason).toBe(reason)
})
})

describe('selectNavigationResult', () => {
it('can change the navigation result within a loader', async () => {
const router = getRouter()
const l1 = mockedLoader()
router.addRoute({
name: '_test',
path: '/fetch',
component,
meta: {
loaders: [l1.loader],
},
})

router.push('/fetch')
await vi.runOnlyPendingTimersAsync()
l1.resolve(new NavigationResult('/#ok'))
await router.getPendingNavigation()
expect(router.currentRoute.value.fullPath).toBe('/#ok')
})

it('selectNavigationResult is called with an array of all the results returned by the loaders', async () => {
const router = getRouter()
const l1 = mockedLoader()
const l2 = mockedLoader()
const l3 = mockedLoader()
router.addRoute({
name: '_test',
path: '/fetch',
component,
meta: {
loaders: [l1.loader, l2.loader, l3.loader],
},
})

router.push('/fetch')
await vi.runOnlyPendingTimersAsync()
const r1 = new NavigationResult('/#ok')
const r2 = new NavigationResult('/#ok2')
l1.resolve(r1)
l2.resolve('some data')
l3.resolve(r2)
await router.getPendingNavigation()
expect(selectNavigationResult).toHaveBeenCalledTimes(1)
expect(selectNavigationResult).toHaveBeenCalledWith([r1, r2])
})

it('can change the navigation result returned by multiple loaders', async () => {
const router = getRouter()
const l1 = mockedLoader()
const l2 = mockedLoader()
router.addRoute({
name: '_test',
path: '/fetch',
component,
meta: {
loaders: [l1.loader, l2.loader],
},
})

selectNavigationResult.mockImplementation(() => true)
router.push('/fetch')
await vi.runOnlyPendingTimersAsync()
const r1 = new NavigationResult('/#ok')
const r2 = new NavigationResult('/#ok2')
l1.resolve(r1)
l2.resolve(r2)
await router.getPendingNavigation()
expect(router.currentRoute.value.fullPath).toBe('/fetch')
})
})
})
Loading

0 comments on commit 7a7da74

Please sign in to comment.